Skip to content

Commit 36baec1

Browse files
authored
Merge pull request #6396 from BitGo/coin-4737-sol-message-builder
feat(sdk-core): add simple message builder for sol
2 parents 71fbabb + ce78b54 commit 36baec1

File tree

18 files changed

+184
-199
lines changed

18 files changed

+184
-199
lines changed
Lines changed: 6 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import { EIP191Message } from './eip191Message';
22
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3-
import {
4-
BaseMessageBuilder,
5-
BroadcastableMessage,
6-
deserializeSignatures,
7-
IMessage,
8-
MessageStandardType,
9-
} from '@bitgo/sdk-core';
3+
import { BaseMessageBuilder, IMessage, MessageOptions, MessageStandardType } from '@bitgo/sdk-core';
104

115
/**
126
* Builder for EIP-191 messages
@@ -21,52 +15,11 @@ export class Eip191MessageBuilder extends BaseMessageBuilder {
2115
}
2216

2317
/**
24-
* Build a signable message using the EIP-191 standard
25-
* with previously set input and metadata
26-
* @returns A signable message
18+
* Builds an EIP-191 message instance with the provided options
19+
* @param options Options to create the message
20+
* @returns A Promise that resolves to an EIP191Message instance
2721
*/
28-
public async build(): Promise<IMessage> {
29-
try {
30-
if (!this.payload) {
31-
throw new Error('Message payload must be set before building the message');
32-
}
33-
return new EIP191Message({
34-
coinConfig: this.coinConfig,
35-
payload: this.payload,
36-
signatures: this.signatures,
37-
signers: this.signers,
38-
metadata: {
39-
...this.metadata,
40-
encoding: 'utf8',
41-
},
42-
});
43-
} catch (err) {
44-
if (err instanceof Error) {
45-
throw err;
46-
}
47-
throw new Error('Failed to build EIP-191 message');
48-
}
49-
}
50-
51-
/**
52-
* Parse a broadcastable message back into a message
53-
* @param broadcastMessage The broadcastable message to parse
54-
* @returns The parsed message
55-
*/
56-
public async fromBroadcastFormat(broadcastMessage: BroadcastableMessage): Promise<IMessage> {
57-
const { type, payload, serializedSignatures, signers, metadata } = broadcastMessage;
58-
if (type !== MessageStandardType.EIP191) {
59-
throw new Error(`Invalid message type, expected ${MessageStandardType.EIP191}`);
60-
}
61-
return new EIP191Message({
62-
coinConfig: this.coinConfig,
63-
payload,
64-
signatures: deserializeSignatures(serializedSignatures),
65-
signers,
66-
metadata: {
67-
...metadata,
68-
encoding: 'utf8',
69-
},
70-
});
22+
async buildMessage(options: MessageOptions): Promise<IMessage> {
23+
return new EIP191Message(options);
7124
}
7225
}

modules/account-lib/test/unit/fixtures.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,8 @@ export class MockMessageBuilder extends BaseMessageBuilder {
2424
super(coinConfig, type);
2525
}
2626

27-
async build(): Promise<IMessage> {
28-
return new MockMessage({
29-
coinConfig: this.coinConfig,
30-
payload: this.payload,
31-
type: this.type,
32-
signatures: this.signatures,
33-
signers: this.signers,
34-
metadata: {
35-
...this.metadata,
36-
},
37-
});
27+
async buildMessage(options: MessageOptions): Promise<IMessage> {
28+
return new MockMessage(options);
3829
}
3930

4031
async fromBroadcastFormat(broadcastMessage: any): Promise<IMessage> {

modules/sdk-coin-ada/src/lib/messages/cip8/cip8Message.ts

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,54 +13,28 @@ export class Cip8Message extends BaseMessage {
1313
});
1414
}
1515

