Skip to content

Commit 71fbabb

Browse files
authored
Merge pull request #6389 from BitGo/coin-4724-ada-message-buider
feat(sdk-coin-ada): CIP-8 Message Builder
2 parents 2ad9e03 + 50c53ec commit 71fbabb

File tree

27 files changed

+1084
-81
lines changed

27 files changed

+1084
-81
lines changed

.gitcommitscopes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
account-lib
2+
sdk-coin-ada
23
sdk-coin-rune
34
sdk-coin-sui
45
sdk-core

modules/abstract-eth/src/lib/messages/eip191/eip191MessageBuilder.ts

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

511
/**
612
* Builder for EIP-191 messages
@@ -48,14 +54,14 @@ export class Eip191MessageBuilder extends BaseMessageBuilder {
4854
* @returns The parsed message
4955
*/
5056
public async fromBroadcastFormat(broadcastMessage: BroadcastableMessage): Promise<IMessage> {
51-
const { type, payload, signatures, signers, metadata } = broadcastMessage;
57+
const { type, payload, serializedSignatures, signers, metadata } = broadcastMessage;
5258
if (type !== MessageStandardType.EIP191) {
5359
throw new Error(`Invalid message type, expected ${MessageStandardType.EIP191}`);
5460
}
5561
return new EIP191Message({
5662
coinConfig: this.coinConfig,
5763
payload,
58-
signatures,
64+
signatures: deserializeSignatures(serializedSignatures),
5965
signers,
6066
metadata: {
6167
...metadata,

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

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'should';
22
import sinon from 'sinon';
3-
import { MessageStandardType } from '@bitgo/sdk-core';
3+
import { MessageStandardType, serializeSignatures } from '@bitgo/sdk-core';
44
import { fixtures } from '../fixtures';
55
import { EIP191Message } from '../../../../src';
66

@@ -84,17 +84,33 @@ describe('EIP191 Message', () => {
8484
message.getSigners().should.containEql(fixtures.eip191.signer);
8585

8686
// Test adding new ones
87-
message.addSignature('new-signature');
87+
message.addSignature({
88+
publicKey: { pub: 'pub1' },
89+
signature: Buffer.from('new-signature'),
90+
});
8891
message.addSigner('new-signer');
8992

90-
message.getSignatures().should.containEql('new-signature');
93+
message.getSignatures().should.containEql({
94+
publicKey: { pub: 'pub1' },
95+
signature: Buffer.from('new-signature'),
96+
});
9197
message.getSigners().should.containEql('new-signer');
9298

9399
// Test replacing all
94-
message.setSignatures(['replaced-signature']);
100+
message.setSignatures([
101+
{
102+
publicKey: { pub: 'pub2' },
103+
signature: Buffer.from('replaced-signature'),
104+
},
105+
]);
95106
message.setSigners(['replaced-signer']);
96107

97-
message.getSignatures().should.deepEqual(['replaced-signature']);
108+
message.getSignatures().should.deepEqual([
109+
{
110+
publicKey: { pub: 'pub2' },
111+
signature: Buffer.from('replaced-signature'),
112+
},
113+
]);
98114
message.getSigners().should.deepEqual(['replaced-signer']);
99115
});
100116

@@ -121,12 +137,13 @@ describe('EIP191 Message', () => {
121137

122138
const broadcastFormat = await message.toBroadcastFormat();
123139

140+
const expectedSerializedSignatures = serializeSignatures([fixtures.eip191.signature]);
124141
broadcastFormat.type.should.equal(MessageStandardType.EIP191);
125142
broadcastFormat.payload.should.equal(fixtures.messages.validMessage);
126-
broadcastFormat.signatures.should.deepEqual([fixtures.eip191.signature]);
143+
broadcastFormat.serializedSignatures.should.deepEqual(expectedSerializedSignatures);
127144
broadcastFormat.signers.should.deepEqual([fixtures.eip191.signer]);
128145
broadcastFormat.metadata!.should.deepEqual(fixtures.eip191.metadata);
129-
broadcastFormat.signablePayload!.should.equal('test-signable-payload');
146+
broadcastFormat.signablePayload!.should.equal('dGVzdC1zaWduYWJsZS1wYXlsb2Fk');
130147
});
131148

132149
it('should throw error when broadcasting without signatures', async () => {
@@ -164,10 +181,11 @@ describe('EIP191 Message', () => {
164181

165182
const broadcastString = await message.toBroadcastString();
166183
const parsedBroadcast = JSON.parse(broadcastString);
184+
const expectedSerializedSignatures = serializeSignatures([fixtures.eip191.signature]);
167185

168186
parsedBroadcast.type.should.equal(MessageStandardType.EIP191);
169187
parsedBroadcast.payload.should.equal(fixtures.messages.validMessage);
170-
parsedBroadcast.signatures.should.deepEqual([fixtures.eip191.signature]);
188+
parsedBroadcast.serializedSignatures.should.deepEqual(expectedSerializedSignatures);
171189
parsedBroadcast.signers.should.deepEqual([fixtures.eip191.signer]);
172190
parsedBroadcast.metadata.should.deepEqual(fixtures.eip191.metadata);
173191
});

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'should';
22
import sinon from 'sinon';
3-
import { MessageStandardType } from '@bitgo/sdk-core';
3+
import { BroadcastableMessage, MessageStandardType, serializeSignatures } from '@bitgo/sdk-core';
44
import { fixtures } from '../fixtures';
55
import { EIP191Message, Eip191MessageBuilder } from '../../../../src';
66

@@ -79,10 +79,10 @@ describe('EIP191 Message Builder', () => {
7979
it('should reconstruct a message from broadcast format', async () => {
8080
const builder = new Eip191MessageBuilder(fixtures.coin);
8181

82-
const broadcastMessage = {
82+
const broadcastMessage: BroadcastableMessage = {
8383
type: MessageStandardType.EIP191,
8484
payload: fixtures.messages.validMessage,
85-
signatures: [fixtures.eip191.signature],
85+
serializedSignatures: serializeSignatures([fixtures.eip191.signature]),
8686
signers: [fixtures.eip191.signer],
8787
metadata: fixtures.eip191.metadata,
8888
};
@@ -101,10 +101,10 @@ describe('EIP191 Message Builder', () => {
101101
it('should throw an error for incorrect message type', async () => {
102102
const builder = new Eip191MessageBuilder(fixtures.coin);
103103

104-
const broadcastMessage = {
104+
const broadcastMessage: BroadcastableMessage = {
105105
type: MessageStandardType.UNKNOWN,
106106
payload: fixtures.messages.validMessage,
107-
signatures: [fixtures.eip191.signature],
107+
serializedSignatures: serializeSignatures([fixtures.eip191.signature]),
108108
signers: [fixtures.eip191.signer],
109109
metadata: {},
110110
};

modules/abstract-eth/test/unit/messages/fixtures.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ export const fixtures = {
1313
},
1414
eip191: {
1515
validSignablePayload: '0x19457468657265756d205369676e6564204d6573736167653a0d48656c6c6f2c20776f726c6421',
16-
signature:
17-
'0x5d99b6f7f6d1f73d1a26497f2b1c89b24c0993913f86e9a2d02cd69887d9c94f3c880358579d811b21dd1b7fd9bb01c1d81d10e69f0384e675c32b39643be8921b',
16+
signature: {
17+
publicKey: { pub: '0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf' },
18+
signature: Buffer.from(
19+
'5d99b6f7f6d1f73d1a26497f2b1c89b24c0993913f86e9a2d02cd69887d9c94f3c880358579d811b21dd1b7fd9bb01c1d81d10e69f0384e675c32b39643be8921b',
20+
'hex'
21+
),
22+
},
1823
signer: '0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf',
1924
metadata: {
2025
encoding: 'utf8',

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export { StakingWithdrawBuilder } from './stakingWithdrawBuilder';
1212
export { StakingPledgeBuilder } from './stakingPledgeBuilder';
1313
export { VoteDelegationBuilder } from './voteDelegationBuilder';
1414
export { Utils };
15+
export * from './messages';
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { BaseMessage, MessageOptions, MessageStandardType, Signature } from '@bitgo/sdk-core';
2+
import * as CardanoSL from '@emurgo/cardano-serialization-lib-nodejs';
3+
import { constructCSLCoseObjects, coseObjectsOutputToBuffer, createCSLSigStructure } from './utils';
4+
5+
/**
6+
* Implementation of Message for CIP8 standard
7+
*/
8+
export class Cip8Message extends BaseMessage {
9+
constructor(options: MessageOptions) {
10+
super({
11+
...options,
12+
type: MessageStandardType.CIP8,
13+
});
14+
}
15+
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+
44+
/**
45+
* Returns the hash of the CIP-8 prefixed message
46+
*/
47+
async getSignablePayload(): Promise<string | Buffer> {
48+
if (!this.signablePayload) {
49+
this.signablePayload = this.buildSignablePayload();
50+
}
51+
return this.signablePayload;
52+
}
53+
54+
/**
55+
* Builds the signable payload for a CIP8 message
56+
* @returns The signable payload as a Buffer
57+
*/
58+
buildSignablePayload(): string | Buffer {
59+
const { addressCborBytes } = this.validateAndGetCommonSetup();
60+
const { sigStructureCborBytes } = createCSLSigStructure(addressCborBytes, this.payload);
61+
return Buffer.from(sigStructureCborBytes);
62+
}
63+
64+
getBroadcastableSignatures(): Signature[] {
65+
if (!this.signatures.length) {
66+
return [];
67+
}
68+
69+
const signature = this.signatures[0].signature;
70+
const publicKeyHex = this.signatures[0].publicKey.pub;
71+
72+
const { addressCborBytes } = this.validateAndGetCommonSetup();
73+
const { protectedHeaderCborBytes, payloadBytes } = createCSLSigStructure(addressCborBytes, this.payload);
74+
75+
const coseObjectsOutput = constructCSLCoseObjects(
76+
protectedHeaderCborBytes,
77+
payloadBytes,
78+
signature,
79+
CardanoSL.PublicKey.from_bytes(Buffer.from(publicKeyHex, 'hex'))
80+
);
81+
const coseObjectsBuffer = coseObjectsOutputToBuffer(coseObjectsOutput);
82+
return [
83+
{
84+
signature: coseObjectsBuffer,
85+
publicKey: {
86+
pub: publicKeyHex,
87+
},
88+
},
89+
];
90+
}
91+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Cip8Message } from './cip8Message';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import {
4+
BaseMessageBuilder,
5+
BroadcastableMessage,
6+
deserializeSignatures,
7+
IMessage,
8+
MessageStandardType,
9+
} from '@bitgo/sdk-core';
10+
11+
/**
12+
* Builder for CIP-8 messages
13+
*/
14+
export class Cip8MessageBuilder extends BaseMessageBuilder {
15+
/**
16+
* Base constructor.
17+
* @param _coinConfig BaseCoin from statics library
18+
*/
19+
public constructor(_coinConfig: Readonly<CoinConfig>) {
20+
super(_coinConfig, MessageStandardType.CIP8);
21+
}
22+
23+
/**
24+
* Build a signable message using the CIP-8 standard
25+
* with previously set input and metadata
26+
* @returns A signable message
27+
*/
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+
});
71+
}
72+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './cip8Message';
2+
export * from './cip8MessageBuilder';

0 commit comments

Comments
 (0)