Skip to content

Commit ac8cf0a

Browse files
committed
fix: canton raw prepared transaction parsing
Ticket: COIN-6482
1 parent c75db75 commit ac8cf0a

File tree

3 files changed

+101
-44
lines changed

3 files changed

+101
-44
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export class Transaction extends BaseTransaction {
150150
// TODO: extract other required data (utxo used, request time, execute before etc)
151151
let parsedInfo: PreparedTxnParsedInfo;
152152
try {
153-
parsedInfo = utils.parseRawCantonTransactionData(this._prepareCommand.preparedTransaction);
153+
parsedInfo = utils.parseRawCantonTransactionData(this._prepareCommand.preparedTransaction, this.type);
154154
} catch (e) {
155155
throw new InvalidTransactionError(`Failed to parse transaction hash: ${e instanceof Error ? e.message : e}`);
156156
}

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

Lines changed: 76 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import BigNumber from 'bignumber.js';
22
import crypto from 'crypto';
33

4-
import { BaseUtils, isValidEd25519PublicKey } from '@bitgo/sdk-core';
4+
import { BaseUtils, isValidEd25519PublicKey, TransactionType } from '@bitgo/sdk-core';
55

66
import { computePreparedTransaction } from '../../resources/hash/hash.js';
77
import { PreparedTransaction } from '../../resources/proto/preparedTransaction.js';
@@ -79,66 +79,102 @@ export class Utils implements BaseUtils {
7979
/**
8080
* Method to parse raw canton transaction & get required data
8181
* @param {String} rawData base64 encoded string
82+
* @param {TransactionType} txType the transaction type
8283
* @returns {PreparedTxnParsedInfo}
8384
*/
84-
parseRawCantonTransactionData(rawData: string): PreparedTxnParsedInfo {
85+
parseRawCantonTransactionData(rawData: string, txType: TransactionType): PreparedTxnParsedInfo {
8586
const decodedData = this.decodePreparedTransaction(rawData);
8687
let sender = '';
8788
let receiver = '';
8889
let amount = '';
89-
decodedData.transaction?.nodes?.forEach((node) => {
90+
let preApprovalNode: RecordField[] = [];
91+
let transferNode: RecordField[] = [];
92+
let transferAcceptRejectNode: RecordField[] = [];
93+
const nodes = decodedData.transaction?.nodes;
94+
95+
nodes?.forEach((node) => {
9096
const versionedNode = node.versionedNode;
9197
if (!versionedNode || versionedNode.oneofKind !== 'v1') return;
92-
9398
const v1Node = versionedNode.v1;
9499
const nodeType = v1Node.nodeType;
95-
96100
if (nodeType.oneofKind !== 'create') return;
97-
98101
const createNode = nodeType.create;
99-
100-
const getField = (fields: RecordField[], label: string) => fields.find((f) => f.label === label)?.value?.sum;
101-
102-
// Check if it's the correct template
103102
const template = createNode.templateId;
104103
const argSum = createNode.argument?.sum;
105104
if (!argSum || argSum.oneofKind !== 'record') return;
106105
const fields = argSum.record?.fields;
107106
if (!fields) return;
108-
if (template?.entityName === 'AmuletTransferInstruction') {
109-
const transferField = fields.find((f) => f.label === 'transfer');
110-
const transferSum = transferField?.value?.sum;
111-
if (!transferSum || transferSum.oneofKind !== 'record') return;
107+
if (
108+
template?.entityName === 'TransferPreapprovalProposal' &&
109+
!preApprovalNode.length &&
110+
txType === TransactionType.OneStepPreApproval
111+
) {
112+
preApprovalNode = fields;
113+
}
114+
if (
115+
template?.entityName === 'Amulet' &&
116+
node?.nodeId === '9' &&
117+
!transferAcceptRejectNode.length &&
118+
txType === TransactionType.TransferAccept
119+
) {
120+
transferAcceptRejectNode = fields;
121+
}
122+
if (
123+
template?.entityName === 'Amulet' &&
124+
node?.nodeId === '4' &&
125+
!transferAcceptRejectNode.length &&
126+
txType === TransactionType.TransferReject
127+
) {
128+
transferAcceptRejectNode = fields;
129+
}
130+
if (
131+
template?.entityName === 'AmuletTransferInstruction' &&
132+
node?.nodeId === '14' &&
133+
!transferNode.length &&
134+
txType === TransactionType.Send
135+
) {
136+
transferNode = fields;
137+
}
138+
});
139+
140+
const getField = (fields: RecordField[], label: string) => fields.find((f) => f.label === label)?.value?.sum;
141+
142+
if (preApprovalNode.length) {
143+
const receiverData = getField(preApprovalNode, 'receiver');
144+
if (receiverData?.oneofKind === 'party') receiver = receiverData.party ?? '';
145+
const providerData = getField(preApprovalNode, 'provider');
146+
if (providerData?.oneofKind === 'party') sender = providerData.party ?? '';
147+
amount = '0';
148+
} else if (transferNode.length) {
149+
const transferField = transferNode.find((f) => f.label === 'transfer');
150+
const transferSum = transferField?.value?.sum;
151+
if (transferSum && transferSum.oneofKind === 'record') {
112152
const transferRecord = transferSum.record?.fields;
113-
if (!transferRecord) return;
114-
const senderData = getField(transferRecord, 'sender');
115-
if (senderData?.oneofKind === 'party') sender = senderData.party ?? '';
116-
117-
const receiverData = getField(transferRecord, 'receiver');
118-
if (receiverData?.oneofKind === 'party') receiver = receiverData.party ?? '';
119-
120-
const amountData = getField(transferRecord, 'amount');
121-
if (amountData?.oneofKind === 'numeric') amount = amountData.numeric ?? '';
122-
} else if (template?.entityName === 'Amulet') {
123-
const dsoData = getField(fields, 'dso');
124-
if (dsoData?.oneofKind === 'party') sender = dsoData.party ?? '';
125-
const ownerData = getField(fields, 'owner');
126-
if (ownerData?.oneofKind === 'party') receiver = ownerData.party ?? '';
127-
const amountField = getField(fields, 'amount');
128-
if (!amountField || amountField.oneofKind !== 'record') return;
153+
if (transferRecord?.length) {
154+
const senderData = getField(transferRecord, 'sender');
155+
if (senderData?.oneofKind === 'party') sender = senderData.party ?? '';
129156

157+
const receiverData = getField(transferRecord, 'receiver');
158+
if (receiverData?.oneofKind === 'party') receiver = receiverData.party ?? '';
159+
160+
const amountData = getField(transferRecord, 'amount');
161+
if (amountData?.oneofKind === 'numeric') amount = amountData.numeric ?? '';
162+
}
163+
}
164+
} else if (transferAcceptRejectNode.length) {
165+
const dsoData = getField(transferAcceptRejectNode, 'dso');
166+
if (dsoData?.oneofKind === 'party') sender = dsoData.party ?? '';
167+
const ownerData = getField(transferAcceptRejectNode, 'owner');
168+
if (ownerData?.oneofKind === 'party') receiver = ownerData.party ?? '';
169+
const amountField = getField(transferAcceptRejectNode, 'amount');
170+
if (amountField && amountField.oneofKind === 'record') {
130171
const amountRecord = amountField.record?.fields;
131-
if (!amountRecord) return;
132-
const initialAmountData = getField(amountRecord, 'initialAmount');
133-
if (initialAmountData?.oneofKind === 'numeric') amount = initialAmountData.numeric ?? '';
134-
} else if (template?.entityName === 'TransferPreapprovalProposal') {
135-
const receiverData = getField(fields, 'receiver');
136-
if (receiverData?.oneofKind === 'party') receiver = receiverData.party ?? '';
137-
const providerData = getField(fields, 'provider');
138-
if (providerData?.oneofKind === 'party') sender = providerData.party ?? '';
139-
amount = '0';
172+
if (amountRecord?.length) {
173+
const initialAmountData = getField(amountRecord, 'initialAmount');
174+
if (initialAmountData?.oneofKind === 'numeric') amount = initialAmountData.numeric ?? '';
175+
}
140176
}
141-
});
177+
}
142178
if (!sender || !receiver || !amount) {
143179
const missingFields: string[] = [];
144180
if (!sender) missingFields.push('sender');

modules/sdk-coin-canton/test/unit/utils.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import assert from 'assert';
22
import should from 'should';
3+
4+
import { TransactionType } from '@bitgo/sdk-core';
5+
36
import utils from '../../src/lib/utils';
47
import {
58
CANTON_ADDRESSES,
@@ -9,20 +12,24 @@ import {
912
PreparedTransactionRawData,
1013
PrepareSubmissionResponse,
1114
TransferAcceptancePrepareResponse,
15+
TransferRejectionPrepareResponse,
1216
} from '../resources';
1317

1418
describe('Canton Util', function () {
1519
describe('Raw transaction parser', function () {
1620
it('should parse the prepared transaction', () => {
17-
const parsedData = utils.parseRawCantonTransactionData(PreparedTransactionRawData);
21+
const parsedData = utils.parseRawCantonTransactionData(PreparedTransactionRawData, TransactionType.Send);
1822
should.exist(parsedData);
1923
assert.equal(parsedData.sender, 'abc-1::12200c1ee226fbdf9fba3461c2c0c73331b69d3c6fd8cfce28cdf864141141cc656d');
2024
assert.equal(parsedData.receiver, 'abc-2::12207e96ada18a845adf4dc01410265633d5266dca9bb280c98e35c3692db87d3e35');
2125
assert.equal(parsedData.amount, '200000000000');
2226
});
2327

2428
it('should parse the acceptance prepared transaction', () => {
25-
const parsedData = utils.parseRawCantonTransactionData(TransferAcceptancePrepareResponse.preparedTransaction);
29+
const parsedData = utils.parseRawCantonTransactionData(
30+
TransferAcceptancePrepareResponse.preparedTransaction,
31+
TransactionType.TransferAccept
32+
);
2633
should.exist(parsedData);
2734
assert.equal(parsedData.sender, 'DSO::1220be58c29e65de40bf273be1dc2b266d43a9a002ea5b18955aeef7aac881bb471a');
2835
assert.equal(
@@ -32,8 +39,22 @@ describe('Canton Util', function () {
3239
assert.equal(parsedData.amount, '50000000000');
3340
});
3441

42+
it('should parse the rejection prepared transaction', () => {
43+
const parsedData = utils.parseRawCantonTransactionData(
44+
TransferRejectionPrepareResponse.preparedTransaction,
45+
TransactionType.TransferReject
46+
);
47+
should.exist(parsedData);
48+
assert.equal(parsedData.sender, 'DSO::1220be58c29e65de40bf273be1dc2b266d43a9a002ea5b18955aeef7aac881bb471a');
49+
assert.equal(parsedData.receiver, '12201::122038402cf1650876d2920d6047b11a4aaf0de7b428e9916009cba2a22b1ae22c1a');
50+
assert.equal(parsedData.amount, '50000000000');
51+
});
52+
3553
it('should parse the one-step preapproval prepared transaction', () => {
36-
const parsedData = utils.parseRawCantonTransactionData(OneStepPreApprovalPrepareResponse.preparedTransaction);
54+
const parsedData = utils.parseRawCantonTransactionData(
55+
OneStepPreApprovalPrepareResponse.preparedTransaction,
56+
TransactionType.OneStepPreApproval
57+
);
3758
should.exist(parsedData);
3859
assert.equal(
3960
parsedData.sender,

0 commit comments

Comments
 (0)