Skip to content

Commit c0a98b3

Browse files
authored
Merge pull request #50 from tokenbound/bj/prepareCreateAccount-with-multicall-createAccount-custom-impl
Multicall handling in prepareCreateAccount
2 parents 9c4ecea + ccbb53d commit c0a98b3

File tree

6 files changed

+115
-101
lines changed

6 files changed

+115
-101
lines changed

.changeset/dull-taxis-speak.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tokenbound/sdk': patch
3+
---
4+
5+
handle multicall for createAccount in prepareCreateAccount

.github/workflows/on-create-release.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ jobs:
1414
- name: Clone repository
1515
uses: actions/checkout@v4
1616

17-
# - name: Set up PNPM
18-
# uses: pnpm/action-setup@v2
19-
# with:
20-
# version: 8
17+
- name: Set up PNPM
18+
uses: pnpm/action-setup@v2
19+
with:
20+
version: 8
2121

2222
- name: Set up Node.js ${{ matrix.node-version }}
2323
uses: actions/setup-node@v3

packages/sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tokenbound/sdk",
3-
"version": "0.4.2",
3+
"version": "0.4.3",
44
"type": "module",
55
"files": [
66
"dist"

packages/sdk/src/TokenboundClient.ts

Lines changed: 98 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ class TokenboundClient {
7676
private supportsV3: boolean = true // Default to V3 implementation
7777
private signer?: AbstractEthersSigner
7878
private walletClient?: WalletClient
79-
private implementationAddress?: `0x${string}`
80-
private registryAddress?: `0x${string}`
79+
private implementationAddress: `0x${string}`
80+
private registryAddress: `0x${string}`
8181

8282
constructor(options: TokenboundClientOptions) {
8383
const {
@@ -123,26 +123,19 @@ class TokenboundClient {
123123
transport: http(publicClientRPCUrl ?? undefined),
124124
})
125125

126-
if (registryAddress) {
127-
this.registryAddress = registryAddress
128-
}
126+
this.registryAddress = registryAddress ?? ERC_6551_DEFAULT.REGISTRY.ADDRESS
127+
this.implementationAddress =
128+
implementationAddress ?? ERC_6551_DEFAULT.ACCOUNT_PROXY!.ADDRESS
129129

130-
if (implementationAddress) {
131-
this.implementationAddress = implementationAddress
132-
133-
// If legacy V2 implementation is in use, use V2 registry (unless custom registry is provided)
134-
const isV2 =
135-
(version && version === TBVersion.V2) ||
136-
(implementationAddress &&
137-
isAddressEqual(
138-
implementationAddress,
139-
ERC_6551_LEGACY_V2.IMPLEMENTATION.ADDRESS
140-
))
141-
142-
if (isV2) {
143-
this.supportsV3 = false
144-
if (!registryAddress) this.registryAddress = ERC_6551_LEGACY_V2.REGISTRY.ADDRESS
145-
}
130+
// If legacy V2 implementation is in use, use V2 registry (unless custom registry is provided)
131+
const isV2 =
132+
(version && version === TBVersion.V2) ||
133+
(implementationAddress &&
134+
isAddressEqual(implementationAddress, ERC_6551_LEGACY_V2.IMPLEMENTATION.ADDRESS))
135+
136+
if (isV2) {
137+
this.supportsV3 = false
138+
if (!registryAddress) this.registryAddress = ERC_6551_LEGACY_V2.REGISTRY.ADDRESS
146139
}
147140

148141
this.isInitialized = true
@@ -153,6 +146,14 @@ class TokenboundClient {
153146
}
154147
}
155148

149+
/**
150+
* Returns the SDK's package version.
151+
* @returns The version of the SDK.
152+
*/
153+
public getSDKVersion(): string {
154+
return TB_SDK_VERSION
155+
}
156+
156157
/**
157158
* Returns the tokenbound account address for a given token contract and token ID.
158159
* @param {`0x${string}`} params.tokenContract The address of the token contract.
@@ -161,13 +162,17 @@ class TokenboundClient {
161162
*/
162163
public getAccount(params: GetAccountParams): `0x${string}` {
163164
const { tokenContract, tokenId, salt = 0 } = params
164-
const implementation =
165-
this.implementationAddress ?? ERC_6551_DEFAULT.ACCOUNT_PROXY!.ADDRESS
166-
const registry = this.registryAddress ?? ERC_6551_DEFAULT.REGISTRY.ADDRESS
167165

168166
try {
169167
const getAcct = this.supportsV3 ? getTokenboundV3Account : computeAccount
170-
return getAcct(tokenContract, tokenId, this.chainId, implementation, registry, salt)
168+
return getAcct(
169+
tokenContract,
170+
tokenId,
171+
this.chainId,
172+
this.implementationAddress,
173+
this.registryAddress,
174+
salt
175+
)
171176
} catch (error) {
172177
throw error
173178
}
@@ -179,50 +184,90 @@ class TokenboundClient {
179184
* @param {string} params.tokenId The token ID.
180185
* @returns The prepared transaction to create a tokenbound account. Can be sent via `sendTransaction` on an Ethers signer or viem WalletClient.
181186
*/
182-
public async prepareCreateAccount(params: PrepareCreateAccountParams): Promise<{
183-
to: `0x${string}`
184-
value: bigint
185-
data: `0x${string}`
186-
}> {
187+
public async prepareCreateAccount(params: PrepareCreateAccountParams): Promise<
188+
| MultiCallTx
189+
| {
190+
to: `0x${string}`
191+
value: bigint
192+
data: `0x${string}`
193+
}
194+
> {
187195
const { tokenContract, tokenId, salt = 0 } = params
188-
const implementation =
189-
this.implementationAddress ?? ERC_6551_DEFAULT.ACCOUNT_PROXY!.ADDRESS
190-
const registry = this.registryAddress ?? ERC_6551_DEFAULT.REGISTRY.ADDRESS
191196

192-
const prepareCreation = this.supportsV3
197+
const getAcct = this.supportsV3 ? getTokenboundV3Account : computeAccount
198+
199+
const computedAcct = getAcct(
200+
tokenContract,
201+
tokenId,
202+
this.chainId,
203+
this.implementationAddress,
204+
this.registryAddress,
205+
salt
206+
)
207+
208+
const isCustomImplementation = ![
209+
ERC_6551_DEFAULT.ACCOUNT_PROXY!.ADDRESS,
210+
ERC_6551_DEFAULT.IMPLEMENTATION.ADDRESS,
211+
].includes(getAddress(this.implementationAddress))
212+
213+
const prepareBasicCreateAccount = this.supportsV3
193214
? prepareCreateTokenboundV3Account
194215
: prepareCreateAccount
195216

196-
return prepareCreation(
217+
const preparedBasicCreateAccount = await prepareBasicCreateAccount(
197218
tokenContract,
198219
tokenId,
199220
this.chainId,
200-
implementation,
201-
registry,
221+
this.implementationAddress,
222+
this.registryAddress,
202223
salt
203224
)
225+
226+
if (isCustomImplementation) {
227+
// Don't initialize for custom implementations. Allow third-party handling of initialization.
228+
return preparedBasicCreateAccount
229+
} else {
230+
// For standard implementations, use the multicall3 aggregate function to create and initialize the account in one transaction
231+
return {
232+
to: MULTICALL_ADDRESS,
233+
value: BigInt(0),
234+
data: encodeFunctionData({
235+
abi: multicall3Abi,
236+
functionName: 'aggregate3',
237+
args: [
238+
[
239+
{
240+
target: this.registryAddress,
241+
allowFailure: false,
242+
callData: preparedBasicCreateAccount.data,
243+
},
244+
{
245+
target: computedAcct,
246+
allowFailure: false,
247+
callData: encodeFunctionData({
248+
abi: ERC_6551_DEFAULT.ACCOUNT_PROXY?.ABI!,
249+
functionName: 'initialize',
250+
args: [ERC_6551_DEFAULT.IMPLEMENTATION!.ADDRESS],
251+
}),
252+
},
253+
],
254+
],
255+
}),
256+
} as MultiCallTx
257+
}
204258
}
205259

206260
/**
207261
* Returns the transaction hash of the transaction that created the tokenbound account for a given token contract and token ID.
208262
* @param {`0x${string}`} params.tokenContract The address of the token contract.
209263
* @param {string} params.tokenId The token ID.
210-
* @param {`0x${string}`} [params.implementationAddress] The address of the implementation contract.
211-
* @param {`0x${string}`} [params.registryAddress] The address of the registry contract.
212264
* @returns a Promise that resolves to the account address of the created tokenbound account.
213265
*/
214266
public async createAccount(
215267
params: CreateAccountParams
216268
): Promise<{ account: `0x${string}`; txHash: `0x${string}` }> {
217269
const { tokenContract, tokenId, salt = 0 } = params
218270

219-
const implementation =
220-
this.implementationAddress ?? ERC_6551_DEFAULT.ACCOUNT_PROXY!.ADDRESS
221-
const registry = this.registryAddress ?? ERC_6551_DEFAULT.REGISTRY.ADDRESS
222-
const isCustomImplementation =
223-
this.implementationAddress &&
224-
!isAddressEqual(this.implementationAddress, ERC_6551_DEFAULT.ACCOUNT_PROXY!.ADDRESS)
225-
226271
try {
227272
let txHash: `0x${string}` | undefined
228273

@@ -232,77 +277,34 @@ class TokenboundClient {
232277
tokenContract,
233278
tokenId,
234279
this.chainId,
235-
implementation,
236-
registry,
280+
this.implementationAddress,
281+
this.registryAddress,
237282
salt
238283
)
239284

240-
const prepareCreateAccount = await this.prepareCreateAccount({
285+
const preparedCreateAccount = await this.prepareCreateAccount({
241286
tokenContract,
242287
tokenId,
243288
salt,
244289
})
245290

246-
let prepareCreateV3Account:
247-
| MultiCallTx
248-
| {
249-
to: `0x${string}`
250-
value: bigint
251-
data: `0x${string}`
252-
}
253-
254-
if (isCustomImplementation) {
255-
// Don't initalize for custom implementations. Allow third-party handling of initialization.
256-
prepareCreateV3Account = prepareCreateAccount
257-
} else {
258-
// For standard implementations, use the multicall3 aggregate function to create the account and initialize it in one transaction
259-
prepareCreateV3Account = {
260-
to: MULTICALL_ADDRESS,
261-
value: BigInt(0),
262-
data: encodeFunctionData({
263-
abi: multicall3Abi,
264-
functionName: 'aggregate3',
265-
args: [
266-
[
267-
{
268-
target: registry,
269-
allowFailure: false,
270-
callData: prepareCreateAccount.data,
271-
},
272-
{
273-
target: computedAcct,
274-
allowFailure: false,
275-
callData: encodeFunctionData({
276-
abi: ERC_6551_DEFAULT.ACCOUNT_PROXY?.ABI!,
277-
functionName: 'initialize',
278-
args: [ERC_6551_DEFAULT.IMPLEMENTATION!.ADDRESS],
279-
}),
280-
},
281-
],
282-
],
283-
}),
284-
} as MultiCallTx
285-
}
286-
287291
if (this.signer) {
288292
txHash = (await this.signer
289-
.sendTransaction(
290-
this.supportsV3 ? prepareCreateV3Account : prepareCreateAccount
291-
)
293+
.sendTransaction(preparedCreateAccount)
292294
.then((tx: AbstractEthersTransactionResponse) => tx.hash)) as `0x${string}`
293295
} else if (this.walletClient) {
294296
txHash = this.supportsV3
295297
? await this.walletClient.sendTransaction({
296-
...prepareCreateV3Account,
298+
...preparedCreateAccount,
297299
chain: chainIdToChain(this.chainId),
298300
account: this.walletClient?.account?.address!,
299301
}) // @BJ TODO: extract into viemV3?
300302
: await createAccount(
301303
tokenContract,
302304
tokenId,
303305
this.walletClient,
304-
implementation,
305-
registry,
306+
this.implementationAddress,
307+
this.registryAddress,
306308
salt
307309
)
308310
}

packages/sdk/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
BytecodeParams,
2929
TBImplementationVersion,
3030
TBVersion,
31+
MultiCallTx,
3132
} from './types'
3233

3334
import {
@@ -71,4 +72,5 @@ export type {
7172
ETHTransferParams,
7273
NFTTransferParams,
7374
TBImplementationVersion,
75+
MultiCallTx,
7476
}

packages/sdk/src/test/TestAll.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ function runTxTests({
167167
console.log(`END → \x1b[94m ${testName} \x1b[0m`)
168168
})
169169

170+
it('can get the SDK version', () => {
171+
const sdkVersion: string = tokenboundClient.getSDKVersion()
172+
expect(sdkVersion).toBeDefined()
173+
})
174+
170175
// To test the SDK methods, we need to mint some NFTs into the Anvil wallet
171176
// so that we can transfer them to the TBA and test the TBA methods.
172177
it(

0 commit comments

Comments
 (0)