Skip to content

Commit 29e8823

Browse files
authored
Merge pull request #6398 from BitGo/coin-4737-sol-message-builder
test(sdk-coin-sol): unit tests for SimpleMessage support
2 parents 52c7cc2 + 4ed22a0 commit 29e8823

File tree

11 files changed

+330
-77
lines changed

11 files changed

+330
-77
lines changed

.gitcommitscopes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
account-lib
22
sdk-coin-ada
33
sdk-coin-rune
4+
sdk-coin-sol
45
sdk-coin-sui
56
sdk-core
67
statics

modules/abstract-eth/test/unit/messages/eip191/eip191Message.ts

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -140,36 +140,12 @@ describe('EIP191 Message', () => {
140140
const expectedSerializedSignatures = serializeSignatures([fixtures.eip191.signature]);
141141
broadcastFormat.type.should.equal(MessageStandardType.EIP191);
142142
broadcastFormat.payload.should.equal(fixtures.messages.validMessage);
143-
broadcastFormat.serializedSignatures.should.deepEqual(expectedSerializedSignatures);
144-
broadcastFormat.signers.should.deepEqual([fixtures.eip191.signer]);
143+
broadcastFormat.serializedSignatures?.should.deepEqual(expectedSerializedSignatures);
144+
broadcastFormat.signers?.should.deepEqual([fixtures.eip191.signer]);
145145
broadcastFormat.metadata!.should.deepEqual(fixtures.eip191.metadata);
146146
broadcastFormat.signablePayload!.should.equal('dGVzdC1zaWduYWJsZS1wYXlsb2Fk');
147147
});
148148

