Skip to content

Commit 4dd2b6d

Browse files
committed
fix(sdk-coin-ada): token verifytransaction
TICKET: COIN-7292
1 parent aa2abeb commit 4dd2b6d

2 files changed

Lines changed: 233 additions & 2 deletions

File tree

modules/sdk-coin-ada/src/adaToken.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import { Ada } from './ada';
2-
import { BitGoBase, CoinConstructor, NamedCoinConstructor } from '@bitgo/sdk-core';
2+
import {
3+
BitGoBase,
4+
CoinConstructor,
5+
NamedCoinConstructor,
6+
VerifyTransactionOptions,
7+
NodeEnvironmentError,
8+
} from '@bitgo/sdk-core';
39
import { coins, tokens, AdaTokenConfig } from '@bitgo/statics';
10+
import { Transaction } from './lib';
11+
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs';
12+
import assert from 'assert';
413

514
export class AdaToken extends Ada {
615
public readonly tokenConfig: AdaTokenConfig;
@@ -85,4 +94,50 @@ export class AdaToken extends Ada {
8594
get contractAddress() {
8695
return this.tokenConfig.contractAddress;
8796
}
97+
98+
/**
99+
* Verify that a token transaction prebuild complies with the original intention.
100+
* For token transfers, we need to verify the token amount in multiAssets, not the ADA amount.
101+
*
102+
* @param params.txPrebuild prebuild transaction
103+
* @param params.txParams transaction parameters
104+
* @return true if verification success
105+
*/
106+
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
107+
try {
108+
const coinConfig = coins.get(this.getBaseChain());
109+
const { txPrebuild, txParams } = params;
110+
const transaction = new Transaction(coinConfig);
111+
assert(txPrebuild.txHex, new Error('missing required tx prebuild property txHex'));
112+
113+
transaction.fromRawTransaction(txPrebuild.txHex);
114+
const txJson = transaction.toJson();
115+
116+
if (txParams.recipients !== undefined) {
117+
const policyScriptHash = CardanoWasm.ScriptHash.from_hex(this.tokenConfig.policyId);
118+
const assetName = CardanoWasm.AssetName.new(Buffer.from(this.tokenConfig.assetName, 'hex'));
119+
120+
for (const recipient of txParams.recipients) {
121+
const found = txJson.outputs.some((output) => {
122+
if (recipient.address !== output.address || !output.multiAssets) {
123+
return false;
124+
}
125+
const multiAssets = output.multiAssets as CardanoWasm.MultiAsset;
126+
const tokenQty = multiAssets.get_asset(policyScriptHash, assetName);
127+
return tokenQty && tokenQty.to_str() === recipient.amount;
128+
});
129+
130+
if (!found) {
131+
throw new Error('cannot find recipient in expected output');
132+
}
133+
}
134+
}
135+
} catch (e) {
136+
if (e instanceof NodeEnvironmentError) {
137+
return true;
138+
}
139+
throw e;
140+
}
141+
return true;
142+
}
88143
}

modules/sdk-coin-ada/test/unit/tokenWithdrawal.ts

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import should from 'should';
22
import { TransactionType } from '@bitgo/sdk-core';
33
import * as testData from '../resources';
4-
import { TransactionBuilderFactory } from '../../src';
4+
import { TransactionBuilderFactory, AdaToken } from '../../src';
55
import { coins } from '@bitgo/statics';
66
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs';
77
import { Transaction } from '../../src/lib/transaction';
8+
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
9+
import { BitGoAPI } from '@bitgo/sdk-api';
10+
11+
// Set test password for BitGoJS tests
12+
process.env.BITGOJS_TEST_PASSWORD = 't3stSicretly!';
813

