Skip to content

Commit 7f0ede7

Browse files
Merge branch 'master' into rel/latest
2 parents c0dfa98 + 2ad9e03 commit 7f0ede7

File tree

88 files changed

+4115
-98
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+4115
-98
lines changed

.gitcommitscopes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
account-lib
12
sdk-coin-rune
23
sdk-coin-sui
4+
sdk-core
35
statics

modules/abstract-eth/src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * from './transferBuilder';
99
export * from './types';
1010
export * from './utils';
1111
export * from './walletUtil';
12+
export * from './messages';
1213

1314
// for Backwards Compatibility
1415
import * as Interface from './iface';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { BaseMessage, MessageOptions, MessageStandardType } from '@bitgo/sdk-core';
2+
3+
/**
4+
* Implementation of Message for EIP191 standard
5+
*/
6+
export class EIP191Message extends BaseMessage {
7+
constructor(options: MessageOptions) {
8+
super({
9+
...options,
10+
type: MessageStandardType.EIP191,
11+
});
12+
}
13+
14+
/**
15+
* Returns the hash of the EIP-191 prefixed message
16+
*/
17+
async getSignablePayload(): Promise<string | Buffer> {
18+
if (!this.signablePayload) {
19+
const prefix = `\u0019Ethereum Signed Message:\n${this.payload.length}`;
20+
this.signablePayload = Buffer.from(prefix.concat(this.payload)).toString('hex');
21+
}
22+
return this.signablePayload;
23+
}
24+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { EIP191Message } from './eip191Message';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { BaseMessageBuilder, BroadcastableMessage, IMessage, MessageStandardType } from '@bitgo/sdk-core';
4+
5+
/**
6+
* Builder for EIP-191 messages
7+
*/
8+
export class Eip191MessageBuilder extends BaseMessageBuilder {
9+
/**
10+
* Base constructor.
11+
* @param _coinConfig BaseCoin from statics library
12+
*/
13+
public constructor(_coinConfig: Readonly<CoinConfig>) {
14+
super(_coinConfig, MessageStandardType.EIP191);
15+
}
16+
17+
/**
18+
* Build a signable message using the EIP-191 standard
19+
* with previously set input and metadata
20+
* @returns A signable message
21+
*/
22+
public async build(): Promise<IMessage> {
23+
try {
24+
if (!this.payload) {
25+
throw new Error('Message payload must be set before building the message');
26+
}
27+
return new EIP191Message({
28+
coinConfig: this.coinConfig,
29+
payload: this.payload,
30+
signatures: this.signatures,
31+
signers: this.signers,
32+
metadata: {
33+
...this.metadata,
34+
encoding: 'utf8',
35+
},
36+
});
37+
} catch (err) {
38+
if (err instanceof Error) {
39+
throw err;
40+
}
41+
throw new Error('Failed to build EIP-191 message');
42+
}
43+
}
44+
45+
/**
46+
* Parse a broadcastable message back into a message
47+
* @param broadcastMessage The broadcastable message to parse
48+
* @returns The parsed message
49+
*/
50+
public async fromBroadcastFormat(broadcastMessage: BroadcastableMessage): Promise<IMessage> {
51+
const { type, payload, signatures, signers, metadata } = broadcastMessage;
52+
if (type !== MessageStandardType.EIP191) {
53+
throw new Error(`Invalid message type, expected ${MessageStandardType.EIP191}`);
54+
}
55+
return new EIP191Message({
56+
coinConfig: this.coinConfig,
57+
payload,
58+
signatures,
59+
signers,
60+
metadata: {
61+
...metadata,
62+
encoding: 'utf8',
63+
},
64+
});
65+
}
66+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './eip191Message';
2+
export * from './eip191MessageBuilder';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './messageBuilderFactory';
2+
export * from './eip191';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Eip191MessageBuilder } from './eip191';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { BaseMessageBuilderFactory, IMessageBuilder, MessageStandardType } from '@bitgo/sdk-core';
4+
5+
export class MessageBuilderFactory extends BaseMessageBuilderFactory {
6+
constructor(coinConfig: Readonly<CoinConfig>) {
7+
super(coinConfig);
8+
}
9+
10+
public getMessageBuilder(type: MessageStandardType): IMessageBuilder {
11+
switch (type) {
12+
case MessageStandardType.EIP191:
13+
return new Eip191MessageBuilder(this.coinConfig);
14+
default:
15+
throw new Error(`Invalid message standard ${type}`);
16+
}
17+
}
18+
}

modules/abstract-eth/test/unit/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './transactionBuilder';
22
export * from './token';
33
export * from './transaction';
44
export * from './coin';
5+
export * from './messages';
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import 'should';
2+
import sinon from 'sinon';
3+
import { MessageStandardType } from '@bitgo/sdk-core';
4+
import { fixtures } from '../fixtures';
5+
import { EIP191Message } from '../../../../src';
6+
7+
describe('EIP191 Message', () => {
8+
const sandbox = sinon.createSandbox();
9+
10+
afterEach(() => {
11+
sandbox.restore();
12+
});
13+
14+
it('should initialize with the correct type', () => {
15+
const message = new EIP191Message({
16+
coinConfig: fixtures.coin,
17+
payload: fixtures.messages.validMessage,
18+
});
19+
20+
message.getType().should.equal(MessageStandardType.EIP191);
21+
});
22+
23+
it('should generate the correct signable payload with Ethereum prefix', async () => {
24+
const message = new EIP191Message({
25+
coinConfig: fixtures.coin,
26+
payload: fixtures.messages.validMessage,
27+
});
28+
29+
const signablePayload = await message.getSignablePayload();
30+
// Message is prefixed with "\u0019Ethereum Signed Message:\n<length><message>"
31+
const expectedPrefix = `\u0019Ethereum Signed Message:\n${fixtures.messages.validMessage.length}`;
32+
const expectedPayload = Buffer.from(expectedPrefix.concat(fixtures.messages.validMessage)).toString('hex');
33+
34+
signablePayload.should.equal(expectedPayload);
35+
});
36+
37+
it('should handle empty messages correctly', async () => {
38+
const message = new EIP191Message({
39+
coinConfig: fixtures.coin,
40+
payload: fixtures.messages.emptyMessage,
41+
});
42+
43+
const signablePayload = await message.getSignablePayload();
44+
// Empty message has length 0
45+
const expectedPrefix = `\u0019Ethereum Signed Message:\n0`;
46+
const expectedPayload = Buffer.from(expectedPrefix.concat('')).toString('hex');
47+
48+
signablePayload.should.equal(expectedPayload);
49+
});
50+
51+
it('should handle messages with special characters', async () => {
52+
const message = new EIP191Message({
53+
coinConfig: fixtures.coin,
54+
payload: fixtures.messages.specialCharsMessage,
55+
});
56+
57+
const signablePayload = await message.getSignablePayload();
58+
const expectedPrefix = `\u0019Ethereum Signed Message:\n${fixtures.messages.specialCharsMessage.length}`;
59+
const expectedPayload = Buffer.from(expectedPrefix.concat(fixtures.messages.specialCharsMessage)).toString('hex');
60+
61+
signablePayload.should.equal(expectedPayload);
62+
});
63+
64+
it('should reuse existing signable payload if already set', async () => {
65+
const message = new EIP191Message({
66+
coinConfig: fixtures.coin,
67+
payload: fixtures.messages.validMessage,
68+
signablePayload: 'predefined-payload',
69+
});
70+
71+
const signablePayload = await message.getSignablePayload();
72+
signablePayload.should.equal('predefined-payload');
73+
});
74+
75+
it('should maintain signatures and signers correctly', () => {
76+
const message = new EIP191Message({
77+
coinConfig: fixtures.coin,
78+
payload: fixtures.messages.validMessage,
79+
signatures: [fixtures.eip191.signature],
80+
signers: [fixtures.eip191.signer],
81+
});
82+
83+
message.getSignatures().should.containEql(fixtures.eip191.signature);
84+
message.getSigners().should.containEql(fixtures.eip191.signer);
85+
86+
// Test adding new ones
87+
message.addSignature('new-signature');
88+
message.addSigner('new-signer');
89+
90+
message.getSignatures().should.containEql('new-signature');
91+
message.getSigners().should.containEql('new-signer');
92+
93+
// Test replacing all
94+
message.setSignatures(['replaced-signature']);
95+
message.setSigners(['replaced-signer']);
96+
97+
message.getSignatures().should.deepEqual(['replaced-signature']);
98+
message.getSigners().should.deepEqual(['replaced-signer']);
99+
});
100+
101+
it('should store and retrieve metadata correctly', () => {
102+
const message = new EIP191Message({
103+
coinConfig: fixtures.coin,
104+
payload: fixtures.messages.validMessage,
105+
metadata: fixtures.eip191.metadata,
106+
});
107+
108+
message.getMetadata()!.should.deepEqual(fixtures.eip191.metadata);
109+
});
110+
111+
describe('Broadcast Format', () => {
112+
it('should convert to broadcast format correctly', async () => {
113+
const message = new EIP191Message({
114+
coinConfig: fixtures.coin,
115+
payload: fixtures.messages.validMessage,
116+
signatures: [fixtures.eip191.signature],
117+
signers: [fixtures.eip191.signer],
118+
metadata: fixtures.eip191.metadata,
119+
signablePayload: 'test-signable-payload',
120+
});
121+
122+
const broadcastFormat = await message.toBroadcastFormat();
123+
124+
broadcastFormat.type.should.equal(MessageStandardType.EIP191);
125+
broadcastFormat.payload.should.equal(fixtures.messages.validMessage);
126+
broadcastFormat.signatures.should.deepEqual([fixtures.eip191.signature]);
127+
broadcastFormat.signers.should.deepEqual([fixtures.eip191.signer]);
128+
broadcastFormat.metadata!.should.deepEqual(fixtures.eip191.metadata);
129+
broadcastFormat.signablePayload!.should.equal('test-signable-payload');
130+
});
131+
132+
it('should throw error when broadcasting without signatures', async () => {
133+
const message = new EIP191Message({
134+
coinConfig: fixtures.coin,
135+
payload: fixtures.messages.validMessage,
136+
signers: [fixtures.eip191.signer],
137+
});
138+
139+
await message
140+
.toBroadcastFormat()
141+
.should.be.rejectedWith('No signatures available for broadcast. Call setSignatures or addSignature first.');
142+
});
143+
144+
it('should throw error when broadcasting without signers', async () => {
145+
const message = new EIP191Message({
146+
coinConfig: fixtures.coin,
147+
payload: fixtures.messages.validMessage,
148+
signatures: [fixtures.eip191.signature],
149+
});
150+
151+
await message
152+
.toBroadcastFormat()
153+
.should.be.rejectedWith('No signers available for broadcast. Call setSigners or addSigner first.');
154+
});
155+
156+
it('should convert to broadcast string correctly', async () => {
157+
const message = new EIP191Message({
158+
coinConfig: fixtures.coin,
159+
payload: fixtures.messages.validMessage,
160+
signatures: [fixtures.eip191.signature],
161+
signers: [fixtures.eip191.signer],
162+
metadata: fixtures.eip191.metadata,
163+
});
164+
165+
const broadcastString = await message.toBroadcastString();
166+
const parsedBroadcast = JSON.parse(broadcastString);
167+
168+
parsedBroadcast.type.should.equal(MessageStandardType.EIP191);
169+
parsedBroadcast.payload.should.equal(fixtures.messages.validMessage);
170+
parsedBroadcast.signatures.should.deepEqual([fixtures.eip191.signature]);
171+
parsedBroadcast.signers.should.deepEqual([fixtures.eip191.signer]);
172+
parsedBroadcast.metadata.should.deepEqual(fixtures.eip191.metadata);
173+
});
174+
});
175+
});

0 commit comments

Comments
 (0)