Skip to content

Commit 49df87a

Browse files
authored
Merge pull request #7207 from BitGo/COIN-5918
feat(sdk-coin-canton): added pre-approval builder
2 parents f702bcc + 8cbdf3e commit 49df87a

File tree

13 files changed

+727
-38
lines changed

13 files changed

+727
-38
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { IPreparedTransaction } from '../../src/lib/iface';
2+
3+
declare module '../../resources/hash/hash.js' {
4+
export function computePreparedTransaction(preparedTransaction: IPreparedTransaction): Promise<Uint8Array>;
5+
}
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
// This is the static helper file from canton SDK, replicating it here since we won't be using the canton wallet SDK
2+
// TODO: https://bitgoinc.atlassian.net/browse/COIN-6016
3+
const PREPARED_TRANSACTION_HASH_PURPOSE = Uint8Array.from([0x00, 0x00, 0x00, 0x30]);
4+
const NODE_ENCODING_VERSION = Uint8Array.from([0x01]);
5+
const HASHING_SCHEME_VERSION = Uint8Array.from([2]);
6+
async function sha256(message) {
7+
const msg = typeof message === 'string' ? new TextEncoder().encode(message) : message;
8+
return crypto.subtle.digest('SHA-256', new Uint8Array(msg)).then((hash) => new Uint8Array(hash));
9+
}
10+
async function mkByteArray(...args) {
11+
const normalizedArgs = args.map((arg) => {
12+
if (typeof arg === 'number') {
13+
return new Uint8Array([arg]);
14+
} else {
15+
return arg;
16+
}
17+
});
18+
let totalLength = 0;
19+
normalizedArgs.forEach((arg) => {
20+
totalLength += arg.length;
21+
});
22+
const mergedArray = new Uint8Array(totalLength);
23+
let offset = 0;
24+
normalizedArgs.forEach((arg) => {
25+
mergedArray.set(arg, offset);
26+
offset += arg.length;
27+
});
28+
return mergedArray;
29+
}
30+
async function encodeBool(value) {
31+
return new Uint8Array([value ? 1 : 0]);
32+
}
33+
async function encodeInt32(value) {
34+
const buffer = new ArrayBuffer(4);
35+
const view = new DataView(buffer);
36+
view.setInt32(0, value, false); // true for little-endian
37+
return new Uint8Array(buffer);
38+
}
39+
async function encodeInt64(value) {
40+
// eslint-disable-next-line no-undef
41+
const num = typeof value === 'bigint' ? value : BigInt(value || 0);
42+
const buffer = new ArrayBuffer(8);
43+
const view = new DataView(buffer);
44+
view.setBigInt64(0, num, false); // true for little-endian
45+
return new Uint8Array(buffer);
46+
}
47+
export async function encodeString(value = '') {
48+
const utf8Bytes = new TextEncoder().encode(value);
49+
return encodeBytes(utf8Bytes);
50+
}
51+
async function encodeBytes(value) {
52+
const length = await encodeInt32(value.length);
53+
return mkByteArray(length, value);
54+
}
55+
async function encodeHash(value) {
56+
return value;
57+
}
58+
function encodeHexString(value = '') {
59+
// Convert hex string to Uint8Array
60+
const bytes = new Uint8Array(value.length / 2);
61+
for (let i = 0; i < value.length; i += 2) {
62+
bytes[i / 2] = parseInt(value.slice(i, i + 2), 16);
63+
}
64+
return encodeBytes(bytes);
65+
}
66+
// Maybe suspicious?
67+
async function encodeOptional(value, encodeFn) {
68+
if (value === undefined || value === null) {
69+
return new Uint8Array([0]); // Return empty array for undefined fields
70+
} else {
71+
return mkByteArray(1, await encodeFn(value));
72+
}
73+
}
74+
// Maybe suspicious?
75+
async function encodeProtoOptional(parentValue, fieldName, value, encodeFn) {
76+
if (parentValue && parentValue[fieldName] !== undefined) {
77+
return mkByteArray(1, await encodeFn(value));
78+
} else {
79+
return new Uint8Array([0]); // Return empty array for undefined fields
80+
}
81+
}
82+
async function encodeRepeated(values = [], encodeFn) {
83+
const length = await encodeInt32(values.length);
84+
const encodedValues = await Promise.all(values.map(encodeFn));
85+
return mkByteArray(length, ...encodedValues);
86+
}
87+
function findSeed(nodeId, nodeSeeds) {
88+
const seed = nodeSeeds.find((seed) => seed.nodeId.toString() === nodeId)?.seed;
89+
return seed;
90+
}
91+
async function encodeIdentifier(identifier) {
92+
return mkByteArray(
93+
await encodeString(identifier.packageId),
94+
await encodeRepeated(identifier.moduleName.split('.'), encodeString),
95+
await encodeRepeated(identifier.entityName.split('.'), encodeString)
96+
);
97+
}
98+
async function encodeMetadata(metadata) {
99+
return mkByteArray(
100+
Uint8Array.from([0x01]),
101+
await encodeRepeated(metadata.submitterInfo?.actAs, encodeString),
102+
await encodeString(metadata.submitterInfo?.commandId),
103+
await encodeString(metadata.transactionUuid),
104+
await encodeInt32(metadata.mediatorGroup),
105+
await encodeString(metadata.synchronizerId),
106+
await encodeProtoOptional(metadata, 'minLedgerEffectiveTime', metadata.minLedgerEffectiveTime, encodeInt64),
107+
await encodeProtoOptional(metadata, 'maxLedgerEffectiveTime', metadata.maxLedgerEffectiveTime, encodeInt64),
108+
await encodeInt64(metadata.preparationTime),
109+
await encodeRepeated(metadata.inputContracts, encodeInputContract)
110+
);
111+
}
112+
async function encodeCreateNode(create, nodeId, nodeSeeds) {
113+
return create
114+
? mkByteArray(
115+
NODE_ENCODING_VERSION,
116+
await encodeString(create.lfVersion),
117+
0 /** Create node tag */,
118+
await encodeOptional(findSeed(nodeId, nodeSeeds), encodeHash),
119+
await encodeHexString(create.contractId),
120+
await encodeString(create.packageName),
121+
await encodeIdentifier(create.templateId),
122+
await encodeValue(create.argument),
123+
await encodeRepeated(create.signatories, encodeString),
124+
await encodeRepeated(create.stakeholders, encodeString)
125+
)
126+
: mkByteArray();
127+
}
128+
async function encodeExerciseNode(exercise, nodeId, nodesDict, nodeSeeds) {
129+
return mkByteArray(
130+
NODE_ENCODING_VERSION,
131+
await encodeString(exercise.lfVersion),
132+
1 /** Exercise node tag */,
133+
await encodeHash(findSeed(nodeId, nodeSeeds)),
134+
await encodeHexString(exercise.contractId),
135+
await encodeString(exercise.packageName),
136+
await encodeIdentifier(exercise.templateId),
137+
await encodeRepeated(exercise.signatories, encodeString),
138+
await encodeRepeated(exercise.stakeholders, encodeString),
139+
await encodeRepeated(exercise.actingParties, encodeString),
140+
await encodeProtoOptional(exercise, 'interfaceId', exercise.interfaceId, encodeIdentifier),
141+
await encodeString(exercise.choiceId),
142+
await encodeValue(exercise.chosenValue),
143+
await encodeBool(exercise.consuming),
144+
await encodeProtoOptional(exercise, 'exerciseResult', exercise.exerciseResult, encodeValue),
145+
await encodeRepeated(exercise.choiceObservers, encodeString),
146+
await encodeRepeated(exercise.children, encodeNodeId(nodesDict, nodeSeeds))
147+
);
148+
}
149+
async function encodeFetchNode(fetch) {
150+
return mkByteArray(
151+
NODE_ENCODING_VERSION,
152+
await encodeString(fetch.lfVersion),
153+
2 /** Fetch node tag */,
154+
await encodeHexString(fetch.contractId),
155+
await encodeString(fetch.packageName),
156+
await encodeIdentifier(fetch.templateId),
157+
await encodeRepeated(fetch.signatories, encodeString),
158+
await encodeRepeated(fetch.stakeholders, encodeString),
159+
await encodeProtoOptional(fetch, 'interfaceId', fetch.interfaceId, encodeIdentifier),
160+
await encodeRepeated(fetch.actingParties, encodeString)
161+
);
162+
}
163+
async function encodeRollbackNode(rollback, nodesDict, nodeSeeds) {
164+
return mkByteArray(
165+
NODE_ENCODING_VERSION,
166+
3 /** Rollback node tag */,
167+
await encodeRepeated(rollback.children, encodeNodeId(nodesDict, nodeSeeds))
168+
);
169+
}
170+
async function encodeInputContract(contract) {
171+
if (contract.contract.oneofKind === 'v1')
172+
return mkByteArray(
173+
await encodeInt64(contract.createdAt),
174+
await sha256(await encodeCreateNode(contract.contract.v1, 'unused_node_id', []))
175+
);
176+
else throw new Error('Unsupported contract version');
177+
}
178+
async function encodeValue(value) {
179+
if (value.sum.oneofKind === 'unit') {
180+
return Uint8Array.from([0]); // Unit value
181+
} else if (value.sum.oneofKind === 'bool') {
182+
return mkByteArray(Uint8Array.from([0x01]), await encodeBool(value.sum.bool));
183+
} else if (value.sum.oneofKind === 'int64') {
184+
return mkByteArray(Uint8Array.from([0x02]), await encodeInt64(parseInt(value.sum.int64, 10)));
185+
} else if (value.sum.oneofKind === 'numeric') {
186+
return mkByteArray(Uint8Array.from([0x03]), await encodeString(value.sum.numeric));
187+
} else if (value.sum.oneofKind === 'timestamp') {
188+
// eslint-disable-next-line no-undef
189+
return mkByteArray(Uint8Array.from([0x04]), await encodeInt64(BigInt(value.sum.timestamp)));
190+
} else if (value.sum.oneofKind === 'date') {
191+
return mkByteArray(Uint8Array.from([0x05]), await encodeInt32(value.sum.date));
192+
} else if (value.sum.oneofKind === 'party') {
193+
return mkByteArray(Uint8Array.from([0x06]), await encodeString(value.sum.party));
194+
} else if (value.sum.oneofKind === 'text') {
195+
return mkByteArray(Uint8Array.from([0x07]), await encodeString(value.sum.text));
196+
} else if (value.sum.oneofKind === 'contractId') {
197+
return mkByteArray(Uint8Array.from([0x08]), await encodeHexString(value.sum.contractId));
198+
} else if (value.sum.oneofKind === 'optional') {
199+
return mkByteArray(
200+
Uint8Array.from([0x09]),
201+
await encodeProtoOptional(value.sum.optional, 'value', value.sum.optional.value, encodeValue)
202+
);
203+
} else if (value.sum.oneofKind === 'list') {
204+
return mkByteArray(Uint8Array.from([0x0a]), await encodeRepeated(value.sum.list.elements, encodeValue));
205+
} else if (value.sum.oneofKind === 'textMap') {
206+
return mkByteArray(Uint8Array.from([0x0b]), await encodeRepeated(value.sum.textMap?.entries, encodeTextMapEntry));
207+
} else if (value.sum.oneofKind === 'record') {
208+
return mkByteArray(
209+
Uint8Array.from([0x0c]),
210+
await encodeProtoOptional(value.sum.record, 'recordId', value.sum.record.recordId, encodeIdentifier),
211+
await encodeRepeated(value.sum.record.fields, encodeRecordField)
212+
);
213+
} else if (value.sum.oneofKind === 'variant') {
214+
return mkByteArray(
215+
Uint8Array.from([0x0d]),
216+
await encodeProtoOptional(value.sum.variant, 'variantId', value.sum.variant.variantId, encodeIdentifier),
217+
await encodeString(value.sum.variant.constructor),
218+
await encodeValue(value.sum.variant.value)
219+
);
220+
} else if (value.sum.oneofKind === 'enum') {
221+
return mkByteArray(
222+
Uint8Array.from([0x0e]),
223+
await encodeProtoOptional(value.sum.enum, 'enumId', value.sum.enum.enumId, encodeIdentifier),
224+
await encodeString(value.sum.enum.constructor)
225+
);
226+
} else if (value.sum.oneofKind === 'genMap') {
227+
return mkByteArray(Uint8Array.from([0x0f]), await encodeRepeated(value.sum.genMap?.entries, encodeGenMapEntry));
228+
}
229+
throw new Error('Unsupported value type: ' + JSON.stringify(value));
230+
}
231+
async function encodeTextMapEntry(entry) {
232+
return mkByteArray(await encodeString(entry.key), await encodeValue(entry.value));
233+
}
234+
async function encodeRecordField(field) {
235+
return mkByteArray(await encodeOptional(field.label, encodeString), await encodeValue(field.value));
236+
}
237+
async function encodeGenMapEntry(entry) {
238+
return mkByteArray(await encodeValue(entry.key), await encodeValue(entry.value));
239+
}
240+
function encodeNodeId(nodesDict, nodeSeeds) {
241+
return async (nodeId) => {
242+
const node = nodesDict[nodeId];
243+
if (!node) {
244+
throw new Error(`Node with ID ${nodeId} not found in transaction`);
245+
}
246+
const encodedNode = await encodeNode(node, nodesDict, nodeSeeds);
247+
return sha256(encodedNode);
248+
};
249+
}
250+
async function encodeNode(node, nodesDict, nodeSeeds) {
251+
if (node.versionedNode.oneofKind === 'v1') {
252+
if (node.versionedNode.v1.nodeType.oneofKind === 'create') {
253+
return encodeCreateNode(node.versionedNode.v1.nodeType.create, node.nodeId, nodeSeeds);
254+
} else if (node.versionedNode.v1.nodeType.oneofKind === 'exercise') {
255+
return encodeExerciseNode(node.versionedNode.v1.nodeType.exercise, node.nodeId, nodesDict, nodeSeeds);
256+
} else if (node.versionedNode.v1.nodeType.oneofKind === 'fetch') {
257+
return encodeFetchNode(node.versionedNode.v1.nodeType.fetch);
258+
} else if (node.versionedNode.v1.nodeType.oneofKind === 'rollback') {
259+
return encodeRollbackNode(node.versionedNode.v1.nodeType.rollback, nodesDict, nodeSeeds);
260+
}
261+
throw new Error('Unsupported node type');
262+
} else {
263+
throw new Error(`Unsupported node version`);
264+
}
265+
}
266+
function createNodesDict(preparedTransaction) {
267+
const nodesDict = {};
268+
const nodes = preparedTransaction.transaction?.nodes || [];
269+
for (const node of nodes) {
270+
nodesDict[node.nodeId] = node;
271+
}
272+
return nodesDict;
273+
}
274+
async function encodeTransaction(transaction, nodesDict, nodeSeeds) {
275+
return mkByteArray(
276+
await encodeString(transaction.version),
277+
await encodeRepeated(transaction.roots, encodeNodeId(nodesDict, nodeSeeds))
278+
);
279+
}
280+
async function hashTransaction(transaction, nodesDict) {
281+
const encodedTransaction = await encodeTransaction(transaction, nodesDict, transaction.nodeSeeds);
282+
const hash = await sha256(await mkByteArray(PREPARED_TRANSACTION_HASH_PURPOSE, encodedTransaction));
283+
return hash;
284+
}
285+
async function hashMetadata(metadata) {
286+
const hash = await sha256(await mkByteArray(PREPARED_TRANSACTION_HASH_PURPOSE, await encodeMetadata(metadata)));
287+
return hash;
288+
}
289+
async function encodePreparedTransaction(preparedTransaction) {
290+
const nodesDict = createNodesDict(preparedTransaction);
291+
const transactionHash = await hashTransaction(preparedTransaction.transaction, nodesDict);
292+
const metadataHash = await hashMetadata(preparedTransaction.metadata);
293+
return mkByteArray(PREPARED_TRANSACTION_HASH_PURPOSE, HASHING_SCHEME_VERSION, transactionHash, metadataHash);
294+
}
295+
export async function computePreparedTransaction(preparedTransaction) {
296+
return sha256(await encodePreparedTransaction(preparedTransaction));
297+
}

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface CantonPrepareCommandResponse {
2727
preparedTransaction?: string;
2828
preparedTransactionHash: string;
2929
hashingSchemeVersion: string;
30-
hashingDetails?: string;
30+
hashingDetails?: string | null;
3131
}
3232

3333
export interface PreparedParty {
@@ -57,3 +57,27 @@ export interface WalletInitRequest {
5757
confirmationThreshold: number;
5858
observingParticipantUids: string[];
5959
}
60+
61+
interface PreApprovalCreateCommand {
62+
templateId: string;
63+
createArguments: {
64+
receiver: string;
65+
provider: string;
66+
expectedDso: string;
67+
};
68+
}
69+
70+
export interface OneStepEnablementRequest {
71+
commandId: string;
72+
commands: [
73+
{
74+
CreateCommand: PreApprovalCreateCommand;
75+
}
76+
];
77+
disclosedContracts: [];
78+
synchronizerId: string;
79+
verboseHashing: boolean;
80+
actAs: string[];
81+
readAs: string[];
82+
packageIdSelectionPreference: string[];
83+
}

0 commit comments

Comments
 (0)