16-
/**
17-
* Validates required fields and returns common setup objects
18-
* @private
19-
*/
20-
private validateAndGetCommonSetup() {
21-
if (!this.payload) {
22-
throw new Error('Payload is required to build a CIP8 message');
23-
}
24-
if (!this.signers || this.signers.length === 0) {
25-
throw new Error('A signer address is required to build a CIP8 message');
26-
}
27-
28-
let cslAddress: CardanoSL.Address;
29-
try {
30-
cslAddress = CardanoSL.Address.from_bech32(this.signers[0]);
31-
} catch (error) {
32-
// Convert string errors to proper Error objects
33-
if (typeof error === 'string') {
34-
throw new Error(`Invalid signer address: ${error}`);
35-
}
36-
throw error;
37-
}
38-
39-
const addressCborBytes = cslAddress.to_bytes();
40-
41-
return { addressCborBytes };
42-
}
43-
4416
/**
4517
* Returns the hash of the CIP-8 prefixed message
4618
*/
4719
async getSignablePayload(): Promise<string | Buffer> {
4820
if (!this.signablePayload) {
49-
this.signablePayload = this.buildSignablePayload();
21+
const { addressCborBytes } = this.validateAndGetCommonSetup();
22+
const { sigStructureCborBytes } = createCSLSigStructure(addressCborBytes, this.payload);
23+
this.signablePayload = Buffer.from(sigStructureCborBytes);
5024
}
5125
return this.signablePayload;
5226
}
5327

54-
/**
55-
* Builds the signable payload for a CIP8 message
56-
* @returns The signable payload as a Buffer
28+
/*
29+
* Returns broadcastable signatures in COSE format according to CIP8 standard
30+
*
31+
* This method transforms the internal signatures into a format suitable for broadcasting
32+
* by constructing COSE (CBOR Object Signing and Encryption) objects that comply with
33+
* the CIP8 message signing specification.
34+
*
35+
* @returns Array of signatures with COSE-formatted signature data and public keys
36+
* @throws Error if required setup validation fails
5737
*/
58-
buildSignablePayload(): string | Buffer {
59-
const { addressCborBytes } = this.validateAndGetCommonSetup();
60-
const { sigStructureCborBytes } = createCSLSigStructure(addressCborBytes, this.payload);
61-
return Buffer.from(sigStructureCborBytes);
62-
}
63-
6438
getBroadcastableSignatures(): Signature[] {
6539
if (!this.signatures.length) {
6640
return [];
@@ -88,4 +62,31 @@ export class Cip8Message extends BaseMessage {
8862
},
8963
];
9064
}
65+
66+
/**
67+
* Validates required fields and returns common setup objects
68+
* @private
69+
*/
70+
private validateAndGetCommonSetup() {
71+
if (!this.payload) {
72+
throw new Error('Payload is required to build a CIP8 message');
73+
}
74+
if (!this.signers || this.signers.length === 0) {
75+
throw new Error('A signer address is required to build a CIP8 message');
76+
}
77+
78+
let cslAddress: CardanoSL.Address;
79+
try {
80+
cslAddress = CardanoSL.Address.from_bech32(this.signers[0]);
81+
} catch (error) {
82+
// Convert string errors to proper Error objects
83+
if (typeof error === 'string') {
84+
throw new Error(`Invalid signer address: ${error}`);
85+
}
86+
throw error;
87+
}
88+
89+
const addressCborBytes = cslAddress.to_bytes();
90+
return { addressCborBytes };
91+
}
9192
}
Lines changed: 6 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import { Cip8Message } from './cip8Message';
22
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3-
import {
4-
BaseMessageBuilder,
5-
BroadcastableMessage,
6-
deserializeSignatures,
7-
IMessage,
8-
MessageStandardType,
9-
} from '@bitgo/sdk-core';
3+
import { BaseMessageBuilder, IMessage, MessageOptions, MessageStandardType } from '@bitgo/sdk-core';
104

