Skip to content

Commit 5f14318

Browse files
feat(abstract-eth): support eip712 signing
Ticket: SC-2622
1 parent 2c56848 commit 5f14318

17 files changed

+636
-342
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { BaseMessage, MessageOptions, MessageStandardType } from '@bitgo/sdk-core';
2+
import { SignTypedDataVersion, TypedDataUtils } from '@metamask/eth-sig-util';
3+
4+
export class EIP712Message extends BaseMessage {
5+
constructor(options: MessageOptions) {
6+
super({
7+
...options,
8+
type: MessageStandardType.EIP712,
9+
});
10+
}
11+
12+
async getSignablePayload(): Promise<string | Buffer> {
13+
const data = JSON.parse(this.payload);
14+
const sanitizedData = TypedDataUtils.sanitizeData(data);
15+
const parts: Buffer[] = [];
16+
17+
parts.push(Buffer.from('1901', 'hex'));
18+
parts.push(TypedDataUtils.eip712DomainHash(data, SignTypedDataVersion.V4));
19+
if (sanitizedData.primaryType !== 'EIP712Domain') {
20+
parts.push(
21+
TypedDataUtils.hashStruct(
22+
sanitizedData.primaryType as string,
23+
sanitizedData.message,
24+
sanitizedData.types,
25+
SignTypedDataVersion.V4
26+
)
27+
);
28+
}
29+
30+
this.signablePayload = Buffer.concat(parts);
31+
return this.signablePayload;
32+
}
33+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
2+
import { BaseMessageBuilder, IMessage, MessageOptions, MessageStandardType } from '@bitgo/sdk-core';
3+
import { EIP712Message } from './eip712Message';
4+
5+
export class Eip712MessageBuilder extends BaseMessageBuilder {
6+
public constructor(_coinConfig: Readonly<CoinConfig>) {
7+
super(_coinConfig, MessageStandardType.EIP712);
8+
}
9+
10+
async buildMessage(options: MessageOptions): Promise<IMessage> {
11+
return new EIP712Message(options);
12+
}
13+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './eip712Message';
2+
export * from './eip712MessageBuilder';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './messageBuilderFactory';
22
export * from './eip191';
3+
export * from './eip712';

modules/abstract-eth/src/lib/messages/messageBuilderFactory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Eip191MessageBuilder } from './eip191';
2+
import { Eip712MessageBuilder } from './eip712';
23
import { BaseCoin as CoinConfig } from '@bitgo/statics';
34
import { BaseMessageBuilderFactory, IMessageBuilder, MessageStandardType } from '@bitgo/sdk-core';
45

@@ -11,6 +12,8 @@ export class MessageBuilderFactory extends BaseMessageBuilderFactory {
1112
switch (type) {
1213
case MessageStandardType.EIP191:
1314
return new Eip191MessageBuilder(this.coinConfig);
15+
case MessageStandardType.EIP712:
16+
return new Eip712MessageBuilder(this.coinConfig);
1417
default:
1518
throw new Error(`Invalid message standard ${type}`);
1619
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { MessageStandardType, serializeSignatures } from '@bitgo/sdk-core';
2+
import { coins } from '@bitgo/statics';
3+
import { MessageBuilderFactory } from '../../../src';
4+
import should from 'should';
5+
import { MessageBuildingTestConfig } from './abstractEthMessageTestTypes';
6+
7+
const coinConfig = coins.get('eth');
8+
9+
export function testEthMessageBuilding(testConfig: MessageBuildingTestConfig): void {
10+
const { messageType, messageBuilderClass, messageClass, test } = testConfig;
11+
12+
describe(`${messageType} - Build Method`, () => {
13+
const factory = new MessageBuilderFactory(coinConfig);
14+
const { payload, metadata, signature, signer } = test.input;
15+
16+
it('should initialize with the correct message type', () => {
17+
const builder = factory.getMessageBuilder(messageType);
18+
builder.should.be.instanceof(messageBuilderClass);
19+
});
20+
21+
it('should build a valid message', async () => {
22+
const builder = factory.getMessageBuilder(messageType);
23+
builder.setPayload(payload).setMetadata(metadata || {});
24+
25+
const builtMessage = await builder.build();
26+
builtMessage.getType().should.equal(messageType);
27+
builtMessage.getPayload().should.equal(payload);
28+
29+
if (metadata) {
30+
should.deepEqual(builtMessage.getMetadata(), metadata);
31+
} else {
32+
should.deepEqual(builtMessage.getMetadata(), {});
33+
}
34+
});
35+
36+
it('should throw an error when building without setting the payload', async () => {
37+
const builder = factory.getMessageBuilder(messageType);
38+
await builder.build().should.be.rejectedWith('Message payload must be set before building the message');
39+
});
40+
41+
it('should include signers when building a message', async () => {
42+
const builder = factory.getMessageBuilder(messageType);
43+
builder.setPayload(payload);
44+
builder.addSigner(signer);
45+
46+
const message = await builder.build();
47+
message.getSigners().should.containEql(signer);
48+
});
49+
50+
it('should include signatures when building a message', async () => {
51+
const builder = factory.getMessageBuilder(messageType);
52+
builder.setPayload(payload);
53+
builder.addSignature(signature);
54+
55+
const message = await builder.build();
56+
message.getSignatures().should.containEql(signature);
57+
});
58+
59+
it('should override metadata.encoding with utf8', async () => {
60+
const builder = factory.getMessageBuilder(messageType);
61+
builder.setPayload(payload);
62+
builder.setMetadata({ encoding: 'hex', customData: 'test data' });
63+
64+
const message = await builder.build();
65+
const metadata = message.getMetadata();
66+
should(metadata).not.be.undefined();
67+
should(metadata).have.property('encoding', 'utf8');
68+
should(metadata).have.property('customData', 'test data');
69+
});
70+
});
71+
72+
describe(`${messageType} - From Broadcast Format`, () => {
73+
const factory = new MessageBuilderFactory(coinConfig);
74+
const { payload, signature, signer, metadata } = test.input;
75+
76+
const broadcastMessage = {
77+
payload,
78+
type: messageType,
79+
serializedSignatures: serializeSignatures([signature]),
80+
signers: [signer],
81+
metadata: metadata,
82+
};
83+
84+
it('should reconstruct a message from broadcast format', async () => {
85+
const builder = factory.getMessageBuilder(messageType);
86+
const message = await builder.fromBroadcastFormat(broadcastMessage);
87+
88+
message.getType().should.equal(messageType);
89+
message.getPayload().should.equal(payload);
90+
message.getSignatures().should.containEql(signature);
91+
message.getSigners().should.containEql(signer);
92+
message.should.be.instanceof(messageClass);
93+
94+
if (metadata) {
95+
should.deepEqual(message.getMetadata(), metadata);
96+
} else {
97+
should.deepEqual(message.getMetadata(), {});
98+
}
99+
});
100+
101+
it('should throw an error for incorrect message type', async () => {
102+
const builder = factory.getMessageBuilder(messageType);
103+
const broadcastMessageWrongType = { ...broadcastMessage, type: MessageStandardType.UNKNOWN };
104+
await builder
105+
.fromBroadcastFormat(broadcastMessageWrongType)
106+
.should.be.rejectedWith(`Invalid message type, expected ${messageType}`);
107+
});
108+
});
109+
110+
describe(`${messageType} - From Broadcast String`, () => {
111+
const { payload, signature, signer } = test.input;
112+
const broadcastHex = test.broadcastHex;
113+
114+
it('should parse broadcastable string and return correct builder type', async () => {
115+
const factory = new MessageBuilderFactory(coinConfig);
116+
const builder = factory.fromBroadcastString(broadcastHex);
117+
const message = await builder.build();
118+
119+
message.getType().should.equal(messageType);
120+
message.getPayload().should.equal(payload);
121+
message.getSignatures().should.containEql(signature);
122+
message.getSigners().should.containEql(signer);
123+
});
124+
});
125+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
BaseMessage,
3+
BaseMessageBuilder,
4+
MessageMetadata,
5+
MessageOptions,
6+
MessageStandardType,
7+
Signature,
8+
} from '@bitgo/sdk-core';
9+
10+
type MessageCtor = new (options: MessageOptions) => BaseMessage;
11+
12+
type MessageBuildParams = { payload: string; metadata?: MessageMetadata };
13+
type SignatureParams = { signature: Signature; signer: string };
14+
15+
type MessageVerificationParams = { expectedSignableHex: string };
16+
type MessageVerificationParamsBase64 = { expectedSignableBase64: string };
17+
18+
type MessageTestCase = { input: MessageBuildParams; expected: MessageVerificationParams };
19+
type SignedMessageTestCase = {
20+
input: MessageBuildParams & SignatureParams;
21+
expected: MessageVerificationParams & MessageVerificationParamsBase64;
22+
};
23+
24+
type MessageBuilderTestCase = {
25+
input: MessageBuildParams & SignatureParams;
26+
expected: MessageVerificationParams;
27+
broadcastHex: string;
28+
};
29+
30+
export type MessageTestConfig = {
31+
messageType: MessageStandardType;
32+
messageClass: MessageCtor;
33+
tests: Record<string, MessageTestCase>;
34+
signedTest: SignedMessageTestCase;
35+
};
36+
37+
export type MessageBuildingTestConfig = {
38+
messageType: MessageStandardType;
39+
messageBuilderClass: typeof BaseMessageBuilder;
40+
messageClass: typeof BaseMessage;
41+
test: MessageBuilderTestCase;
42+
};
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { serializeSignatures } from '@bitgo/sdk-core';
2+
import { coins } from '@bitgo/statics';
3+
import should from 'should';
4+
import { MessageTestConfig } from './abstractEthMessageTestTypes';
5+
6+
const coinConfig = coins.get('eth');
7+
8+
export function testEthMessageSigning(testConfig: MessageTestConfig): void {
9+
const { messageType, messageClass, tests, signedTest } = testConfig;
10+
11+
describe(`${messageType} - Message Type`, () => {
12+
it('should have the correct message type', () => {
13+
const msgInstance = new messageClass({
14+
...signedTest.input,
15+
coinConfig,
16+
});
17+
msgInstance.getType().should.equal(messageType);
18+
});
19+
});
20+
21+
describe(`${messageType} - Signable Payload Generation`, () => {
22+
Object.entries(tests).map(([key, { input, expected }]) => {
23+
it(`should generate the correct signable payload for message '${key}'`, async () => {
24+
const message = new messageClass({
25+
...input,
26+
coinConfig,
27+
});
28+
29+
const signablePayload = await message.getSignablePayload();
30+
signablePayload.toString('hex').should.equal(expected.expectedSignableHex);
31+
32+
if (input.metadata) {
33+
should.deepEqual(message.getMetadata(), input.metadata);
34+
} else {
35+
should.deepEqual(message.getMetadata(), {});
36+
}
37+
});
38+
});
39+
});
40+
41+
describe(`${messageType} - Maintaining Signers and Signatures`, () => {
42+
const { payload, signature, signer } = signedTest.input;
43+
44+
it('should be created with the correct signatures and signers', () => {
45+
const message = new messageClass({
46+
coinConfig,
47+
payload,
48+
signatures: [signature],
49+
signers: [signer],
50+
});
51+
52+
message.getSignatures().should.containEql(signature);
53+
message.getSigners().should.containEql(signer);
54+
});
55+
56+
it('should maintain signatures and signers correctly', () => {
57+
const message = new messageClass({
58+
coinConfig,
59+
payload,
60+
signatures: [signature],
61+
signers: [signer],
62+
});
63+
64+
message.addSignature({
65+
publicKey: { pub: 'pub1' },
66+
signature: Buffer.from('new-signature'),
67+
});
68+
message.addSigner('new-signer');
69+
70+
message.getSignatures().should.containEql({
71+
publicKey: { pub: 'pub1' },
72+
signature: Buffer.from('new-signature'),
73+
});
74+
message.getSigners().should.containEql('new-signer');
75+
76+
// Test replacing all
77+
message.setSignatures([
78+
{
79+
publicKey: { pub: 'pub2' },
80+
signature: Buffer.from('replaced-signature'),
81+
},
82+
]);
83+
message.setSigners(['replaced-signer']);
84+
85+
message.getSignatures().should.deepEqual([
86+
{
87+
publicKey: { pub: 'pub2' },
88+
signature: Buffer.from('replaced-signature'),
89+
},
90+
]);
91+
message.getSigners().should.deepEqual(['replaced-signer']);
92+
});
93+
});
94+
95+
describe(`${messageType} - Broadcast Format`, () => {
96+
const { payload, signature, signer } = signedTest.input;
97+
const { expectedSignableBase64 } = signedTest.expected;
98+
99+
it('should convert to broadcast format correctly', async () => {
100+
const message = new messageClass({
101+
coinConfig,
102+
payload,
103+
signatures: [signature],
104+
signers: [signer],
105+
});
106+
107+
const broadcastFormat = await message.toBroadcastFormat();
108+
const expectedSerializedSignatures = serializeSignatures([signature]);
109+
110+
broadcastFormat.type.should.equal(messageType);
111+
broadcastFormat.payload.should.equal(message.getPayload());
112+
broadcastFormat.serializedSignatures?.should.deepEqual(expectedSerializedSignatures);
113+
broadcastFormat.signers?.should.deepEqual([signer]);
114+
broadcastFormat.signablePayload?.should.equal(expectedSignableBase64);
115+
116+
if (broadcastFormat.metadata) {
117+
broadcastFormat.metadata.should.deepEqual(message.getMetadata());
118+
} else {
119+
should.deepEqual(message.getMetadata(), {});
120+
}
121+
});
122+
123+
it('should convert to broadcast string correctly', async () => {
124+
const message = new messageClass({
125+
coinConfig,
126+
payload,
127+
signatures: [signature],
128+
signers: [signer],
129+
});
130+
131+
const broadcastHex = await message.toBroadcastString();
132+
const broadcastString = Buffer.from(broadcastHex, 'hex').toString();
133+
const parsedBroadcast = JSON.parse(broadcastString);
134+
const expectedSerializedSignatures = serializeSignatures([signature]);
135+
136+
parsedBroadcast.type.should.equal(messageType);
137+
parsedBroadcast.payload.should.equal(message.getPayload());
138+
parsedBroadcast.serializedSignatures.should.deepEqual(expectedSerializedSignatures);
139+
parsedBroadcast.signers.should.deepEqual([signer]);
140+
parsedBroadcast.metadata.should.deepEqual(message.getMetadata());
141+
});
142+
});
143+
}

0 commit comments

Comments
 (0)