Skip to content

Commit 7bbb13a

Browse files
committed
feat(sdk-coin-canton): added message signing builders for canton
Ticket: CHALO-545
1 parent 1165435 commit 7bbb13a

19 files changed

Lines changed: 480 additions & 0 deletions

modules/account-lib/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,8 @@ const coinMessageBuilderFactoryMap = {
347347
tada: Ada.MessageBuilderFactory,
348348
sol: Sol.MessageBuilderFactory,
349349
tsol: Sol.MessageBuilderFactory,
350+
canton: Canton.MessageBuilderFactory,
351+
tcanton: Canton.MessageBuilderFactory,
350352
};
351353

352354
coins

modules/sdk-coin-canton/src/canton.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ export class Canton extends BaseCoin {
9393
return true;
9494
}
9595

96+
/** @inheritDoc */
97+
supportsMessageSigning(): boolean {
98+
return true;
99+
}
100+
96101
/** inherited doc */
97102
getDefaultMultisigType(): MultisigType {
98103
return multisigTypes.tss;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { MessageStandardType } from '@bitgo/sdk-core';
2+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
3+
// @ts-ignore – JS resource with hand-written .d.ts
4+
import { PreparedTransaction } from '../../resources/proto/preparedTransaction.js';
5+
import utils from './utils';
6+
7+
/**
8+
* Clear signing and payload-type detection for Canton message signing requests.
9+
*
10+
* The Canton Signing Driver receives raw `tx` bytes and a `txHash` from the
11+
* Canton Gateway's signTransaction() call. It does not know whether `tx` is a
12+
* Daml prepared transaction or a topology transaction. This module owns that
13+
* detection, keeping Canton-specific proto knowledge inside sdk-coin-canton.
14+
*
15+
* Usage in wallet-platform (buildUnsignedMsgWithIntent):
16+
* 1. Call detectCantonSigningPayloadType(tx) → MessageStandardType
17+
* 2. Use that type as messageStandardType in the msgrequest
18+
* 3. In getHsmPayload, key off the type to choose Format 1 vs Format 2
19+
*/
20+
21+
export interface DecodedCantonTransaction {
22+
/** 'CreateCommand' or 'ExerciseCommand' */
23+
kind: string;
24+
/** Fully-qualified Daml template identifier */
25+
templateId: {
26+
packageId: string;
27+
moduleName: string;
28+
entityName: string;
29+
};
30+
/** Decoded Daml arguments (create arguments or choice arguments) */
31+
argument: unknown;
32+
/** Choice name — present for ExerciseCommand only */
33+
choice?: string;
34+
/** Contract ID being exercised — present for ExerciseCommand only */
35+
contractId?: string;
36+
/** Parties acting on this command — present for ExerciseCommand only */
37+
actingParties?: string[];
38+
}
39+
40+
/**
41+
* Detect whether a Canton `tx` payload (base64) is a Daml prepared transaction
42+
* or a topology transaction, by attempting to parse as PreparedTransaction proto.
43+
*
44+
* - PreparedTransaction → has `transaction.nodes` populated → CANTON_SIGN_TRANSACTION
45+
* - Topology bytes → parsing fails or nodes empty → CANTON_SIGN_TOPOLOGY
46+
*
47+
* wallet-platform calls this on the raw `tx` bytes from the Canton Gateway to
48+
* determine which MessageStandardType to use and which HSM payload format to apply.
49+
*
50+
* @param txBase64 - base64-encoded `tx` bytes from Canton's signTransaction request
51+
* @returns The appropriate MessageStandardType for this payload
52+
*/
53+
export function detectCantonSigningPayloadType(txBase64: string): MessageStandardType {
54+
try {
55+
const bytes = Buffer.from(txBase64, 'base64');
56+
const decoded = PreparedTransaction.fromBinary(bytes);
57+
if (decoded.transaction && Array.isArray(decoded.transaction.nodes) && decoded.transaction.nodes.length > 0) {
58+
return MessageStandardType.CANTON_SIGN_TRANSACTION;
59+
}
60+
} catch {
61+
// bytes did not parse as a PreparedTransaction proto
62+
}
63+
return MessageStandardType.CANTON_SIGN_TOPOLOGY;
64+
}
65+
66+
/**
67+
* Decode a Canton prepared transaction into a human-readable structure
68+
* suitable for storage as a clearSigningPayload on the txRequest message.
69+
*
70+
* Only meaningful when detectCantonSigningPayloadType() returns CANTON_SIGN_TRANSACTION.
71+
* For topology transactions, there is no Daml command structure to decode.
72+
*
73+
* @param preparedTransactionBase64 - base64-encoded protobuf bytes from the Canton signTransaction request
74+
* @returns Decoded command info describing the Daml operation being signed
75+
*/
76+
export function decodePreparedTransaction(preparedTransactionBase64: string): DecodedCantonTransaction {
77+
const info = utils.extractCantonCommandInfo(preparedTransactionBase64);
78+
return {
79+
kind: info.kind,
80+
templateId: info.templateId,
81+
argument: info.argument,
82+
...(info.choice !== undefined && { choice: info.choice }),
83+
...(info.contractId !== undefined && { contractId: info.contractId }),
84+
...(info.actingParties !== undefined && { actingParties: info.actingParties }),
85+
};
86+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ export { WalletInitBuilder } from './walletInitBuilder';
2020
export { WalletInitTransaction } from './walletInitialization/walletInitTransaction';
2121

2222
export { Utils, Interface };
23+
export * from './messages';
24+
export * from './clearSigning';
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { BaseMessage, MessageOptions, MessageStandardType } from '@bitgo/sdk-core';
2+
3+
/**
4+
* Implementation of Canton sign-topology message.
5+
*
6+
* Used when the Canton Gateway requests signing of a topology transaction —
7+
* for example, when a party is being hosted on an external validator and
8+
* delegates signing to BitGo (party hosting / onboarding flow).
9+
*
10+
* The payload is the base64-encoded txHash. The signable payload is the
11+
* decoded raw bytes of that hash, which is what the TSS signer signs.
12+
*
13+
* The CANTON_SIGN_TOPOLOGY type is the discriminator wallet-platform uses to
14+
* apply the topology HSM payload format (Format 1):
15+
* [txnType (4B LE)] || itemCount (4B LE) || [len || topoTxBytes]... || signableHex
16+
*
17+
* This differs from CANTON_SIGN_TRANSACTION which uses the prepared-transaction
18+
* format (Format 2):
19+
* itemCount=2 (4B LE) || len (4B LE) || preparedTxBinary || signableHex
20+
*/
21+
export class CantonSignTopologyMessage extends BaseMessage {
22+
constructor(options: MessageOptions) {
23+
super({
24+
...options,
25+
type: MessageStandardType.CANTON_SIGN_TOPOLOGY,
26+
});
27+
}
28+
29+
async getSignablePayload(): Promise<string | Buffer> {
30+
if (!this.payload) {
31+
throw new Error('Message payload is missing');
32+
}
33+
// txHash arrives as a base64 string; decode to raw bytes for signing
34+
this.signablePayload = Buffer.from(this.payload, 'base64');
35+
return this.signablePayload;
36+
}
37+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
2+
import { BaseMessageBuilder, IMessage, MessageOptions, MessageStandardType } from '@bitgo/sdk-core';
3+
import { CantonSignTopologyMessage } from './cantonSignTopologyMessage';
4+
5+
/**
6+
* Builder for Canton sign-topology messages.
7+
*
8+
* The payload should be the base64-encoded txHash from Canton's signTransaction
9+
* call when it is requesting a topology transaction to be signed (e.g. party
10+
* hosting on an external validator).
11+
*
12+
* wallet-platform uses the CANTON_SIGN_TOPOLOGY type to apply HSM payload
13+
* Format 1 (topology framing) rather than Format 2 (prepared-transaction framing).
14+
*/
15+
export class CantonSignTopologyMessageBuilder extends BaseMessageBuilder {
16+
public constructor(_coinConfig: Readonly<CoinConfig>) {
17+
super(_coinConfig, MessageStandardType.CANTON_SIGN_TOPOLOGY);
18+
}
19+
20+
public async buildMessage(options: MessageOptions): Promise<IMessage> {
21+
return new CantonSignTopologyMessage(options);
22+
}
23+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './cantonSignTopologyMessage';
2+
export * from './cantonSignTopologyMessageBuilder';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { BaseMessage, MessageOptions, MessageStandardType } from '@bitgo/sdk-core';
2+
3+
/**
4+
* Implementation of Canton sign-transaction message.
5+
*
6+
* The payload is the base64-encoded txHash sent by the Canton network's
7+
* signTransaction RPC. The signable payload is the decoded raw bytes of
8+
* that hash — i.e. what the TSS signer actually signs.
9+
*/
10+
export class CantonSignTransactionMessage extends BaseMessage {
11+
constructor(options: MessageOptions) {
12+
super({
13+
...options,
14+
type: MessageStandardType.CANTON_SIGN_TRANSACTION,
15+
});
16+
}
17+
18+
async getSignablePayload(): Promise<string | Buffer> {
19+
if (!this.payload) {
20+
throw new Error('Message payload is missing');
21+
}
22+
// txHash arrives as a base64 string; decode to raw bytes for signing
23+
this.signablePayload = Buffer.from(this.payload, 'base64');
24+
return this.signablePayload;
25+
}
26+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
2+
import { BaseMessageBuilder, IMessage, MessageOptions, MessageStandardType } from '@bitgo/sdk-core';
3+
import { CantonSignTransactionMessage } from './cantonSignTransactionMessage';
4+
5+
/**
6+
* Builder for Canton sign-transaction messages.
7+
*
8+
* The payload should be the base64-encoded txHash from Canton's
9+
* signTransaction RPC call. The builder produces a CantonSignTransactionMessage
10+
* whose signable payload is the decoded raw bytes of that hash.
11+
*/
12+
export class CantonSignTransactionMessageBuilder extends BaseMessageBuilder {
13+
public constructor(_coinConfig: Readonly<CoinConfig>) {
14+
super(_coinConfig, MessageStandardType.CANTON_SIGN_TRANSACTION);
15+
}
16+
17+
public async buildMessage(options: MessageOptions): Promise<IMessage> {
18+
return new CantonSignTransactionMessage(options);
19+
}
20+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './cantonSignTransactionMessage';
2+
export * from './cantonSignTransactionMessageBuilder';

0 commit comments

Comments
 (0)