115
/**
126
* Builder for CIP-8 messages
@@ -21,52 +15,11 @@ export class Cip8MessageBuilder extends BaseMessageBuilder {
2115
}
2216

2317
/**
24-
* Build a signable message using the CIP-8 standard
25-
* with previously set input and metadata
26-
* @returns A signable message
18+
* Builds a CIP-8 message instance with the provided options
19+
* @param options Options to create the message
20+
* @returns A Promise that resolves to a Cip8Message instance
2721
*/
28-
public async build(): Promise<IMessage> {
29-
try {
30-
if (!this.payload) {
31-
throw new Error('Message payload must be set before building the message');
32-
}
33-
return new Cip8Message({
34-
coinConfig: this.coinConfig,
35-
payload: this.payload,
36-
signatures: this.signatures,
37-
signers: this.signers,
38-
metadata: {
39-
...this.metadata,
40-
encoding: 'utf8',
41-
},
42-
});
43-
} catch (err) {
44-
if (err instanceof Error) {
45-
throw err;
46-
}
47-
throw new Error('Failed to build CIP-8 message');
48-
}
49-
}
50-
51-
/**
52-
* Parse a broadcastable message back into a message
53-
* @param broadcastMessage The broadcastable message to parse
54-
* @returns The parsed message
55-
*/
56-
public async fromBroadcastFormat(broadcastMessage: BroadcastableMessage): Promise<IMessage> {
57-
const { type, payload, serializedSignatures, signers, metadata } = broadcastMessage;
58-
if (type !== MessageStandardType.CIP8) {
59-
throw new Error(`Invalid message type, expected ${MessageStandardType.CIP8}`);
60-
}
61-
return new Cip8Message({
62-
coinConfig: this.coinConfig,
63-
payload,
64-
signatures: deserializeSignatures(serializedSignatures),
65-
signers,
66-
metadata: {
67-
...metadata,
68-
encoding: 'utf8',
69-
},
70-
});
22+
async buildMessage(options: MessageOptions): Promise<IMessage> {
23+
return new Cip8Message(options);
7124
}
7225
}

modules/sdk-coin-ada/test/unit/messages/cip8/cip8Message.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ describe('Cip8Message', function () {
105105

106106
it('should cache signable payload', async function () {
107107
const message = new Cip8Message(createDefaultMessageOptions());
108-
const buildSignablePayloadSpy = sandbox.spy(message as any, 'buildSignablePayload');
109108

110109
const payload1 = await message.getSignablePayload();
111110
const payload2 = await message.getSignablePayload();
@@ -115,27 +114,6 @@ describe('Cip8Message', function () {
115114

116115
// Payloads should be the same
117116
should.equal(payload1, payload2);
118-
119-
// buildSignablePayload should be called only once
120-
buildSignablePayloadSpy.calledOnce.should.be.true();
121-
});
122-
});
123-
124-
describe('buildSignablePayload', function () {
125-
it('should create a buffer from signature structure', function () {
126-
const message = new Cip8Message(createDefaultMessageOptions());
127-
const payload = (message as any).buildSignablePayload();
128-
129-
should.exist(payload);
130-
should.ok(Buffer.isBuffer(payload));
131-
});
132-
133-
it('should throw when validation fails', async function () {
134-
const options = createDefaultMessageOptions();
135-
options.payload = undefined as any;
136-
const message = new Cip8Message(options);
137-
138-
await should(message.getSignablePayload()).be.rejectedWith(`Payload is required to build a CIP8 message`);
139117
});
140118
});
141119

modules/sdk-coin-sol/src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ export { TransferBuilder } from './transferBuilder';
1818
export { TransferBuilderV2 } from './transferBuilderV2';
1919
export { WalletInitializationBuilder } from './walletInitializationBuilder';
2020
export { Interface, Utils };
21+
export { MessageBuilderFactory } from './messages';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './messageBuilderFactory';
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
2+
import { BaseMessageBuilderFactory, IMessageBuilder, MessageStandardType, SimpleMessageBuilder } from '@bitgo/sdk-core';
3+
4+
export class MessageBuilderFactory extends BaseMessageBuilderFactory {
5+
constructor(coinConfig: Readonly<CoinConfig>) {
6+
super(coinConfig);
7+
}
8+
9+
public getMessageBuilder(type: MessageStandardType): IMessageBuilder {
10+
switch (type) {
11+
case MessageStandardType.SIMPLE:
12+
return new SimpleMessageBuilder(this.coinConfig);
13+
default:
14+
throw new Error(`Invalid message standard ${type}`);
15+
}
16+
}
17+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export abstract class BaseMessage implements IMessage {
2525
*/
2626
protected constructor(options: MessageOptions) {
2727
this.coinConfig = options.coinConfig;
28+
this.payload = options.payload;
2829
this.type = options.type || MessageStandardType.UNKNOWN;
29-
this.payload = options.payload || '';
3030
this.signablePayload = options.signablePayload;
3131
this.metadata = options.metadata || {};
3232

@@ -117,6 +117,9 @@ export abstract class BaseMessage implements IMessage {
117117
*/
118118
abstract getSignablePayload(): Promise<string | Buffer>;
119119

120+
/**
121+
* Gets the signatures in a format suitable for broadcasting
122+
*/
120123
getBroadcastableSignatures(): Signature[] {
121124
return this.signatures;
122125
}

0 commit comments

Comments
 (0)