914
describe('ADA Token Operations', async () => {
1015
const factory = new TransactionBuilderFactory(coins.get('tada'));
@@ -326,4 +331,175 @@ describe('ADA Token Operations', async () => {
326331

327332
await txBuilder.build().should.not.be.rejected();
328333
});
334+
335+
describe('AdaToken verifyTransaction', () => {
336+
let bitgo: TestBitGoAPI;
337+
let adaToken;
338+
339+
before(function () {
340+
bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' });
341+
bitgo.initializeTestVars();
342+
const tokenConfig = {
343+
type: 'tada:water',
344+
coin: 'tada',
345+
network: 'Testnet' as const,
346+
name: 'WATER',
347+
decimalPlaces: 0,
348+
policyId: policyId,
349+
assetName: asciiEncodedName,
350+
contractAddress: `${policyId}:${asciiEncodedName}`,
351+
};
352+
adaToken = new AdaToken(bitgo, tokenConfig);
353+
});
354+
355+
it('should verify a token transaction with correct token amount', async () => {
356+
const quantity = '20';
357+
const totalInput = 20000000;
358+
const totalAssetList = {
359+
[fingerprint]: {
360+
quantity: '100',
361+
policy_id: policyId,
362+
asset_name: asciiEncodedName,
363+
},
364+
};
365+
366+
const txBuilder = factory.getTransferBuilder();
367+
txBuilder.input({
368+
transaction_id: '3677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba21',
369+
transaction_index: 1,
370+
});
371+
372+
txBuilder.output({
373+
address: receiverAddress,
374+
amount: '0',
375+
multiAssets: {
376+
asset_name: asciiEncodedName,
377+
policy_id: policyId,
378+
quantity,
379+
fingerprint,
380+
},
381+
});
382+
383+
txBuilder.changeAddress(senderAddress, totalInput.toString(), totalAssetList);
384+
txBuilder.ttl(800000000);
385+
txBuilder.isTokenTransaction();
386+
const tx = (await txBuilder.build()) as Transaction;
387+
const txHex = tx.toBroadcastFormat();
388+
389+
// Verify transaction with correct token amount
390+
const txParams = {
391+
recipients: [
392+
{
393+
address: receiverAddress,
394+
amount: quantity, // Token amount, not ADA amount
395+
},
396+
],
397+
};
398+
399+
const txPrebuild = { txHex };
400+
const isVerified = await adaToken.verifyTransaction({ txParams, txPrebuild });
401+
isVerified.should.equal(true);
402+
});
403+
404+
it('should fail to verify a token transaction with incorrect token amount', async () => {
405+
const quantity = '20';
406+
const totalInput = 20000000;
407+
const totalAssetList = {
408+
[fingerprint]: {
409+
quantity: '100',
410+
policy_id: policyId,
411+
asset_name: asciiEncodedName,
412+
},
413+
};
414+
415+
const txBuilder = factory.getTransferBuilder();
416+
txBuilder.input({
417+
transaction_id: '3677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba21',
418+
transaction_index: 1,
419+
});
420+
421+
txBuilder.output({
422+
address: receiverAddress,
423+
amount: '0',
424+
multiAssets: {
425+
asset_name: asciiEncodedName,
426+
policy_id: policyId,
427+
quantity,
428+
fingerprint,
429+
},
430+
});
431+
432+
txBuilder.changeAddress(senderAddress, totalInput.toString(), totalAssetList);
433+
txBuilder.ttl(800000000);
434+
txBuilder.isTokenTransaction();
435+
const tx = (await txBuilder.build()) as Transaction;
436+
const txHex = tx.toBroadcastFormat();
437+
438+
// Verify transaction with WRONG token amount (should fail)
439+
const txParams = {
440+
recipients: [
441+
{
442+
address: receiverAddress,
443+
amount: '999', // Wrong amount
444+
},
445+
],
446+
};
447+
448+
const txPrebuild = { txHex };
449+
await adaToken
450+
.verifyTransaction({ txParams, txPrebuild })
451+
.should.be.rejectedWith('cannot find recipient in expected output');
452+
});
453+
454+
it('should fail to verify when address does not match', async () => {
455+
const quantity = '20';
456+
const totalInput = 20000000;
457+
const totalAssetList = {
458+
[fingerprint]: {
459+
quantity: '100',
460+
policy_id: policyId,
461+
asset_name: asciiEncodedName,
462+
},
463+
};
464+
465+
const txBuilder = factory.getTransferBuilder();
466+
txBuilder.input({
467+
transaction_id: '3677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba21',
468+
transaction_index: 1,
469+
});
470+
471+
txBuilder.output({
472+
address: receiverAddress,
473+
amount: '0',
474+
multiAssets: {
475+
asset_name: asciiEncodedName,
476+
policy_id: policyId,
477+
quantity,
478+
fingerprint,
479+
},
480+
});
481+
482+
txBuilder.changeAddress(senderAddress, totalInput.toString(), totalAssetList);
483+
txBuilder.ttl(800000000);
484+
txBuilder.isTokenTransaction();
485+
const tx = (await txBuilder.build()) as Transaction;
486+
const txHex = tx.toBroadcastFormat();
487+
488+
// Verify with wrong address (should fail)
489+
const txParams = {
490+
recipients: [
491+
{
492+
address:
493+
'addr_test1qqa86e3d7lfpwu0k2rhjz76ecmfxdr74s9kf9yfcp5hj5vmnh6xccjcclrk8jtaw9jgeuy99p2n8smtdpylmy45qjjfsfmp3g6',
494+
amount: quantity,
495+
},
496+
],
497+
};
498+
499+
const txPrebuild = { txHex };
500+
await adaToken
501+
.verifyTransaction({ txParams, txPrebuild })
502+
.should.be.rejectedWith('cannot find recipient in expected output');
503+
});
504+
});
329505
});

0 commit comments

Comments
 (0)