Skip to content

Commit

Permalink
Merge pull request #770 from safe-global/feat/add-eip1193-provider
Browse files Browse the repository at this point in the history
  • Loading branch information
yagopv authored May 8, 2024
2 parents ef05dd7 + e0aeefd commit b718932
Show file tree
Hide file tree
Showing 211 changed files with 3,998 additions and 10,165 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: e2e Test
name: API Kit - E2E Tests
on:
pull_request:
push:
Expand All @@ -18,7 +18,15 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: yarn
- run: |
yarn install --frozen-lockfile
yarn build
yarn test:ci
- name: Yarn install
run: yarn install --frozen-lockfile

- name: Build
run: yarn build

- name: Test
run: |
cd packages/api-kit
yarn test:ci:ethers
yarn test:ci:web3
yarn test:ci:viem
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
name: Safe Core SDK Test - Contracts
name: Protocol Kit - E2E Tests
on:
workflow_dispatch:
pull_request:
push:
branches:
Expand All @@ -11,7 +12,7 @@ jobs:
strategy:
matrix:
node-version: [20.x]
provider: [ethers, web3]
provider: [ethers, web3, viem]
contract-version: [v1.0.0, v1.1.1, v1.2.0, v1.3.0, v1.4.1]
steps:
- uses: actions/checkout@v4
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Monorepo Test
name: SDK - Unit Tests
on:
pull_request:
push:
Expand Down
89 changes: 44 additions & 45 deletions guides/integrating-the-safe-core-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,14 @@ To integrate the [Safe Core SDK](https://github.com/safe-global/safe-core-sdk) i

## <a name="initialize-sdks">2. Initialize the SDK’s</a>

### Instantiate an EthAdapter
### Select your Ethereum `provider` and `signer`

First of all, we need to create an `EthAdapter`, which contains all the required utilities for the SDKs to interact with the blockchain. It acts as a wrapper for [web3.js](https://web3js.readthedocs.io/) or [ethers.js](https://docs.ethers.org/v6/) Ethereum libraries.
To use our kits, you need to provide an Ethereum provider and a signer. The provider is the connection to the Ethereum network, while the signer is an account that will sign the transactions (a Safe owner). When using an injected provider like MetaMask, the signer is the account selected in the wallet.

Depending on the library used by the Dapp, there are two options:
In the examples below, you can see `provider` and `signer` properties, which represent:

- [Create an `EthersAdapter` instance](https://github.com/safe-global/safe-core-sdk/tree/main/packages/protocol-kit/src/adapters/ethers)
- [Create a `Web3Adapter` instance](https://github.com/safe-global/safe-core-sdk/tree/main/packages/protocol-kit/src/adapters/web3)

Once the instance of `EthersAdapter` or `Web3Adapter` is created, it can be used in the SDK initialization.
- `provider`: You can provide an EIP-1193 compatible provider or an HTTP/WebSocket RPC URL.
- `signer`: This is an optional parameter. It should be the provider's address you want to use or a private key. If not set, it will try to fetch a connected account from the provider.

### Initialize the Safe API Kit

Expand All @@ -59,27 +57,28 @@ const safeService = new SafeApiKit({
```js
import Safe, { SafeFactory } from '@safe-global/protocol-kit'

const safeFactory = await SafeFactory.create({ ethAdapter })
const safeFactory = await SafeFactory.create({ provider, signer })

const safeSdk = await Safe.create({ ethAdapter, safeAddress })
const safeSdk = await Safe.create({ provider, signer, safeAddress })
```

There are two versions of the Safe contracts: [Safe.sol](https://github.com/safe-global/safe-contracts/blob/v1.4.1/contracts/Safe.sol) that does not trigger events in order to save gas and [SafeL2.sol](https://github.com/safe-global/safe-contracts/blob/v1.4.1/contracts/SafeL2.sol) that does, which is more appropriate for L2 networks.

By default `Safe.sol` will be only used on Ethereum Mainnet. For the rest of the networks where the Safe contracts are already deployed, the `SafeL2.sol` contract will be used unless you add the property `isL1SafeSingleton` to force the use of the `Safe.sol` contract.

```js
const safeFactory = await SafeFactory.create({ ethAdapter, isL1SafeSingleton: true })
const safeFactory = await SafeFactory.create({ provider, signer, isL1SafeSingleton: true })

const safeSdk = await Safe.create({ ethAdapter, safeAddress, isL1SafeSingleton: true })
const safeSdk = await Safe.create({ provider, signer, safeAddress, isL1SafeSingleton: true })
```

If the Safe contracts are not deployed to your current network, the property `contractNetworks` will be required to point to the addresses of the Safe contracts previously deployed by you.

```js
import { ContractNetworksConfig } from '@safe-global/protocol-kit'
import { ContractNetworksConfig, SafeProvider } from '@safe-global/protocol-kit'

const chainId = await ethAdapter.getChainId()
const safeProvider = new SafeProvider({ provider, signer })
const chainId = await safeProvider.getChainId()
const contractNetworks: ContractNetworksConfig = {
[chainId]: {
safeSingletonAddress: '<SINGLETON_ADDRESS>',
Expand All @@ -101,16 +100,16 @@ const contractNetworks: ContractNetworksConfig = {
}
}

const safeFactory = await SafeFactory.create({ ethAdapter, contractNetworks })
const safeFactory = await SafeFactory.create({ provider, signer, contractNetworks })

const safeSdk = await Safe.create({ ethAdapter, safeAddress, contractNetworks })
const safeSdk = await Safe.create({ provider, signer, safeAddress, contractNetworks })
```

The `SafeFactory` constructor also accepts the property `safeVersion` to specify the Safe contract version that will be deployed. This string can take the values `1.0.0`, `1.1.1`, `1.2.0`, `1.3.0` or `1.4.1`. If not specified, the `DEFAULT_SAFE_VERSION` value will be used.

```js
const safeVersion = 'X.Y.Z'
const safeFactory = await SafeFactory.create({ ethAdapter, safeVersion })
const safeFactory = await SafeFactory.create({ provider, signer, safeVersion })
```

## <a name="deploy-safe">3. Deploy a new Safe</a>
Expand Down Expand Up @@ -140,37 +139,37 @@ This method takes an array of `MetaTransactionData` objects that represent the i

When the array contains only one transaction, it is not wrapped in the MultiSend.

```js
import { SafeTransactionOptionalProps } from '@safe-global/protocol-kit'
import { MetaTransactionData } from '@safe-global/safe-core-sdk-types'

const transactions: MetaTransactionData[] = [
{
to,
data,
value,
operation
},
{
to,
data,
value,
operation
}
// ...
]

const options: SafeTransactionOptionalProps = {
safeTxGas, // Optional
baseGas, // Optional
gasPrice, // Optional
gasToken, // Optional
refundReceiver, // Optional
nonce // Optional
```js
import { SafeTransactionOptionalProps } from '@safe-global/protocol-kit'
import { MetaTransactionData } from '@safe-global/safe-core-sdk-types'

const transactions: MetaTransactionData[] = [
{
to,
data,
value,
operation
},
{
to,
data,
value,
operation
}
// ...
]

const options: SafeTransactionOptionalProps = {
safeTxGas, // Optional
baseGas, // Optional
gasPrice, // Optional
gasToken, // Optional
refundReceiver, // Optional
nonce // Optional
}

const safeTransaction = await safeSdk.createTransaction({ transactions, options })
```
const safeTransaction = await safeSdk.createTransaction({ transactions, options })
```

We can specify the `nonce` of our Safe transaction as long as it is not lower than the current Safe nonce. If multiple transactions are created but not executed they will share the same `nonce` if no `nonce` is specified, validating the first executed transaction and invalidating all the rest. We can prevent this by calling the method `getNextNonce` from the Safe API Kit instance. This method takes all queued/pending transactions into account when calculating the next nonce, creating a unique one for all different transactions.

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"build": "lerna run build --stream",
"lint:check": "eslint './packages/**/*.{js,jsx,ts,tsx}'",
"test": "FORCE_COLOR=1 lerna run test --stream",
"test:ci": "FORCE_COLOR=1 lerna run test:ci --stream",
"play": "ts-node ./playground/config/run.ts",
"format": "lerna run format && prettier --write \"playground/**/*.ts\"",
"prepare": "husky install"
Expand Down
75 changes: 40 additions & 35 deletions packages/account-abstraction-kit/src/AccountAbstraction.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Safe, { predictSafeAddress, EthAdapter } from '@safe-global/protocol-kit'
import Safe, { predictSafeAddress, SafeProvider } from '@safe-global/protocol-kit'
import { GelatoRelayPack } from '@safe-global/relay-kit'
import { SafeTransaction } from '@safe-global/safe-core-sdk-types'
import AccountAbstraction from './AccountAbstraction'
Expand All @@ -9,67 +9,72 @@ jest.mock('@safe-global/relay-kit')
const GelatoRelayPackMock = GelatoRelayPack as jest.MockedClass<typeof GelatoRelayPack>
const predictSafeAddressMock = predictSafeAddress as jest.MockedFunction<typeof predictSafeAddress>
const SafeMock = Safe as jest.MockedClass<typeof Safe>
const SafeProviderMock = SafeProvider as jest.MockedClass<typeof SafeProvider>

describe('AccountAbstraction', () => {
const ethersAdapter = {
getSignerAddress: jest.fn(),
isContractDeployed: jest.fn(),
getChainId: jest.fn()
const provider = {
request: jest.fn()
}

const signerAddress = '0xSignerAddress'
const predictSafeAddress = '0xPredictSafeAddressMock'

beforeEach(() => {
jest.clearAllMocks()
ethersAdapter.getSignerAddress.mockResolvedValue(signerAddress)
predictSafeAddressMock.mockResolvedValue(predictSafeAddress)
SafeProviderMock.prototype.getSignerAddress.mockResolvedValue(signerAddress)
})

describe('init', () => {
const accountAbstraction = new AccountAbstraction(ethersAdapter as unknown as EthAdapter)
const accountAbstraction = new AccountAbstraction({ provider, signer: signerAddress })

it('should initialize a Safe instance with its address if contract is deployed already', async () => {
ethersAdapter.isContractDeployed.mockResolvedValueOnce(true)
SafeProviderMock.prototype.isContractDeployed.mockResolvedValueOnce(true)

await accountAbstraction.init()

expect(ethersAdapter.getSignerAddress).toHaveBeenCalledTimes(1)
expect(SafeProviderMock.prototype.getSignerAddress).toHaveBeenCalledTimes(1)
expect(predictSafeAddressMock).toHaveBeenCalledTimes(1)
expect(predictSafeAddressMock).toHaveBeenCalledWith({
ethAdapter: ethersAdapter,
safeAccountConfig: { owners: ['0xSignerAddress'], threshold: 1 }
})
expect(predictSafeAddressMock).toHaveBeenCalledWith(
expect.objectContaining({
safeAccountConfig: { owners: ['0xSignerAddress'], threshold: 1 }
})
)
expect(SafeMock.create).toHaveBeenCalledTimes(1)
expect(SafeMock.create).toHaveBeenCalledWith({
ethAdapter: ethersAdapter,
safeAddress: predictSafeAddress
})
expect(SafeMock.create).toHaveBeenCalledWith(
expect.objectContaining({
safeAddress: predictSafeAddress
})
)
})

it('should initialize a Safe instance with a config if contract is NOT deployed yet', async () => {
ethersAdapter.isContractDeployed.mockResolvedValueOnce(false)
SafeProviderMock.prototype.isContractDeployed.mockResolvedValueOnce(false)

await accountAbstraction.init()

expect(ethersAdapter.getSignerAddress).toHaveBeenCalledTimes(1)
expect(SafeProviderMock.prototype.getSignerAddress).toHaveBeenCalledTimes(1)
expect(predictSafeAddressMock).toHaveBeenCalledTimes(1)
expect(predictSafeAddressMock).toHaveBeenCalledWith({
ethAdapter: ethersAdapter,
safeAccountConfig: { owners: ['0xSignerAddress'], threshold: 1 }
})
expect(predictSafeAddressMock).toHaveBeenCalledWith(
expect.objectContaining({
safeAccountConfig: { owners: ['0xSignerAddress'], threshold: 1 }
})
)
expect(SafeMock.create).toHaveBeenCalledTimes(1)
expect(SafeMock.create).toHaveBeenCalledWith({
ethAdapter: ethersAdapter,
predictedSafe: { safeAccountConfig: { owners: ['0xSignerAddress'], threshold: 1 } }
})
expect(SafeMock.create).toHaveBeenCalledWith(
expect.objectContaining({
predictedSafe: { safeAccountConfig: { owners: ['0xSignerAddress'], threshold: 1 } }
})
)
})

it('should throw an error if the provider has not a signer', async () => {
ethersAdapter.getSignerAddress.mockResolvedValueOnce(undefined)
SafeProviderMock.prototype.getSignerAddress.mockResolvedValueOnce(undefined)

expect(accountAbstraction.init()).rejects.toThrow(
`There's no signer in the provided EthAdapter`
`There's no signer available with the provided config (provider, signer)`
)

expect(SafeMock.create).not.toHaveBeenCalled()
})
})
Expand All @@ -83,7 +88,7 @@ describe('AccountAbstraction', () => {
}

const initAccountAbstraction = async () => {
const accountAbstraction = new AccountAbstraction(ethersAdapter as unknown as EthAdapter)
const accountAbstraction = new AccountAbstraction({ provider })
await accountAbstraction.init()
return accountAbstraction
}
Expand All @@ -107,7 +112,7 @@ describe('AccountAbstraction', () => {
})

it('should not be called if the protocol-kit is not initialized', async () => {
const accountAbstraction = new AccountAbstraction(ethersAdapter as unknown as EthAdapter)
const accountAbstraction = new AccountAbstraction({ provider })
expect(accountAbstraction.protocolKit).toBe(undefined)
expect(safeInstanceMock.getNonce).not.toHaveBeenCalled()
})
Expand All @@ -124,7 +129,7 @@ describe('AccountAbstraction', () => {
})

it('should not be called if the protocol-kit is not initialized', async () => {
const accountAbstraction = new AccountAbstraction(ethersAdapter as unknown as EthAdapter)
const accountAbstraction = new AccountAbstraction({ provider })
expect(accountAbstraction.protocolKit).toBe(undefined)
expect(safeInstanceMock.getAddress).not.toHaveBeenCalled()
})
Expand All @@ -139,7 +144,7 @@ describe('AccountAbstraction', () => {
})

it('should not be called if the protocol-kit is not initialized', async () => {
const accountAbstraction = new AccountAbstraction(ethersAdapter as unknown as EthAdapter)
const accountAbstraction = new AccountAbstraction({ provider })
expect(accountAbstraction.protocolKit).toBe(undefined)
expect(safeInstanceMock.isSafeDeployed).not.toHaveBeenCalled()
})
Expand Down Expand Up @@ -181,7 +186,7 @@ describe('AccountAbstraction', () => {
})

it('should throw if the protocol-kit is not initialized', async () => {
const accountAbstraction = new AccountAbstraction(ethersAdapter as unknown as EthAdapter)
const accountAbstraction = new AccountAbstraction({ provider })
accountAbstraction.setRelayKit(
new GelatoRelayPack({ protocolKit: accountAbstraction.protocolKit })
)
Expand All @@ -196,7 +201,7 @@ describe('AccountAbstraction', () => {
})

it('should throw if relay-kit is not initialized', async () => {
const accountAbstraction = new AccountAbstraction(ethersAdapter as unknown as EthAdapter)
const accountAbstraction = new AccountAbstraction({ provider })
await accountAbstraction.init()

expect(accountAbstraction.relayTransaction(transactionsMock, optionsMock)).rejects.toThrow(
Expand Down
Loading

0 comments on commit b718932

Please sign in to comment.