Skip to content

Commit 94a92df

Browse files
authored
Merge pull request #7479 from BitGo/COIN-6482
fix: canton raw prepared transaction parsing
2 parents 838a76d + 5dbf201 commit 94a92df

File tree

4 files changed

+129
-46
lines changed

4 files changed

+129
-46
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: 75 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,101 @@ 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+
!transferAcceptRejectNode.length &&
117+
(txType === TransactionType.TransferAccept || txType === TransactionType.TransferReject)
118+
) {
119+
transferAcceptRejectNode = fields;
120+
}
121+
});
122+
123+
nodes?.forEach((node) => {
124+
const versionedNode = node.versionedNode;
125+
if (!versionedNode || versionedNode.oneofKind !== 'v1') return;
126+
const v1Node = versionedNode.v1;
127+
const nodeType = v1Node.nodeType;
128+
if (nodeType.oneofKind !== 'exercise') return;
129+
const exerciseNode = nodeType.exercise;
130+
const choiceId = exerciseNode.choiceId;
131+
if (!choiceId || choiceId !== 'TransferFactory_Transfer') return;
132+
const argSum = exerciseNode.chosenValue?.sum;
133+
if (!argSum || argSum.oneofKind !== 'record') return;
134+
const fields = argSum.record?.fields;
135+
if (!fields) return;
136+
transferNode = fields;
137+
});
138+
139+
const getField = (fields: RecordField[], label: string) => fields.find((f) => f.label === label)?.value?.sum;
140+
141+
if (preApprovalNode.length) {
142+
const receiverData = getField(preApprovalNode, 'receiver');
143+
if (receiverData?.oneofKind === 'party') receiver = receiverData.party ?? '';
144+
const providerData = getField(preApprovalNode, 'provider');
145+
if (providerData?.oneofKind === 'party') sender = providerData.party ?? '';
146+
amount = '0';
147+
} else if (transferNode.length) {
148+
const transferField = transferNode.find((f) => f.label === 'transfer');
149+
const transferSum = transferField?.value?.sum;
150+
if (transferSum && transferSum.oneofKind === 'record') {
112151
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;
152+
if (transferRecord?.length) {
153+
const senderData = getField(transferRecord, 'sender');
154+
if (senderData?.oneofKind === 'party') sender = senderData.party ?? '';
155+
156+
const receiverData = getField(transferRecord, 'receiver');
157+
if (receiverData?.oneofKind === 'party') receiver = receiverData.party ?? '';
129158

159+
const amountData = getField(transferRecord, 'amount');
160+
if (amountData?.oneofKind === 'numeric') amount = amountData.numeric ?? '';
161+
}
162+
}
163+
} else if (transferAcceptRejectNode.length) {
164+
const dsoData = getField(transferAcceptRejectNode, 'dso');
165+
if (dsoData?.oneofKind === 'party') sender = dsoData.party ?? '';
166+
const ownerData = getField(transferAcceptRejectNode, 'owner');
167+
if (ownerData?.oneofKind === 'party') receiver = ownerData.party ?? '';
168+
const amountField = getField(transferAcceptRejectNode, 'amount');
169+
if (amountField && amountField.oneofKind === 'record') {
130170
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';
171+
if (amountRecord?.length) {
172+
const initialAmountData = getField(amountRecord, 'initialAmount');
173+
if (initialAmountData?.oneofKind === 'numeric') amount = initialAmountData.numeric ?? '';
174+
}
140175
}
141-
});
176+
}
142177
if (!sender || !receiver || !amount) {
143178
const missingFields: string[] = [];
144179
if (!sender) missingFields.push('sender');

modules/sdk-coin-canton/test/resources.ts

Lines changed: 6 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,81 @@
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,
69
CANTON_BLOCK_HEIGHT,
710
GenerateTopologyResponse,
811
OneStepPreApprovalPrepareResponse,
912
PreparedTransactionRawData,
13+
PreparedTxn1StepReceiver,
14+
PreparedTxn2StepReceiver,
1015
PrepareSubmissionResponse,
1116
TransferAcceptancePrepareResponse,
17+
TransferRejectionPrepareResponse,
1218
} from '../resources';
1319

