Skip to content

Commit 295e4f9

Browse files
jxomjsutaria
andauthored
feat: prefer signTypedData on owners for toCoinbaseSmartWallet (#3579)
feat: prefer `signTypedData` on owners for cbsw Co-authored-by: Jainil Sutaria <[email protected]>
1 parent 26e4bca commit 295e4f9

File tree

3 files changed

+129
-9
lines changed

3 files changed

+129
-9
lines changed

.changeset/brown-schools-care.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"viem": patch
3+
---
4+
5+
**Account Abstraction:** Made `toCoinbaseSmartWallet` prefer `signTypedData` on owners.

src/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,39 @@ describe('return value: sign', () => {
415415

416416
expect(result).toBeTruthy()
417417
})
418+
419+
test('behavior: owner uses `sign` instead of `signTypedData`', async () => {
420+
const owner = privateKeyToAccount(accounts[0].privateKey)
421+
// @ts-expect-error
422+
owner.signTypedData = undefined
423+
424+
const account = await toCoinbaseSmartAccount({
425+
client,
426+
owners: [owner],
427+
nonce: 70n,
428+
})
429+
430+
await writeContract(client, {
431+
...account.factory,
432+
functionName: 'createAccount',
433+
args: [[pad(owner.address)], 70n],
434+
})
435+
await mine(client, {
436+
blocks: 1,
437+
})
438+
439+
const signature = await account.sign({
440+
hash: '0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68',
441+
})
442+
443+
const result = await verifyHash(client, {
444+
address: await account.getAddress(),
445+
hash: '0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68',
446+
signature,
447+
})
448+
449+
expect(result).toBeTruthy()
450+
})
418451
})
419452

420453
describe('return value: signMessage', () => {
@@ -466,6 +499,39 @@ describe('return value: signMessage', () => {
466499

467500
expect(result).toBeTruthy()
468501
})
502+
503+
test('behavior: owner uses `sign` instead of `signTypedData`', async () => {
504+
const owner = privateKeyToAccount(accounts[0].privateKey)
505+
// @ts-expect-error
506+
owner.signMessage = undefined
507+
508+
const account = await toCoinbaseSmartAccount({
509+
client,
510+
owners: [owner],
511+
nonce: 70n,
512+
})
513+
514+
await writeContract(client, {
515+
...account.factory,
516+
functionName: 'createAccount',
517+
args: [[pad(owner.address)], 70n],
518+
})
519+
await mine(client, {
520+
blocks: 1,
521+
})
522+
523+
const signature = await account.signMessage({
524+
message: 'hello world',
525+
})
526+
527+
const result = await verifyMessage(client, {
528+
address: await account.getAddress(),
529+
message: 'hello world',
530+
signature,
531+
})
532+
533+
expect(result).toBeTruthy()
534+
})
469535
})
470536

471537
describe('return value: signTypedData', () => {
@@ -519,6 +585,40 @@ describe('return value: signTypedData', () => {
519585
})
520586
expect(result).toBeTruthy()
521587
})
588+
589+
test('behavior: owner uses `sign` instead of `signTypedData`', async () => {
590+
const owner = privateKeyToAccount(accounts[0].privateKey)
591+
// @ts-expect-error
592+
owner.signTypedData = undefined
593+
594+
const account = await toCoinbaseSmartAccount({
595+
client,
596+
owners: [owner],
597+
nonce: 515151n,
598+
})
599+
600+
await writeContract(client, {
601+
...account.factory,
602+
functionName: 'createAccount',
603+
args: [[pad(owner.address)], 515151n],
604+
})
605+
await mine(client, {
606+
blocks: 1,
607+
})
608+
609+
const signature = await account.signTypedData({
610+
...typedData.basic,
611+
primaryType: 'Mail',
612+
})
613+
614+
const result = await verifyTypedData(client, {
615+
address: await account.getAddress(),
616+
signature,
617+
...typedData.basic,
618+
primaryType: 'Mail',
619+
})
620+
expect(result).toBeTruthy()
621+
})
522622
})
523623

524624
describe('return value: signUserOperation', () => {

src/account-abstraction/accounts/implementations/toCoinbaseSmartAccount.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -177,14 +177,14 @@ export async function toCoinbaseSmartAccount(
177177
async sign(parameters) {
178178
const address = await this.getAddress()
179179

180-
const hash = toReplaySafeHash({
180+
const typedData = toReplaySafeTypedData({
181181
address,
182182
chainId: client.chain!.id,
183183
hash: parameters.hash,
184184
})
185185

186186
if (owner.type === 'address') throw new Error('owner cannot sign')
187-
const signature = await sign({ hash, owner })
187+
const signature = await signTypedData({ owner, typedData })
188188

189189
return wrapSignature({
190190
ownerIndex,
@@ -196,14 +196,14 @@ export async function toCoinbaseSmartAccount(
196196
const { message } = parameters
197197
const address = await this.getAddress()
198198

199-
const hash = toReplaySafeHash({
199+
const typedData = toReplaySafeTypedData({
200200
address,
201201
chainId: client.chain!.id,
202202
hash: hashMessage(message),
203203
})
204204

205205
if (owner.type === 'address') throw new Error('owner cannot sign')
206-
const signature = await sign({ hash, owner })
206+
const signature = await signTypedData({ owner, typedData })
207207

208208
return wrapSignature({
209209
ownerIndex,
@@ -216,7 +216,7 @@ export async function toCoinbaseSmartAccount(
216216
parameters as TypedDataDefinition<TypedData, string>
217217
const address = await this.getAddress()
218218

219-
const hash = toReplaySafeHash({
219+
const typedData = toReplaySafeTypedData({
220220
address,
221221
chainId: client.chain!.id,
222222
hash: hashTypedData({
@@ -228,7 +228,7 @@ export async function toCoinbaseSmartAccount(
228228
})
229229

230230
if (owner.type === 'address') throw new Error('owner cannot sign')
231-
const signature = await sign({ hash, owner })
231+
const signature = await signTypedData({ owner, typedData })
232232

233233
return wrapSignature({
234234
ownerIndex,
@@ -278,6 +278,21 @@ export async function toCoinbaseSmartAccount(
278278
// Utilities
279279
/////////////////////////////////////////////////////////////////////////////////////////////
280280

281+
/** @internal */
282+
export async function signTypedData({
283+
typedData,
284+
owner,
285+
}: {
286+
typedData: TypedDataDefinition
287+
owner: OneOf<LocalAccount | WebAuthnAccount>
288+
}) {
289+
if (owner.type === 'local' && owner.signTypedData)
290+
return owner.signTypedData(typedData)
291+
292+
const hash = hashTypedData(typedData)
293+
return sign({ hash, owner })
294+
}
295+
281296
/** @internal */
282297
export async function sign({
283298
hash,
@@ -297,12 +312,12 @@ export async function sign({
297312
}
298313

299314
/** @internal */
300-
export function toReplaySafeHash({
315+
export function toReplaySafeTypedData({
301316
address,
302317
chainId,
303318
hash,
304319
}: { address: Address; chainId: number; hash: Hash }) {
305-
return hashTypedData({
320+
return {
306321
domain: {
307322
chainId,
308323
name: 'Coinbase Smart Wallet',
@@ -321,7 +336,7 @@ export function toReplaySafeHash({
321336
message: {
322337
hash,
323338
},
324-
})
339+
} as const
325340
}
326341

327342
/** @internal */

0 commit comments

Comments
 (0)