Skip to content

Commit d9402f4

Browse files
committed
feat(sdk-coin-icp): enhance raw transaction handling
TICKET: WIN-4908
1 parent 04c8e7b commit d9402f4

File tree

5 files changed

+102
-58
lines changed

5 files changed

+102
-58
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import {
44
} from '@bitgo/sdk-core';
55

66
export const REQUEST_STATUS = 'request_status';
7+
export const MAX_INGRESS_TTL = 5 * 60 * 1000_000_000; // 5 minutes in nanoseconds
8+
export const PERMITTED_DRIFT = 60 * 1000_000_000; // 60 seconds in nanoseconds
9+
export const LEDGER_CANISTER_ID = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 2, 1, 1]); // Uint8Array value for "00000000000000020101" and the string value is "ryjl3-tyaaa-aaaaa-aaaba-cai"
710

811
export enum RequestType {
912
CALL = 'call',
@@ -189,3 +192,8 @@ export interface SignedTransactionRequest {
189192
network_identifier: NetworkIdentifier;
190193
signed_transaction: string;
191194
}
195+
196+
export interface RawTransaction {
197+
serializedTxHex: string;
198+
publicKey: string;
199+
}

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

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import {
2222
ParsedTransaction,
2323
IcpOperation,
2424
IcpAccount,
25+
MAX_INGRESS_TTL,
26+
PERMITTED_DRIFT,
27+
RawTransaction,
2528
} from './iface';
2629
import { Utils } from './utils';
2730

@@ -78,20 +81,23 @@ export class Transaction extends BaseTransaction {
7881
return this._payloadsData;
7982
}
8083

81-
fromRawTransaction(rawTransaction: string): void {
84+
async fromRawTransaction(rawTransaction: string): Promise<void> {
8285
try {
83-
const parsedTx = JSON.parse(rawTransaction);
84-
switch (parsedTx.type) {
86+
const jsonRawTransaction: RawTransaction = JSON.parse(rawTransaction);
87+
const parsedTx = await this.parseUnsignedTransaction(jsonRawTransaction.serializedTxHex);
88+
const senderPublicKeyHex = jsonRawTransaction.publicKey;
89+
const transactionType = parsedTx.operations[0].type;
90+
switch (transactionType) {
8591
case OperationType.TRANSACTION:
8692
this._icpTransactionData = {
87-
senderAddress: parsedTx.address,
88-
receiverAddress: parsedTx.externalOutputs[0].address,
89-
amount: parsedTx.spendAmountString,
90-
fee: parsedTx.fee,
91-
senderPublicKeyHex: parsedTx.senderKey,
92-
memo: parsedTx.seqno,
93-
transactionType: parsedTx.type,
94-
expiryTime: parsedTx.expiryTime,
93+
senderAddress: parsedTx.operations[0].account.address,
94+
receiverAddress: parsedTx.operations[1].account.address,
95+
amount: parsedTx.operations[1].amount.value,
96+
fee: parsedTx.operations[2].amount.value,
97+
senderPublicKeyHex: senderPublicKeyHex,
98+
memo: parsedTx.metadata.memo,
99+
transactionType: transactionType,
100+
expiryTime: Number(parsedTx.metadata.created_at_time + (MAX_INGRESS_TTL - PERMITTED_DRIFT)),
95101
};
96102
this._utils.validateRawTransaction(this._icpTransactionData);
97103
break;

modules/sdk-coin-icp/src/lib/unsignedTransactionBuilder.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@ import {
77
SignatureType,
88
OperationType,
99
MethodName,
10+
MAX_INGRESS_TTL,
11+
PERMITTED_DRIFT,
12+
LEDGER_CANISTER_ID,
1013
} from './iface';
1114
import utils from './utils';
1215

13-
const MAX_INGRESS_TTL = 5 * 60 * 1000_000_000; // 5 minutes in nanoseconds
14-
const PERMITTED_DRIFT = 60 * 1000_000_000; // 60 seconds in nanoseconds
15-
const LEDGER_CANISTER_ID = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 2, 1, 1]); // Uint8Array value for "00000000000000020101" and the string value is "ryjl3-tyaaa-aaaaa-aaaba-cai"
16-
1716
export class UnsignedTransactionBuilder {
1817
private _icpTransactionPayload: IcpTransaction;
1918
constructor(icpTransactionPayload: IcpTransaction) {

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

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -98,23 +98,71 @@ export const IcpTransactionData = {
9898
};
9999

100100
export const rawTransaction = {
101-
outputAmount: '10',
102-
inputAmount: '10',
103-
spendAmount: '10',
104-
fee: '-10000',
105-
externalOutputs: [
101+
serializedTxHex:
102+
'b90002677570646174657381826b5452414e53414354494f4eb900056b63616e69737465725f69644a000000000000000201016b6d6574686f645f6e616d656773656e645f70626361726758410a02080112060a0408c0843d1a0308904e2a220a20c3d30f404955975adaba89f2e1ebc75c1f44a6a204578afce8f3780d64fe252e3a0a0880ecbac5bfcc9d97186673656e646572581dd5fc1dc4d74d4aa35d81cf345533d20548113412d32fffdcece2f68a026e696e67726573735f6578706972791b000000000000000070696e67726573735f6578706972696573811b182e769bd9cc1600',
103+
publicKey:
104+
'042ab77b959e28c4fa47fa8fb9e57cec3d66df5684d076ac2e4c5f28fd69a23dd31a59f908c8add51eab3530b4ac5d015166eaf2198c52fa9a8df7cfaeb8fdb7d4',
105+
};
106+
107+
export const parsedRawTransaction = {
108+
operations: [
109+
{
110+
operation_identifier: {
111+
index: 0,
112+
},
113+
type: 'TRANSACTION',
114+
status: null,
115+
account: {
116+
address: '0af815da8259ba8bb3d34fbfb2ac730f07a1adc81438d40d667d91b408b25f2f',
117+
},
118+
amount: {
119+
value: '-1000000',
120+
currency: {
121+
symbol: 'ICP',
122+
decimals: 8,
123+
},
124+
},
125+
},
126+
{
127+
operation_identifier: {
128+
index: 1,
129+
},
130+
type: 'TRANSACTION',
131+
status: null,
132+
account: {
133+
address: 'c3d30f404955975adaba89f2e1ebc75c1f44a6a204578afce8f3780d64fe252e',
134+
},
135+
amount: {
136+
value: '1000000',
137+
currency: {
138+
symbol: 'ICP',
139+
decimals: 8,
140+
},
141+
},
142+
},
106143
{
107-
amount: '10',
108-
address: accounts.account2.address,
144+
operation_identifier: {
145+
index: 2,
146+
},
147+
type: 'FEE',
148+
status: null,
149+
account: {
150+
address: '0af815da8259ba8bb3d34fbfb2ac730f07a1adc81438d40d667d91b408b25f2f',
151+
},
152+
amount: {
153+
value: '-10000',
154+
currency: {
155+
symbol: 'ICP',
156+
decimals: 8,
157+
},
158+
},
109159
},
110160
],
111-
type: OperationType.TRANSACTION,
112-
address: accounts.account1.address,
113-
senderKey: accounts.account1.publicKey,
114-
seqno: 1740638136656000000,
115-
spendAmountString: '10',
116-
id: '5jTEPuDcMCeEgp1iyEbNBKsnhYz4F4c1EPDtRmxm3wCw',
117-
expiryTime: Date.now() * 1000_000 + 5 * 60 * 1000_000_000,
161+
account_identifier_signers: [],
162+
metadata: {
163+
created_at_time: 1742457444920999936,
164+
memo: 1,
165+
},
118166
};
119167

120168
export const metaData: IcpMetadata = {

modules/sdk-coin-icp/test/unit/transaction.ts

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import assert from 'assert';
44
import should from 'should';
55
import { Utils } from '../../src/lib/utils';
66
import { InvalidTransactionError, TransactionType as BitGoTransactionType } from '@bitgo/sdk-core';
7-
import { rawTransaction, accounts } from '../resources/icp';
7+
import { rawTransaction, accounts, parsedRawTransaction } from '../resources/icp';
8+
import sinon from 'sinon';
89

910
describe('ICP Transaction', () => {
1011
let tx: Transaction;
@@ -15,7 +16,8 @@ describe('ICP Transaction', () => {
1516
beforeEach(() => {
1617
utils = new Utils();
1718
tx = new Transaction(config, utils);
18-
localRawTransaction = JSON.parse(JSON.stringify(rawTransaction));
19+
localRawTransaction = JSON.stringify(rawTransaction);
20+
sinon.stub(utils, 'validateExpireTime').returns(true);
1921
});
2022

2123
describe('empty transaction', () => {
@@ -35,43 +37,24 @@ describe('ICP Transaction', () => {
3537

3638
describe('from raw transaction', () => {
3739
it('build a json transaction from raw hex', async () => {
38-
tx.fromRawTransaction(JSON.stringify(localRawTransaction));
40+
await tx.fromRawTransaction(localRawTransaction);
3941
const json = tx.toJson();
40-
should.equal(json.expirationTime, localRawTransaction.expiryTime);
41-
should.equal(json.memo, localRawTransaction.seqno);
42-
should.equal(json.feeAmount, '-10000');
43-
should.equal(json.sender, localRawTransaction.address);
44-
should.equal(json.recipient, localRawTransaction.externalOutputs[0].address);
42+
should.equal(json.memo, parsedRawTransaction.metadata.memo);
43+
should.equal(json.feeAmount, parsedRawTransaction.operations[2].amount.value);
44+
should.equal(json.sender, parsedRawTransaction.operations[0].account.address);
45+
should.equal(json.recipient, parsedRawTransaction.operations[1].account.address);
4546
should.equal(json.type, BitGoTransactionType.Send);
4647
should.equal(json.senderPublicKey, accounts.account1.publicKey);
4748
});
48-
49-
it('should fail when memo is passed as alphanumeric', async () => {
50-
(localRawTransaction as any).seqno = 'abc123';
51-
assert.throws(
52-
() => tx.fromRawTransaction(JSON.stringify(localRawTransaction)),
53-
(err) => err instanceof InvalidTransactionError && err.message === 'Invalid raw transaction',
54-
'Expected an InvalidTransactionError with message "Invalid raw transaction"'
55-
);
56-
});
57-
58-
it('should fail when memo is passed as string', async () => {
59-
(localRawTransaction as any).seqno = 'abc';
60-
assert.throws(
61-
() => tx.fromRawTransaction(JSON.stringify(localRawTransaction)),
62-
(err) => err instanceof InvalidTransactionError && err.message === 'Invalid raw transaction',
63-
'Expected an InvalidTransactionError with message "Invalid raw transaction"'
64-
);
65-
});
6649
});
6750

6851
describe('Explain', () => {
6952
it('explain transaction', async () => {
70-
tx.fromRawTransaction(JSON.stringify(localRawTransaction));
53+
await tx.fromRawTransaction(localRawTransaction);
7154
const explain = tx.explainTransaction();
7255

73-
explain.outputAmount.should.equal('10');
74-
explain.outputs[0].amount.should.equal('10');
56+
explain.outputAmount.should.equal('1000000');
57+
explain.outputs[0].amount.should.equal('1000000');
7558
explain.outputs[0].address.should.equal(accounts.account2.address);
7659
explain.fee.fee.should.equal('-10000');
7760
explain.changeAmount.should.equal('0');

0 commit comments

Comments
 (0)