1420
describe('Canton Util', function () {
1521
describe('Raw transaction parser', function () {
1622
it('should parse the prepared transaction', () => {
17-
const parsedData = utils.parseRawCantonTransactionData(PreparedTransactionRawData);
23+
const parsedData = utils.parseRawCantonTransactionData(PreparedTransactionRawData, TransactionType.Send);
1824
should.exist(parsedData);
1925
assert.equal(parsedData.sender, 'abc-1::12200c1ee226fbdf9fba3461c2c0c73331b69d3c6fd8cfce28cdf864141141cc656d');
2026
assert.equal(parsedData.receiver, 'abc-2::12207e96ada18a845adf4dc01410265633d5266dca9bb280c98e35c3692db87d3e35');
2127
assert.equal(parsedData.amount, '200000000000');
2228
});
2329

24-
it('should parse the acceptance prepared transaction', () => {
25-
const parsedData = utils.parseRawCantonTransactionData(TransferAcceptancePrepareResponse.preparedTransaction);
30+
it('should parse correctly when receiver is on 2-step', () => {
31+
const parsedData = utils.parseRawCantonTransactionData(PreparedTxn2StepReceiver, TransactionType.Send);
2632
should.exist(parsedData);
27-
assert.equal(parsedData.sender, 'DSO::1220be58c29e65de40bf273be1dc2b266d43a9a002ea5b18955aeef7aac881bb471a');
33+
assert.equal(parsedData.sender, '1220a::1220ade60300cf7d0b18ffaa2ffe4f492ad1ad601cfc162b20f77ec99d16c2c2f158');
34+
assert.equal(
35+
parsedData.receiver,
36+
'ravi-test-party-1::122092e7d33ac10c0f3d55976342f37555df05da5b742956d56a62ae2367769079d2'
37+
);
38+
assert.equal(parsedData.amount, '20000000000');
39+
});
40+
41+
it('should parse correctly 1', () => {
42+
const parsedData = utils.parseRawCantonTransactionData(PreparedTxn1StepReceiver, TransactionType.Send);
43+
should.exist(parsedData);
44+
assert.equal(parsedData.sender, '1220a::1220ade60300cf7d0b18ffaa2ffe4f492ad1ad601cfc162b20f77ec99d16c2c2f158');
2845
assert.equal(
2946
parsedData.receiver,
3047
'ravi-demo-party-txn-01-tapper::1220ea7ab5a723f8a6b2078e617e6c58cb7e78e49947ddc239e1a941aa56e6ba08b4'
3148
);
49+
assert.equal(parsedData.amount, '20000000000');
50+
});
51+
52+
it('should parse the acceptance prepared transaction', () => {
53+
const parsedData = utils.parseRawCantonTransactionData(
54+
TransferAcceptancePrepareResponse.preparedTransaction,
55+
TransactionType.TransferAccept
56+
);
57+
should.exist(parsedData);
58+
assert.equal(parsedData.sender, 'DSO::1220be58c29e65de40bf273be1dc2b266d43a9a002ea5b18955aeef7aac881bb471a');
59+
assert.equal(parsedData.receiver, 'abcde::12205b4e3537a95126d90604592344d8ad3c3ddccda4f79901954280ee19c576714d');
60+
assert.equal(parsedData.amount, '50000000000');
61+
});
62+
63+
it('should parse the rejection prepared transaction', () => {
64+
const parsedData = utils.parseRawCantonTransactionData(
65+
TransferRejectionPrepareResponse.preparedTransaction,
66+
TransactionType.TransferReject
67+
);
68+
should.exist(parsedData);
69+
assert.equal(parsedData.sender, 'DSO::1220be58c29e65de40bf273be1dc2b266d43a9a002ea5b18955aeef7aac881bb471a');
70+
assert.equal(parsedData.receiver, '12201::122038402cf1650876d2920d6047b11a4aaf0de7b428e9916009cba2a22b1ae22c1a');
3271
assert.equal(parsedData.amount, '50000000000');
3372
});
3473

3574
it('should parse the one-step preapproval prepared transaction', () => {
36-
const parsedData = utils.parseRawCantonTransactionData(OneStepPreApprovalPrepareResponse.preparedTransaction);
75+
const parsedData = utils.parseRawCantonTransactionData(
76+
OneStepPreApprovalPrepareResponse.preparedTransaction,
77+
TransactionType.OneStepPreApproval
78+
);
3779
should.exist(parsedData);
3880
assert.equal(
3981
parsedData.sender,

0 commit comments

Comments
 (0)