Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brown-schools-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": patch
---

**Account Abstraction:** Made `toCoinbaseSmartWallet` prefer `signTypedData` on owners.
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,39 @@ describe('return value: sign', () => {

expect(result).toBeTruthy()
})

test('behavior: owner uses `sign` instead of `signTypedData`', async () => {
const owner = privateKeyToAccount(accounts[0].privateKey)
// @ts-expect-error
owner.signTypedData = undefined

const account = await toCoinbaseSmartAccount({
client,
owners: [owner],
nonce: 70n,
})

await writeContract(client, {
...account.factory,
functionName: 'createAccount',
args: [[pad(owner.address)], 70n],
})
await mine(client, {
blocks: 1,
})

const signature = await account.sign({
hash: '0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68',
})

const result = await verifyHash(client, {
address: await account.getAddress(),
hash: '0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68',
signature,
})

expect(result).toBeTruthy()
})
})

describe('return value: signMessage', () => {
Expand Down Expand Up @@ -466,6 +499,39 @@ describe('return value: signMessage', () => {

expect(result).toBeTruthy()
})

test('behavior: owner uses `sign` instead of `signTypedData`', async () => {
const owner = privateKeyToAccount(accounts[0].privateKey)
// @ts-expect-error
owner.signMessage = undefined

const account = await toCoinbaseSmartAccount({
client,
owners: [owner],
nonce: 70n,
})

await writeContract(client, {
...account.factory,
functionName: 'createAccount',
args: [[pad(owner.address)], 70n],
})
await mine(client, {
blocks: 1,
})

const signature = await account.signMessage({
message: 'hello world',
})

const result = await verifyMessage(client, {
address: await account.getAddress(),
message: 'hello world',
signature,
})

expect(result).toBeTruthy()
})
})

describe('return value: signTypedData', () => {
Expand Down Expand Up @@ -519,6 +585,40 @@ describe('return value: signTypedData', () => {
})
expect(result).toBeTruthy()
})

test('behavior: owner uses `sign` instead of `signTypedData`', async () => {
const owner = privateKeyToAccount(accounts[0].privateKey)
// @ts-expect-error
owner.signTypedData = undefined

const account = await toCoinbaseSmartAccount({
client,
owners: [owner],
nonce: 515151n,
})

await writeContract(client, {
...account.factory,
functionName: 'createAccount',
args: [[pad(owner.address)], 515151n],
})
await mine(client, {
blocks: 1,
})

const signature = await account.signTypedData({
...typedData.basic,
primaryType: 'Mail',
})

const result = await verifyTypedData(client, {
address: await account.getAddress(),
signature,
...typedData.basic,
primaryType: 'Mail',
})
expect(result).toBeTruthy()
})
})

describe('return value: signUserOperation', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,14 @@ export async function toCoinbaseSmartAccount(
async sign(parameters) {
const address = await this.getAddress()

const hash = toReplaySafeHash({
const typedData = toReplaySafeTypedData({
address,
chainId: client.chain!.id,
hash: parameters.hash,
})

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

return wrapSignature({
ownerIndex,
Expand All @@ -196,14 +196,14 @@ export async function toCoinbaseSmartAccount(
const { message } = parameters
const address = await this.getAddress()

const hash = toReplaySafeHash({
const typedData = toReplaySafeTypedData({
address,
chainId: client.chain!.id,
hash: hashMessage(message),
})

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

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

const hash = toReplaySafeHash({
const typedData = toReplaySafeTypedData({
address,
chainId: client.chain!.id,
hash: hashTypedData({
Expand All @@ -228,7 +228,7 @@ export async function toCoinbaseSmartAccount(
})

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

return wrapSignature({
ownerIndex,
Expand Down Expand Up @@ -278,6 +278,21 @@ export async function toCoinbaseSmartAccount(
// Utilities
/////////////////////////////////////////////////////////////////////////////////////////////

/** @internal */
export async function signTypedData({
typedData,
owner,
}: {
typedData: TypedDataDefinition
owner: OneOf<LocalAccount | WebAuthnAccount>
}) {
if (owner.type === 'local' && owner.signTypedData)
return owner.signTypedData(typedData)

const hash = hashTypedData(typedData)
return sign({ hash, owner })
}

/** @internal */
export async function sign({
hash,
Expand All @@ -297,12 +312,12 @@ export async function sign({
}

/** @internal */
export function toReplaySafeHash({
export function toReplaySafeTypedData({
address,
chainId,
hash,
}: { address: Address; chainId: number; hash: Hash }) {
return hashTypedData({
return {
domain: {
chainId,
name: 'Coinbase Smart Wallet',
Expand All @@ -321,7 +336,7 @@ export function toReplaySafeHash({
message: {
hash,
},
})
} as const
}

/** @internal */
Expand Down
Loading