Skip to content

Commit 6a84860

Browse files
michaelhlydeodadpfletcherhill
authored
feat: implement ViemLocalEip712Signer (#787)
* Implement viemEip712Signer * Add tests * clean * Stubs * Deps * Fixes * Clean * Fix bugs * Rename * Add changeset * rename * clean up * rename * update import * Upgrade * Update test * ViemEip712Signer -> ViemLocalEip712Signer * clean up * Convert hash bytes to hex string before signing * use random wallet * Add test for mnemonic account * Simplify * Use random mnemonic phrase * More clean up * Add comment * refactor EIP-712 signer tests into util * update changeset to patch core --------- Co-authored-by: Tony D'Addeo <[email protected]> Co-authored-by: Paul Fletcher-Hill <[email protected]>
1 parent 3af9f6e commit 6a84860

File tree

10 files changed

+240
-122
lines changed

10 files changed

+240
-122
lines changed

.changeset/angry-fireants-melt.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@farcaster/core': patch
3+
---
4+
5+
feat: add ViemLocalEip712Signer

packages/core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"@noble/ed25519": "^1.7.3",
2222
"@noble/hashes": "^1.3.0",
2323
"ethers": "~6.2.1",
24-
"neverthrow": "^6.0.0"
24+
"neverthrow": "^6.0.0",
25+
"viem": "^0.3.2"
2526
},
2627
"scripts": {
2728
"build": "tsup --config tsup.config.ts",

packages/core/src/crypto/eip712.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const EIP_712_FARCASTER_DOMAIN = {
1111
name: 'Farcaster Verify Ethereum Address',
1212
version: '2.0.0',
1313
// fixed salt to minimize collisions
14-
salt: '0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558',
14+
salt: '0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558' as `0x${string}`, // Type cast for viem compatibility
1515
};
1616

1717
export const EIP_712_FARCASTER_VERIFICATION_CLAIM = [
Lines changed: 6 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,11 @@
1-
import { FarcasterNetwork } from '../protobufs';
2-
import { blake3 } from '@noble/hashes/blake3';
3-
import { Signer as EthersSigner, Wallet, randomBytes } from 'ethers';
4-
import { bytesToHexString } from '../bytes';
5-
import { eip712 } from '../crypto';
6-
import { Factories } from '../factories';
7-
import { VerificationEthAddressClaim, makeVerificationEthAddressClaim } from '../verifications';
1+
import { Wallet } from 'ethers';
82
import { EthersEip712Signer } from './ethersEip712Signer';
3+
import { testEip712Signer } from './testUtils';
94

105
describe('EthersEip712Signer', () => {
11-
let signer: EthersEip712Signer;
12-
let ethersSigner: EthersSigner;
13-
let signerKey: Uint8Array;
14-
15-
beforeAll(async () => {
16-
ethersSigner = Wallet.createRandom();
17-
signer = new EthersEip712Signer(ethersSigner);
18-
signerKey = (await signer.getSignerKey())._unsafeUnwrap();
19-
});
20-
21-
describe('instanceMethods', () => {
22-
describe('signMessageHash', () => {
23-
test('generates valid signature', async () => {
24-
const bytes = randomBytes(32);
25-
const hash = blake3(bytes, { dkLen: 20 });
26-
const signature = (await signer.signMessageHash(hash))._unsafeUnwrap();
27-
const recoveredAddress = (await eip712.verifyMessageHashSignature(hash, signature))._unsafeUnwrap();
28-
expect(recoveredAddress).toEqual(signerKey);
29-
});
30-
});
31-
32-
describe('signVerificationEthAddressClaim', () => {
33-
let claim: VerificationEthAddressClaim;
34-
let signature: Uint8Array;
35-
36-
beforeAll(async () => {
37-
claim = makeVerificationEthAddressClaim(
38-
Factories.Fid.build(),
39-
signerKey,
40-
FarcasterNetwork.TESTNET,
41-
Factories.BlockHash.build()
42-
)._unsafeUnwrap();
43-
signature = (await signer.signVerificationEthAddressClaim(claim))._unsafeUnwrap();
44-
});
45-
46-
test('succeeds', async () => {
47-
expect(signature).toBeTruthy();
48-
const recoveredAddress = eip712.verifyVerificationEthAddressClaimSignature(claim, signature)._unsafeUnwrap();
49-
expect(recoveredAddress).toEqual(signerKey);
50-
});
51-
52-
test('succeeds when encoding twice', async () => {
53-
const claim2: VerificationEthAddressClaim = { ...claim };
54-
const signature2 = (await signer.signVerificationEthAddressClaim(claim2))._unsafeUnwrap();
55-
expect(signature2).toEqual(signature);
56-
expect(bytesToHexString(signature2)).toEqual(bytesToHexString(signature));
57-
});
58-
});
6+
describe('with ethers Wallet', () => {
7+
const wallet = Wallet.createRandom();
8+
const signer = new EthersEip712Signer(wallet);
9+
testEip712Signer(signer);
5910
});
6011
});
Lines changed: 5 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,11 @@
1-
import { FarcasterNetwork } from '../protobufs';
2-
import { blake3 } from '@noble/hashes/blake3';
3-
import { randomBytes } from 'ethers';
41
import { Wallet } from 'ethers5';
5-
import { bytesToHexString } from '../bytes';
6-
import { eip712 } from '../crypto';
7-
import { Factories } from '../factories';
8-
import { VerificationEthAddressClaim, makeVerificationEthAddressClaim } from '../verifications';
92
import { EthersV5Eip712Signer } from './ethersV5Eip712Signer';
3+
import { testEip712Signer } from './testUtils';
104

115
describe('EthersV5Eip712Signer', () => {
12-
let signer: EthersV5Eip712Signer;
13-
let ethersSigner: Wallet;
14-
let signerKey: Uint8Array;
15-
16-
beforeAll(async () => {
17-
ethersSigner = Wallet.createRandom();
18-
signer = new EthersV5Eip712Signer(ethersSigner);
19-
signerKey = (await signer.getSignerKey())._unsafeUnwrap();
20-
});
21-
22-
describe('instanceMethods', () => {
23-
describe('signMessageHash', () => {
24-
test('generates valid signature', async () => {
25-
const bytes = randomBytes(32);
26-
const hash = blake3(bytes, { dkLen: 20 });
27-
const signature = (await signer.signMessageHash(hash))._unsafeUnwrap();
28-
const recoveredAddress = (await eip712.verifyMessageHashSignature(hash, signature))._unsafeUnwrap();
29-
expect(recoveredAddress).toEqual(signerKey);
30-
});
31-
});
32-
33-
describe('signVerificationEthAddressClaim', () => {
34-
let claim: VerificationEthAddressClaim;
35-
let signature: Uint8Array;
36-
37-
beforeAll(async () => {
38-
claim = makeVerificationEthAddressClaim(
39-
Factories.Fid.build(),
40-
signerKey,
41-
FarcasterNetwork.TESTNET,
42-
Factories.BlockHash.build()
43-
)._unsafeUnwrap();
44-
signature = (await signer.signVerificationEthAddressClaim(claim))._unsafeUnwrap();
45-
});
46-
47-
test('succeeds', async () => {
48-
expect(signature).toBeTruthy();
49-
const recoveredAddress = eip712.verifyVerificationEthAddressClaimSignature(claim, signature)._unsafeUnwrap();
50-
expect(recoveredAddress).toEqual(signerKey);
51-
});
52-
53-
test('succeeds when encoding twice', async () => {
54-
const claim2: VerificationEthAddressClaim = { ...claim };
55-
const signature2 = (await signer.signVerificationEthAddressClaim(claim2))._unsafeUnwrap();
56-
expect(signature2).toEqual(signature);
57-
expect(bytesToHexString(signature2)).toEqual(bytesToHexString(signature));
58-
});
59-
});
6+
describe('with ethers Wallet', () => {
7+
const wallet = Wallet.createRandom();
8+
const signer = new EthersV5Eip712Signer(wallet);
9+
testEip712Signer(signer);
6010
});
6111
});
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { blake3 } from '@noble/hashes/blake3';
22
import { randomBytes } from 'ethers';
3+
import { ok } from 'neverthrow';
34
import { ed25519 } from '../crypto';
45
import { Factories } from '../factories';
56
import { NobleEd25519Signer } from './nobleEd25519Signer';
@@ -13,15 +14,14 @@ describe('NobleEd25519Signer', () => {
1314
signerKey = (await signer.getSignerKey())._unsafeUnwrap();
1415
});
1516

16-
describe('instanceMethods', () => {
17-
describe('signMessageHash', () => {
18-
test('generates valid signature', async () => {
19-
const bytes = randomBytes(32);
20-
const hash = blake3(bytes, { dkLen: 20 });
21-
const signature = (await signer.signMessageHash(hash))._unsafeUnwrap();
22-
const isValid = await ed25519.verifyMessageHashSignature(signature, hash, signerKey);
23-
expect(isValid._unsafeUnwrap()).toBe(true);
24-
});
17+
describe('signMessageHash', () => {
18+
test('generates valid signature', async () => {
19+
const bytes = randomBytes(32);
20+
const hash = blake3(bytes, { dkLen: 20 });
21+
const signature = await signer.signMessageHash(hash);
22+
expect(signature.isOk()).toBeTruthy();
23+
const isValid = await ed25519.verifyMessageHashSignature(signature._unsafeUnwrap(), hash, signerKey);
24+
expect(isValid).toEqual(ok(true));
2525
});
2626
});
2727
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { blake3 } from '@noble/hashes/blake3';
2+
import { ok } from 'neverthrow';
3+
import { bytesToHexString } from '../bytes';
4+
import { eip712 } from '../crypto';
5+
import { Factories } from '../factories';
6+
import { FarcasterNetwork } from '../protobufs';
7+
import { makeVerificationEthAddressClaim, VerificationEthAddressClaim } from '../verifications';
8+
import { Eip712Signer } from './eip712Signer';
9+
10+
export const testEip712Signer = async (signer: Eip712Signer) => {
11+
let signerKey: Uint8Array;
12+
13+
beforeAll(async () => {
14+
signerKey = (await signer.getSignerKey())._unsafeUnwrap();
15+
});
16+
17+
describe('signMessageHash', () => {
18+
test('generates valid signature', async () => {
19+
const bytes = Factories.Bytes.build({}, { transient: { length: 32 } });
20+
const hash = blake3(bytes, { dkLen: 20 });
21+
const signature = await signer.signMessageHash(hash);
22+
expect(signature.isOk()).toBeTruthy();
23+
const recoveredAddress = await eip712.verifyMessageHashSignature(hash, signature._unsafeUnwrap());
24+
expect(recoveredAddress).toEqual(ok(signerKey));
25+
});
26+
});
27+
28+
describe('signVerificationEthAddressClaim', () => {
29+
let claim: VerificationEthAddressClaim;
30+
let signature: Uint8Array;
31+
32+
beforeAll(async () => {
33+
claim = makeVerificationEthAddressClaim(
34+
Factories.Fid.build(),
35+
signerKey,
36+
FarcasterNetwork.TESTNET,
37+
Factories.BlockHash.build()
38+
)._unsafeUnwrap();
39+
const signatureResult = await signer.signVerificationEthAddressClaim(claim);
40+
expect(signatureResult.isOk()).toBeTruthy();
41+
signature = signatureResult._unsafeUnwrap();
42+
});
43+
44+
test('succeeds', async () => {
45+
const recoveredAddress = eip712.verifyVerificationEthAddressClaimSignature(claim, signature);
46+
expect(recoveredAddress).toEqual(ok(signerKey));
47+
});
48+
49+
test('succeeds when encoding twice', async () => {
50+
const claim2: VerificationEthAddressClaim = { ...claim };
51+
const signature2 = await signer.signVerificationEthAddressClaim(claim2);
52+
expect(signature2).toEqual(ok(signature));
53+
expect(bytesToHexString(signature2._unsafeUnwrap())).toEqual(bytesToHexString(signature));
54+
});
55+
});
56+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { ViemLocalEip712Signer } from './viemLocalEip712Signer';
2+
import { Wallet } from 'ethers5';
3+
import { ethersWalletToAccount } from 'viem/ethers';
4+
import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
5+
import { Hex } from 'viem/dist/types/types';
6+
import { testEip712Signer } from './testUtils';
7+
8+
describe('ViemLocalEip712Signer', () => {
9+
describe('with ethers account', () => {
10+
const ethersAccount = ethersWalletToAccount(Wallet.createRandom());
11+
const signer = new ViemLocalEip712Signer(ethersAccount);
12+
testEip712Signer(signer);
13+
});
14+
15+
describe('with private key account', () => {
16+
const privateKeyAccount = privateKeyToAccount(Wallet.createRandom().privateKey as Hex);
17+
const signer = new ViemLocalEip712Signer(privateKeyAccount);
18+
testEip712Signer(signer);
19+
});
20+
21+
describe('with mnemonic account', () => {
22+
const mnemonicAccount = mnemonicToAccount(Wallet.createRandom().mnemonic.phrase);
23+
const signer = new ViemLocalEip712Signer(mnemonicAccount);
24+
testEip712Signer(signer);
25+
});
26+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { ResultAsync } from 'neverthrow';
2+
import { LocalAccount } from 'viem/accounts';
3+
import { bytesToHex } from 'viem/utils';
4+
import { hexStringToBytes } from '../bytes';
5+
import {
6+
EIP_712_FARCASTER_MESSAGE_DATA,
7+
EIP_712_FARCASTER_VERIFICATION_CLAIM,
8+
EIP_712_FARCASTER_DOMAIN,
9+
} from '../crypto/eip712';
10+
import { HubAsyncResult, HubError } from '../errors';
11+
import { VerificationEthAddressClaim } from '../verifications';
12+
import { Eip712Signer } from './eip712Signer';
13+
14+
export class ViemLocalEip712Signer extends Eip712Signer {
15+
private readonly _viemLocalAccount: LocalAccount<string>;
16+
17+
constructor(viemLocalAccount: LocalAccount<string>) {
18+
super();
19+
this._viemLocalAccount = viemLocalAccount;
20+
}
21+
22+
public async getSignerKey(): HubAsyncResult<Uint8Array> {
23+
return ResultAsync.fromPromise(
24+
Promise.resolve(this._viemLocalAccount.address),
25+
(e) => new HubError('unknown', e as Error)
26+
).andThen(hexStringToBytes);
27+
}
28+
29+
public async signMessageHash(hash: Uint8Array): HubAsyncResult<Uint8Array> {
30+
const hexSignature = await ResultAsync.fromPromise(
31+
this._viemLocalAccount.signTypedData({
32+
domain: EIP_712_FARCASTER_DOMAIN,
33+
types: { MessageData: EIP_712_FARCASTER_MESSAGE_DATA },
34+
primaryType: 'MessageData',
35+
message: {
36+
hash: bytesToHex(hash),
37+
},
38+
}),
39+
(e) => new HubError('bad_request.invalid_param', e as Error)
40+
);
41+
return hexSignature.andThen((hex) => hexStringToBytes(hex));
42+
}
43+
44+
public async signVerificationEthAddressClaim(claim: VerificationEthAddressClaim): HubAsyncResult<Uint8Array> {
45+
const hexSignature = await ResultAsync.fromPromise(
46+
this._viemLocalAccount.signTypedData({
47+
domain: EIP_712_FARCASTER_DOMAIN,
48+
types: { VerificationClaim: EIP_712_FARCASTER_VERIFICATION_CLAIM },
49+
primaryType: 'VerificationClaim',
50+
message: {
51+
...claim,
52+
},
53+
}),
54+
(e) => new HubError('bad_request.invalid_param', e as Error)
55+
);
56+
return hexSignature.andThen((hex) => hexStringToBytes(hex));
57+
}
58+
}

0 commit comments

Comments
 (0)