Skip to content

Commit

Permalink
Merge 6e213c4 into 9b282e1
Browse files Browse the repository at this point in the history
  • Loading branch information
yagopv committed Nov 22, 2023
2 parents 9b282e1 + 6e213c4 commit 14f137c
Show file tree
Hide file tree
Showing 193 changed files with 5,032 additions and 8,248 deletions.
55 changes: 20 additions & 35 deletions guides/integrating-the-safe-core-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ To integrate the [Safe Core SDK](https://github.com/safe-global/safe-core-sdk) i

### Instantiate an EthAdapter

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.io/v5/) Ethereum libraries.
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.

Depending on the library used by the Dapp, there are two options:

Expand All @@ -42,8 +42,16 @@ As stated in the introduction, the [Safe API Kit](https://github.com/safe-global
```js
import SafeApiKit from '@safe-global/api-kit'

const txServiceUrl = 'https://safe-transaction-mainnet.safe.global'
const safeService = new SafeApiKit({ txServiceUrl, ethAdapter })
const safeService = new SafeApiKit({ chainId })
```

Using the `chainId` is enough for chains where Safe runs a Transaction Service. For those chains where Safe doesn't run a service, use the `txServiceUrl` parameter to set the custom service endpoint.

```js
const safeService = new SafeApiKit({
chainId,
txServiceUrl: 'https://txServiceUrl.com'
})
```

### Initialize the Protocol Kit
Expand All @@ -58,12 +66,12 @@ const safeSdk = await Safe.create({ ethAdapter, 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 `isL1SafeMasterCopy` to force the use of the `Safe.sol` contract.
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, isL1SafeMasterCopy: true })
const safeFactory = await SafeFactory.create({ ethAdapter, isL1SafeSingleton: true })

const safeSdk = await Safe.create({ ethAdapter, safeAddress, isL1SafeMasterCopy: true })
const safeSdk = await Safe.create({ ethAdapter, 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.
Expand All @@ -74,15 +82,15 @@ import { ContractNetworksConfig } from '@safe-global/protocol-kit'
const chainId = await ethAdapter.getChainId()
const contractNetworks: ContractNetworksConfig = {
[chainId]: {
safeMasterCopyAddress: '<MASTER_COPY_ADDRESS>',
safeSingletonAddress: '<SINGLETON_ADDRESS>',
safeProxyFactoryAddress: '<PROXY_FACTORY_ADDRESS>',
multiSendAddress: '<MULTI_SEND_ADDRESS>',
multiSendCallOnlyAddress: '<MULTI_SEND_CALL_ONLY_ADDRESS>',
fallbackHandlerAddress: '<FALLBACK_HANDLER_ADDRESS>',
signMessageLibAddress: '<SIGN_MESSAGE_LIB_ADDRESS>',
createCallAddress: '<CREATE_CALL_ADDRESS>',
simulateTxAccessorAddress: '<SIMULATE_TX_ACCESSOR_ADDRESS>',
safeMasterCopyAbi: '<MASTER_COPY_ABI>', // Optional. Only needed with web3.js
safeSingletonAbi: '<SINGLETON_ABI>', // Optional. Only needed with web3.js
safeProxyFactoryAbi: '<PROXY_FACTORY_ABI>', // Optional. Only needed with web3.js
multiSendAbi: '<MULTI_SEND_ABI>', // Optional. Only needed with web3.js
multiSendCallOnlyAbi: '<MULTI_SEND_CALL_ONLY_ABI>', // Optional. Only needed with web3.js
Expand Down Expand Up @@ -128,38 +136,15 @@ Calling the method `deploySafe` will deploy the desired Safe and return a Protoc

The Protocol Kit supports the execution of single Safe transactions but also MultiSend transactions. We can create a transaction object by calling the method `createTransaction` in our `Safe` instance.

- **Create a single transaction**

This method can take an object of type `SafeTransactionDataPartial` that represents the transaction we want to execute (once the signatures are collected). It accepts some optional properties as follows.

```js
import { SafeTransactionDataPartial } from '@safe-global/safe-core-sdk-types'

const safeTransactionData: SafeTransactionDataPartial = {
to,
data,
value,
operation, // Optional
safeTxGas, // Optional
baseGas, // Optional
gasPrice, // Optional
gasToken, // Optional
refundReceiver, // Optional
nonce // Optional
}

const safeTransaction = await safeSdk.createTransaction({ safeTransactionData })
```

- **Create a MultiSend transaction**
This method takes an array of `MetaTransactionData` objects that represent the individual transactions we want to include in our MultiSend transaction. If we want to specify some of the optional properties in our MultiSend transaction, we can pass a second argument to the method `createTransaction` with the `SafeTransactionOptionalProps` object.

This method can take an array of `MetaTransactionData` objects that represent the multiple transactions we want to include in our MultiSend transaction. If we want to specify some of the optional properties in our MultiSend transaction, we can pass a second argument to the method `createTransaction` with the `SafeTransactionOptionalProps` object.
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 safeTransactionData: MetaTransactionData[] = [
const transactions: MetaTransactionData[] = [
{
to,
data,
Expand All @@ -184,7 +169,7 @@ The Protocol Kit supports the execution of single Safe transactions but also Mul
nonce // Optional
}

const safeTransaction = await safeSdk.createTransaction({ safeTransactionData, 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
10 changes: 4 additions & 6 deletions packages/account-abstraction-kit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@safe-global/account-abstraction-kit-poc",
"version": "1.3.0",
"version": "2.0.0-alpha.5",
"description": "Safe Account Abstraction Kit PoC",
"main": "dist/src/index.js",
"typings": "dist/src/index.d.ts",
Expand Down Expand Up @@ -34,10 +34,8 @@
"access": "public"
},
"dependencies": {
"@safe-global/protocol-kit": "^1.3.0",
"@safe-global/relay-kit": "^1.3.0",
"@safe-global/safe-core-sdk-types": "^2.3.0",
"ethereumjs-util": "^7.1.5",
"ethers": "^5.7.2"
"@safe-global/protocol-kit": "^2.0.0-alpha.5",
"@safe-global/relay-kit": "^2.0.0-alpha.5",
"@safe-global/safe-core-sdk-types": "^3.0.0-alpha.1"
}
}
134 changes: 59 additions & 75 deletions packages/account-abstraction-kit/src/AccountAbstraction.test.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,76 @@
import { Signer } from '@ethersproject/abstract-signer'
import Safe, { EthersAdapter, predictSafeAddress } from '@safe-global/protocol-kit'
import { GelatoRelayPack, RelayPack } from '@safe-global/relay-kit'
import { SafeTransaction } from '@safe-global/safe-core-sdk-types'
import { ethers } from 'ethers'
import Safe, { predictSafeAddress } from '@safe-global/protocol-kit'
import { GelatoRelayPack, RelayKitBasePack } from '@safe-global/relay-kit'
import { EthAdapter, SafeTransaction } from '@safe-global/safe-core-sdk-types'
import AccountAbstraction from './AccountAbstraction'

jest.mock('@safe-global/protocol-kit')
jest.mock('@safe-global/relay-kit')

const EthersAdapterMock = EthersAdapter as jest.MockedClass<typeof EthersAdapter>
const GelatoRelayPackMock = GelatoRelayPack as jest.MockedClass<typeof GelatoRelayPack>
const predictSafeAddressMock = predictSafeAddress as jest.MockedFunction<typeof predictSafeAddress>
const SafeMock = Safe as jest.MockedClass<typeof Safe>

describe('AccountAbstraction', () => {
const signer = {
provider: {},
getAddress: jest.fn()
const ethersAdapter = {
getSignerAddress: jest.fn(),
isContractDeployed: jest.fn()
}
const signerAddress = '0xSignerAddress'
const predictSafeAddress = '0xPredictSafeAddressMock'

beforeEach(() => {
jest.clearAllMocks()
signer.getAddress.mockResolvedValueOnce(signerAddress)
predictSafeAddressMock.mockResolvedValueOnce(predictSafeAddress)
})

describe('constructor', () => {
it('should create a new EthersAdapter instance', () => {
new AccountAbstraction(signer as unknown as Signer)
expect(EthersAdapterMock).toHaveBeenCalledTimes(1)
expect(EthersAdapterMock).toHaveBeenCalledWith({ ethers, signerOrProvider: signer })
})

it('should throw an error if signer is not connected to a provider', () => {
expect(
() => new AccountAbstraction({ ...signer, provider: undefined } as unknown as Signer)
).toThrow('Signer must be connected to a provider')
expect(EthersAdapterMock).not.toHaveBeenCalled()
})
ethersAdapter.getSignerAddress.mockResolvedValue(signerAddress)
predictSafeAddressMock.mockResolvedValue(predictSafeAddress)
})

describe('init', () => {
const accountAbstraction = new AccountAbstraction(signer as unknown as Signer)
const relayPack = new GelatoRelayPack()
const accountAbstraction = new AccountAbstraction(ethersAdapter as unknown as EthAdapter)

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

await accountAbstraction.init({ relayPack })
await accountAbstraction.init()

expect(signer.getAddress).toHaveBeenCalledTimes(1)
expect(ethersAdapter.getSignerAddress).toHaveBeenCalledTimes(1)
expect(predictSafeAddressMock).toHaveBeenCalledTimes(1)
expect(predictSafeAddressMock).toHaveBeenCalledWith({
ethAdapter: expect.any(EthersAdapterMock),
ethAdapter: ethersAdapter,
safeAccountConfig: { owners: ['0xSignerAddress'], threshold: 1 }
})
expect(SafeMock.create).toHaveBeenCalledTimes(1)
expect(SafeMock.create).toHaveBeenCalledWith({
ethAdapter: expect.any(EthersAdapterMock),
ethAdapter: ethersAdapter,
safeAddress: predictSafeAddress
})
})

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

await accountAbstraction.init({ relayPack })
await accountAbstraction.init()

expect(signer.getAddress).toHaveBeenCalledTimes(1)
expect(ethersAdapter.getSignerAddress).toHaveBeenCalledTimes(1)
expect(predictSafeAddressMock).toHaveBeenCalledTimes(1)
expect(predictSafeAddressMock).toHaveBeenCalledWith({
ethAdapter: expect.any(EthersAdapterMock),
ethAdapter: ethersAdapter,
safeAccountConfig: { owners: ['0xSignerAddress'], threshold: 1 }
})
expect(SafeMock.create).toHaveBeenCalledTimes(1)
expect(SafeMock.create).toHaveBeenCalledWith({
ethAdapter: expect.any(EthersAdapterMock),
ethAdapter: ethersAdapter,
predictedSafe: { safeAccountConfig: { owners: ['0xSignerAddress'], threshold: 1 } }
})
})

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

expect(accountAbstraction.init()).rejects.toThrow(
`There's no signer in the provided EthAdapter`
)
expect(SafeMock.create).not.toHaveBeenCalled()
})
})

describe('initialized', () => {
Expand All @@ -91,41 +81,33 @@ describe('AccountAbstraction', () => {
signTransaction: jest.fn()
}

const initAccountAbstraction = async (initOptions = { relayPack: new GelatoRelayPack() }) => {
const accountAbstraction = new AccountAbstraction(signer as unknown as Signer)
await accountAbstraction.init(initOptions)
const initAccountAbstraction = async () => {
const accountAbstraction = new AccountAbstraction(ethersAdapter as unknown as EthAdapter)
await accountAbstraction.init()
return accountAbstraction
}

let accountAbstraction: AccountAbstraction

beforeEach(async () => {
accountAbstraction = await initAccountAbstraction()
jest.clearAllMocks()
SafeMock.create = () => Promise.resolve(safeInstanceMock as unknown as Safe)
})

describe('getSignerAddress', () => {
it("should return the signer's address", async () => {
const result = await accountAbstraction.getSignerAddress()
expect(result).toBe(signerAddress)
expect(signer.getAddress).toHaveBeenCalledTimes(1)
})
accountAbstraction = await initAccountAbstraction()
})

describe('getNonce', () => {
const nonceMock = 123
safeInstanceMock.getNonce.mockResolvedValueOnce(nonceMock)

it('should return the nonce received from Safe SDK', async () => {
const result = await accountAbstraction.getNonce()
it('should return the nonce from the protocol-kit', async () => {
const result = await accountAbstraction.protocolKit.getNonce()
expect(result).toBe(nonceMock)
expect(safeInstanceMock.getNonce).toHaveBeenCalledTimes(1)
})

it('should throw if Safe SDK is not initialized', async () => {
const accountAbstraction = new AccountAbstraction(signer as unknown as Signer)
expect(accountAbstraction.getNonce()).rejects.toThrow('SDK not initialized')
it('should not be called if the protocol-kit is not initialized', async () => {
const accountAbstraction = new AccountAbstraction(ethersAdapter as unknown as EthAdapter)
expect(accountAbstraction.protocolKit).toBe(undefined)
expect(safeInstanceMock.getNonce).not.toHaveBeenCalled()
})
})
Expand All @@ -134,30 +116,30 @@ describe('AccountAbstraction', () => {
const safeAddressMock = '0xSafeAddress'
safeInstanceMock.getAddress.mockResolvedValueOnce(safeAddressMock)

it('should return the address received from Safe SDK', async () => {
const result = await accountAbstraction.getSafeAddress()
it('should return the Safe address from the protocol-kit', async () => {
const result = await accountAbstraction.protocolKit.getAddress()
expect(result).toBe(safeAddressMock)
expect(safeInstanceMock.getAddress).toHaveBeenCalledTimes(1)
})

it('should throw if Safe SDK is not initialized', async () => {
const accountAbstraction = new AccountAbstraction(signer as unknown as Signer)
expect(accountAbstraction.getSafeAddress()).rejects.toThrow('SDK not initialized')
it('should not be called if the protocol-kit is not initialized', async () => {
const accountAbstraction = new AccountAbstraction(ethersAdapter as unknown as EthAdapter)
expect(accountAbstraction.protocolKit).toBe(undefined)
expect(safeInstanceMock.getAddress).not.toHaveBeenCalled()
})
})

describe('isSafeDeployed', () => {
it.each([true, false])('should return the value received from Safe SDK', async (expected) => {
safeInstanceMock.isSafeDeployed.mockResolvedValueOnce(expected)
const result = await accountAbstraction.isSafeDeployed()
const result = await accountAbstraction.protocolKit.isSafeDeployed()
expect(result).toBe(expected)
expect(safeInstanceMock.isSafeDeployed).toHaveBeenCalledTimes(1)
})

it('should throw if Safe SDK is not initialized', async () => {
const accountAbstraction = new AccountAbstraction(signer as unknown as Signer)
expect(accountAbstraction.isSafeDeployed()).rejects.toThrow('SDK not initialized')
it('should not be called if the protocol-kit is not initialized', async () => {
const accountAbstraction = new AccountAbstraction(ethersAdapter as unknown as EthAdapter)
expect(accountAbstraction.protocolKit).toBe(undefined)
expect(safeInstanceMock.isSafeDeployed).not.toHaveBeenCalled()
})
})
Expand All @@ -175,14 +157,16 @@ describe('AccountAbstraction', () => {
GelatoRelayPackMock.prototype.executeRelayTransaction.mockResolvedValueOnce(
relayResponseMock
)
accountAbstraction.setRelayKit(
new GelatoRelayPack({ protocolKit: accountAbstraction.protocolKit })
)

const result = await accountAbstraction.relayTransaction(transactionsMock, optionsMock)

expect(result).toBe(relayResponseMock.taskId)
expect(result).toBe(relayResponseMock)

expect(GelatoRelayPackMock.prototype.createRelayedTransaction).toHaveBeenCalledTimes(1)
expect(GelatoRelayPackMock.prototype.createRelayedTransaction).toHaveBeenCalledWith({
safe: safeInstanceMock,
transactions: transactionsMock,
options: optionsMock
})
Expand All @@ -193,30 +177,30 @@ describe('AccountAbstraction', () => {
expect(GelatoRelayPackMock.prototype.executeRelayTransaction).toHaveBeenCalledTimes(1)
expect(GelatoRelayPackMock.prototype.executeRelayTransaction).toHaveBeenCalledWith(
signedSafeTxMock,
safeInstanceMock,
optionsMock
)
})

it('should throw if Safe SDK is not initialized', async () => {
const accountAbstraction = new AccountAbstraction(signer as unknown as Signer)
accountAbstraction.setRelayPack(new GelatoRelayPack())
it('should throw if the protocol-kit is not initialized', async () => {
const accountAbstraction = new AccountAbstraction(ethersAdapter as unknown as EthAdapter)
accountAbstraction.setRelayKit(
new GelatoRelayPack({ protocolKit: accountAbstraction.protocolKit })
)

expect(accountAbstraction.relayTransaction(transactionsMock, optionsMock)).rejects.toThrow(
'SDK not initialized'
'protocolKit not initialized. Call init() first'
)

expect(GelatoRelayPackMock.prototype.createRelayedTransaction).not.toHaveBeenCalled()
expect(safeInstanceMock.signTransaction).not.toHaveBeenCalled()
expect(GelatoRelayPackMock.prototype.executeRelayTransaction).not.toHaveBeenCalled()
})

it('should throw if Relay pack is not initialized', async () => {
const accountAbstraction = await initAccountAbstraction()
accountAbstraction.setRelayPack(undefined as unknown as RelayPack)
it('should throw if relay-kit is not initialized', async () => {
accountAbstraction.setRelayKit(undefined as unknown as RelayKitBasePack)

expect(accountAbstraction.relayTransaction(transactionsMock, optionsMock)).rejects.toThrow(
'SDK not initialized'
'relayKit not initialized. Call setRelayKit(pack) first'
)

expect(GelatoRelayPackMock.prototype.createRelayedTransaction).not.toHaveBeenCalled()
Expand Down
Loading

0 comments on commit 14f137c

Please sign in to comment.