149-
it('should throw error when broadcasting without signatures', async () => {
150-
const message = new EIP191Message({
151-
coinConfig: fixtures.coin,
152-
payload: fixtures.messages.validMessage,
153-
signers: [fixtures.eip191.signer],
154-
});
155-
156-
await message
157-
.toBroadcastFormat()
158-
.should.be.rejectedWith('No signatures available for broadcast. Call setSignatures or addSignature first.');
159-
});
160-
161-
it('should throw error when broadcasting without signers', async () => {
162-
const message = new EIP191Message({
163-
coinConfig: fixtures.coin,
164-
payload: fixtures.messages.validMessage,
165-
signatures: [fixtures.eip191.signature],
166-
});
167-
168-
await message
169-
.toBroadcastFormat()
170-
.should.be.rejectedWith('No signers available for broadcast. Call setSigners or addSigner first.');
171-
});
172-
173149
it('should convert to broadcast string correctly', async () => {
174150
const message = new EIP191Message({
175151
coinConfig: fixtures.coin,
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { BaseCoin } from '@bitgo/statics';
2+
import sinon from 'sinon';
3+
import should from 'should';
4+
import { MessageBuilderFactory } from '../../../src';
5+
import {
6+
BroadcastableMessage,
7+
IMessage,
8+
IMessageBuilder,
9+
MessageStandardType,
10+
SerializedSignature,
11+
SimpleMessageBuilder,
12+
} from '@bitgo/sdk-core';
13+
14+
describe('Solana MessageBuilderFactory', function () {
15+
let sandbox: sinon.SinonSandbox;
16+
let factory: MessageBuilderFactory;
17+
const coinConfig = { name: 'tsol' } as BaseCoin;
18+
19+
// Common test data
20+
const signatureBase64 = Buffer.from('signature1').toString('base64');
21+
const testBroadcastMessage: BroadcastableMessage = {
22+
type: MessageStandardType.SIMPLE,
23+
payload: 'test message',
24+
serializedSignatures: [
25+
{
26+
publicKey: 'pubkey1',
27+
signature: signatureBase64,
28+
},
29+
],
30+
signers: ['signer1'],
31+
metadata: {},
32+
};
33+
34+
const unsupportedMessageTypes = [
35+
MessageStandardType.UNKNOWN,
36+
MessageStandardType.EIP191,
37+
MessageStandardType.CIP8,
38+
'UNSUPPORTED' as MessageStandardType,
39+
];
40+
41+
// Helper functions
42+
const assertSimpleMessageBuilder = (builder: IMessageBuilder) => {
43+
should.exist(builder);
44+
builder.should.be.instanceof(SimpleMessageBuilder);
45+
};
46+
47+
const assertBuilderMessageProperties = async (builder: IMessageBuilder, expectedPayload: string) => {
48+
const message = await builder.build();
49+
message.getType().should.equal(MessageStandardType.SIMPLE);
50+
message.getPayload()!.should.equal(expectedPayload);
51+
message.getSignatures().should.have.length(1);
52+
message.getSigners().should.have.length(1);
53+
message.getSigners()[0].should.equal('signer1');
54+
message.getMetadata()!.should.be.an.Object();
55+
return message;
56+
};
57+
58+
const assertSignatureProperties = (message: IMessage) => {
59+
const signature = message.getSignatures()[0];
60+
signature.should.have.properties(['publicKey', 'signature']);
61+
signature.publicKey.pub.should.equal('pubkey1');
62+
signature.signature.toString('base64').should.equal(signatureBase64);
63+
};
64+
65+
const assertBroadcastFormatProperties = async (
66+
message: IMessage,
67+
expectedSerializedSignatures?: SerializedSignature[]
68+
) => {
69+
const rebroadcastMessage = await message.toBroadcastFormat();
70+
rebroadcastMessage.should.have.properties(['type', 'payload', 'serializedSignatures', 'signers', 'metadata']);
71+
rebroadcastMessage.type.should.equal(MessageStandardType.SIMPLE);
72+
rebroadcastMessage.payload.should.equal('test message');
73+
rebroadcastMessage.serializedSignatures?.should.deepEqual(expectedSerializedSignatures);
74+
rebroadcastMessage.signers?.should.deepEqual(['signer1']);
75+
};
76+
77+
beforeEach(function () {
78+
sandbox = sinon.createSandbox();
79+
factory = new MessageBuilderFactory(coinConfig);
80+
});
81+
82+
afterEach(function () {
83+
sandbox.restore();
84+
});
85+
86+
describe('getMessageBuilder', function () {
87+
it('should return SimpleMessageBuilder for SIMPLE type', function () {
88+
const builder = factory.getMessageBuilder(MessageStandardType.SIMPLE);
89+
assertSimpleMessageBuilder(builder);
90+
});
91+
92+
it('should throw error for unsupported message type', function () {
93+
unsupportedMessageTypes.forEach((type) => {
94+
should.throws(() => factory.getMessageBuilder(type), new RegExp(`Invalid message standard ${type}`));
95+
});
96+
});
97+
});
98+
99+
describe('fromBroadcastFormat', function () {
100+
it('should get the correct builder type from broadcastable message', async function () {
101+
const builder = factory.fromBroadcastFormat(testBroadcastMessage);
102+
assertSimpleMessageBuilder(builder);
103+
104+
const message = await assertBuilderMessageProperties(builder, 'test message');
105+
assertSignatureProperties(message);
106+
await assertBroadcastFormatProperties(message, testBroadcastMessage.serializedSignatures);
107+
});
108+
109+
it('should throw for unsupported message type in broadcastable message', function () {
110+
const broadcastMessage = {
111+
...testBroadcastMessage,
112+
type: MessageStandardType.EIP191,
113+
};
114+
115+
should.throws(
116+
() => factory.fromBroadcastFormat(broadcastMessage),
117+
new RegExp(`Invalid message standard ${MessageStandardType.EIP191}`)
118+
);
119+
});
120+
});
121+
122+
describe('fromBroadcastString', function () {
123+
it('should parse broadcastable string and return correct builder type', async function () {
124+
const broadcastString = JSON.stringify(testBroadcastMessage);
125+
const builder = factory.fromBroadcastString(broadcastString);
126+
127+
assertSimpleMessageBuilder(builder);
128+
const message = await assertBuilderMessageProperties(builder, 'test message');
129+
assertSignatureProperties(message);
130+
await assertBroadcastFormatProperties(message, testBroadcastMessage.serializedSignatures);
131+
});
132+
133+
it('should throw for invalid JSON string', function () {
134+
try {
135+
factory.fromBroadcastString('{invalid json');
136+
fail('Expected error not thrown');
137+
} catch (error) {
138+
error.should.be.instanceof(SyntaxError);
139+
}
140+
});
141+
});
142+
});
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { BaseCoin } from '@bitgo/statics';
2+
import sinon from 'sinon';
3+
import should from 'should';
4+
import { BroadcastableMessage, MessageStandardType, SimpleMessage } from '@bitgo/sdk-core';
5+
import { MessageBuilderFactory } from '../../../src';
6+
7+
describe('Solana SimpleMessageBuilder', function () {
8+
let sandbox: sinon.SinonSandbox;
9+
let factory: MessageBuilderFactory;
10+
const coinConfig = { name: 'tsol' } as BaseCoin;
11+
12+
beforeEach(function () {
13+
sandbox = sinon.createSandbox();
14+
factory = new MessageBuilderFactory(coinConfig);
15+
});
16+
17+
afterEach(function () {
18+
sandbox.restore();
19+
});
20+
21+
describe('build', function () {
22+
it('should build a SimpleMessage with correct payload', async function () {
23+
const payload = 'Hello, Solana!';
24+
const builder = factory.getMessageBuilder(MessageStandardType.SIMPLE);
25+
builder.setPayload(payload);
26+
27+
const message = await builder.build();
28+
should.exist(message);
29+
message.should.be.instanceof(SimpleMessage);
30+
should.equal(message.getType(), MessageStandardType.SIMPLE);
31+
should.equal(message.getPayload(), payload);
32+
});
33+
34+
it('should build a SimpleMessage with signatures', async function () {
35+
const payload = 'Sign this message';
36+
const signatures = [
37+
{
38+
publicKey: { pub: 'solPubKey1' },
39+
signature: Buffer.from('solSignature1'),
40+
},
41+
];
42+
43+
const builder = factory.getMessageBuilder(MessageStandardType.SIMPLE);
44+
builder.setPayload(payload).setSignatures(signatures);
45+
46+
const message = await builder.build();
47+
should.exist(message);
48+
should.equal(message.getPayload(), payload);
49+
should.deepEqual(message.getSignatures(), signatures);
50+
});
51+
52+
it('should build a SimpleMessage with signers', async function () {
53+
const payload = 'Message with signers';
54+
const signers = ['solSigner1', 'solSigner2'];
55+
56+
const builder = factory.getMessageBuilder(MessageStandardType.SIMPLE);
57+
builder.setPayload(payload).setSigners(signers);
58+
59+
const message = await builder.build();
60+
should.exist(message);
61+
should.equal(message.getPayload(), payload);
62+
should.deepEqual(message.getSigners(), signers);
63+
});
64+
65+
it('should build a SimpleMessage with metadata', async function () {
66+
const payload = 'Message with metadata';
67+
const metadata = { solNetwork: 'testnet', timestamp: 1625097600 };
68+
69+
const builder = factory.getMessageBuilder(MessageStandardType.SIMPLE);
70+
builder.setPayload(payload).setMetadata(metadata);
71+
72+
const message = await builder.build();
73+
should.exist(message);
74+
should.equal(message.getPayload(), payload);
75+
const messageMetadata = message.getMetadata();
76+
should.equal(messageMetadata?.solNetwork, metadata.solNetwork);
77+
should.equal(messageMetadata?.timestamp, metadata.timestamp);
78+
});
79+
80+
it('should throw error when building without payload', async function () {
81+
const builder = factory.getMessageBuilder(MessageStandardType.SIMPLE);
82+
await should(builder.build()).be.rejectedWith('Message payload must be set before building the message');
83+
});
84+
});
85+
86+
describe('getSignablePayload', function () {
87+
it('should return Buffer with correct payload', async function () {
88+
const payload = 'Signable Solana message';
89+
const builder = factory.getMessageBuilder(MessageStandardType.SIMPLE);
90+
builder.setPayload(payload);
91+
92+
const message = await builder.build();
93+
const signablePayload = await message.getSignablePayload();
94+
95+
should.exist(signablePayload);
96+
Buffer.isBuffer(signablePayload).should.be.true();
97+
signablePayload.toString().should.equal(payload);
98+
});
99+
});
100+
101+
describe('toBroadcastFormat', function () {
102+
it('should convert SimpleMessage to broadcastable format', async function () {
103+
const payload = 'Broadcast me';
104+
const signers = ['solAddress1'];
105+
const signatures = [
106+
{
107+
publicKey: { pub: 'solPubKey1' },
108+
signature: Buffer.from('solSignature1'),
109+
},
110+
];
111+
112+
const builder = factory.getMessageBuilder(MessageStandardType.SIMPLE);
113+
builder.setPayload(payload).setSigners(signers).setSignatures(signatures);
114+
115+
const message = await builder.build();
116+
const broadcastFormat = await message.toBroadcastFormat();
117+
118+
should.exist(broadcastFormat);
119+
should.equal(broadcastFormat.type, MessageStandardType.SIMPLE);
120+
should.equal(broadcastFormat.payload, payload);
121+
should.deepEqual(broadcastFormat.signers, signers);
122+
should.exist(broadcastFormat.serializedSignatures);
123+
const serializedSignatures = broadcastFormat.serializedSignatures;
124+
should.equal(serializedSignatures?.length, 1);
125+
should.equal(serializedSignatures?.[0].publicKey, 'solPubKey1');
126+
should.equal(serializedSignatures?.[0].signature, Buffer.from('solSignature1').toString('base64'));
127+
});
128+
});
129+
130+
describe('fromBroadcastFormat', function () {
131+
it('should rebuild message from broadcastable format', async function () {
132+
const broadcastMessage: BroadcastableMessage = {
133+
type: MessageStandardType.SIMPLE,
134+
payload: 'Solana test message',
135+
serializedSignatures: [
136+
{
137+
publicKey: 'solPubKey1',
138+
signature: Buffer.from('solSignature1').toString('base64'),
139+
},
140+
],
141+
signers: ['solSigner1'],
142+
metadata: { network: 'testnet', encoding: 'utf8' },
143+
};
144+
145+
const builder = factory.getMessageBuilder(MessageStandardType.SIMPLE);
146+
const message = await builder.fromBroadcastFormat(broadcastMessage);
147+
148+
should.exist(message);
149+
should.equal(message.getType(), MessageStandardType.SIMPLE);
150+
should.equal(message.getPayload(), broadcastMessage.payload);
151+
should.deepEqual(message.getSigners(), broadcastMessage.signers);
152+
should.exist(message.getSignatures());
153+
should.equal(message.getSignatures().length, 1);
154+
});
155+
});
156+
});

modules/sdk-core/src/account-lib/baseCoin/iface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ export function deserializeSignature(serialized: SerializedSignature): Signature
5858
};
5959
}
6060

61-
export function deserializeSignatures(serialized: SerializedSignature[]): Signature[] {
62-
return serialized.map(deserializeSignature);
61+
export function deserializeSignatures(serialized?: SerializedSignature[]): Signature[] {
62+
return serialized?.map(deserializeSignature) || [];
6363
}
6464

6565
export type KeyPairOptions = Seed | PrivateKey | PublicKey;

modules/sdk-core/src/account-lib/baseCoin/messages/baseMessage.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -130,20 +130,15 @@ export abstract class BaseMessage implements IMessage {
130130
* @returns A broadcastable message
131131
*/
132132
async toBroadcastFormat(): Promise<BroadcastableMessage> {
133-
if (this.signatures.length === 0) {
134-
throw new Error('No signatures available for broadcast. Call setSignatures or addSignature first.');
133+
if (!this.payload) {
134+
throw new Error('Message payload must be set before converting to broadcast format');
135135
}
136-
if (this.signers.length === 0) {
137-
throw new Error('No signers available for broadcast. Call setSigners or addSigner first.');
138-
}
139-
140-
let signablePayload: string | undefined;
141-
if (this.signablePayload) {
142-
if (Buffer.isBuffer(this.signablePayload)) {
143-
signablePayload = this.signablePayload.toString('base64');
144-
} else {
145-
signablePayload = Buffer.from(String(this.signablePayload)).toString('base64');
146-
}
136+
const signablePayload = await this.getSignablePayload();
137+
let serializedSignablePayload: string;
138+
if (Buffer.isBuffer(signablePayload)) {
139+
serializedSignablePayload = signablePayload.toString('base64');
140+
} else {
141+
serializedSignablePayload = Buffer.from(String(signablePayload)).toString('base64');
147142
}
148143

149144
return {
@@ -154,7 +149,7 @@ export abstract class BaseMessage implements IMessage {
154149
metadata: {
155150
...(this.metadata ? JSON.parse(JSON.stringify(this.metadata)) : {}), // deep copy to avoid mutation
156151
},
157-
signablePayload,
152+
signablePayload: serializedSignablePayload,
158153
};
159154
}
160155

0 commit comments

Comments
 (0)