Skip to content

Commit bed28db

Browse files
committed
feat(sdk-coin-canton): added pre-approval builder
Ticket: COIN-5918
1 parent 0740117 commit bed28db

File tree

9 files changed

+406
-22
lines changed

9 files changed

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

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,10 @@ export interface WalletInitRequest {
5757
confirmationThreshold: number;
5858
observingParticipantUids: string[];
5959
}
60+
61+
export interface PrepareSubmissionResponse {
62+
preparedTransaction: string;
63+
preparedTransactionHash: string;
64+
hashingSchemeVersion: string;
65+
hashingDetails?: string;
66+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { TransactionType } from '@bitgo/sdk-core';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { TransactionBuilder } from './transactionBuilder';
4+
5+
export class OneStepPreApprovalBuilder extends TransactionBuilder {
6+
private _partyId: string;
7+
constructor(_coinConfig: Readonly<CoinConfig>) {
8+
super(_coinConfig);
9+
}
10+
11+
protected get transactionType(): TransactionType {
12+
return TransactionType.OneStepPreApproval;
13+
}
14+
15+
/**
16+
*
17+
*/
18+
public partyId(partyId: string): this {
19+
this._partyId = partyId;
20+
return this;
21+
}
22+
}

0 commit comments

Comments
 (0)