Skip to content

Commit 72b5279

Browse files
authored
Merge pull request #5750 from BitGo/COIN-3290
feat(sdk-coin-stx): implement verify transaction method
2 parents 7aded40 + 2115f4a commit 72b5279

File tree

6 files changed

+276
-3
lines changed

6 files changed

+276
-3
lines changed

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import * as _ from 'lodash';
3131
import { InvalidTransactionError, isValidXprv, isValidXpub, SigningError, UtilsError } from '@bitgo/sdk-core';
3232
import { AddressDetails, SendParams, TokenTransferParams } from './iface';
3333
import { KeyPair } from '.';
34-
import { StacksNetwork as BitgoStacksNetwork } from '@bitgo/statics';
34+
import { coins, Sip10Token, StacksNetwork as BitgoStacksNetwork } from '@bitgo/statics';
3535
import { VALID_CONTRACT_FUNCTION_NAMES } from './constants';
3636

3737
/**
@@ -535,3 +535,19 @@ export function isSameBaseAddress(address: string, baseAddress: string): boolean
535535
}
536536
return getBaseAddress(address) === getBaseAddress(baseAddress);
537537
}
538+
539+
/**
540+
* Function to get tokenName from list of sip10 tokens using contract details
541+
*
542+
* @param {String} contractAddress
543+
* @param {String} contractName
544+
* @returns {String|Undefined}
545+
*/
546+
export function findTokenNameByContract(contractAddress: string, contractName: string): string | undefined {
547+
{
548+
const tokenName = coins
549+
.filter((coin) => coin instanceof Sip10Token && coin.assetId.includes(`${contractAddress}.${contractName}`))
550+
.map((coin) => coin.name);
551+
return tokenName ? tokenName[0] : undefined;
552+
}
553+
}

modules/sdk-coin-stx/src/sip10Token.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import { BitGoBase, CoinConstructor, NamedCoinConstructor } from '@bitgo/sdk-core';
2-
import { coins, NetworkType, Sip10TokenConfig, tokens } from '@bitgo/statics';
1+
import _ from 'lodash';
2+
import BigNumber from 'bignumber.js';
3+
4+
import { BitGoBase, CoinConstructor, NamedCoinConstructor, VerifyTransactionOptions } from '@bitgo/sdk-core';
5+
import { BaseCoin as StaticsBaseCoin, coins, NetworkType, Sip10TokenConfig, tokens } from '@bitgo/statics';
6+
37
import { Stx } from './stx';
8+
import { TransactionBuilderFactory } from './lib';
9+
import { TransactionBuilder } from './lib/transactionBuilder';
410

511
export class Sip10Token extends Stx {
612
public readonly tokenConfig: Sip10TokenConfig;
@@ -59,4 +65,64 @@ export class Sip10Token extends Stx {
5965
getBaseFactor(): number {
6066
return Math.pow(10, this.tokenConfig.decimalPlaces);
6167
}
68+
69+
getTransaction(coinConfig: Readonly<StaticsBaseCoin>): TransactionBuilder {
70+
return new TransactionBuilderFactory(coinConfig).getFungibleTokenTransferBuilder();
71+
}
72+
73+
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
74+
const { txPrebuild: txPrebuild, txParams: txParams } = params;
75+
if (Array.isArray(txParams.recipients) && txParams.recipients.length > 1) {
76+
throw new Error(
77+
`${this.getChain()} doesn't support sending to more than 1 destination address within a single transaction. Try again, using only a single recipient.`
78+
);
79+
}
80+
const rawTx = txPrebuild.txHex;
81+
if (!rawTx) {
82+
throw new Error('missing required tx prebuild property txHex');
83+
}
84+
const coinConfig = coins.get(this.getChain());
85+
const transaction = this.getTransaction(coinConfig);
86+
transaction.from(rawTx);
87+
const explainedTx = await this.explainTransaction({ txHex: rawTx, feeInfo: { fee: '' } });
88+
if (txParams.recipients !== undefined && explainedTx) {
89+
const filteredRecipients = txParams.recipients?.map((recipient) => {
90+
const recipientData = {
91+
address: recipient.address,
92+
amount: BigInt(recipient.amount),
93+
};
94+
if (recipient.memo) {
95+
recipientData['memo'] = recipient.memo;
96+
}
97+
if (recipient.tokenName) {
98+
recipientData['tokenName'] = recipient.tokenName;
99+
}
100+
return recipientData;
101+
});
102+
const filteredOutputs = explainedTx.outputs.map((output) => {
103+
const recipientData = {
104+
address: output.address,
105+
amount: BigInt(output.amount),
106+
};
107+
if (output.memo) {
108+
recipientData['memo'] = output.memo;
109+
}
110+
if (output.tokenName) {
111+
recipientData['tokenName'] = output.tokenName;
112+
}
113+
return recipientData;
114+
});
115+
if (!_.isEqual(filteredOutputs, filteredRecipients)) {
116+
throw new Error('Tx outputs does not match with expected txParams recipients');
117+
}
118+
let totalAmount = new BigNumber(0);
119+
for (const recipients of txParams.recipients) {
120+
totalAmount = totalAmount.plus(recipients.amount);
121+
}
122+
if (!totalAmount.isEqualTo(explainedTx.outputAmount)) {
123+
throw new Error('Tx total amount does not match with expected total amount field');
124+
}
125+
}
126+
return true;
127+
}
62128
}

modules/sdk-coin-stx/src/stx.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ import {
1010
} from '@bitgo/sdk-core';
1111
import { BaseCoin as StaticsBaseCoin, CoinFamily, coins } from '@bitgo/statics';
1212
import { cvToString, cvToValue } from '@stacks/transactions';
13+
1314
import { ExplainTransactionOptions, StxSignTransactionOptions, StxTransactionExplanation } from './types';
1415
import { StxLib } from '.';
16+
import { TransactionBuilderFactory } from './lib';
17+
import { TransactionBuilder } from './lib/transactionBuilder';
18+
import { findTokenNameByContract } from './lib/utils';
1519

1620
export class Stx extends BaseCoin {
1721
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
@@ -46,6 +50,10 @@ export class Stx extends BaseCoin {
4650
return Math.pow(10, this._staticsCoin.decimalPlaces);
4751
}
4852

53+
getTransaction(coinConfig: Readonly<StaticsBaseCoin>): TransactionBuilder {
54+
return new TransactionBuilderFactory(coinConfig).getTransferBuilder();
55+
}
56+
4957
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
5058
const { txParams } = params;
5159
if (Array.isArray(txParams.recipients) && txParams.recipients.length > 1) {
@@ -192,6 +200,7 @@ export class Stx extends BaseCoin {
192200
transactionRecipient = {
193201
address: cvToString(txJson.payload.functionArgs[1]),
194202
amount: outputAmount,
203+
tokenName: findTokenNameByContract(txJson.payload.contractAddress, txJson.payload.contractName),
195204
};
196205
if (txJson.payload.functionArgs.length === 4) {
197206
memo = txJson.payload.functionArgs[3].buffer.toString('ascii');

modules/sdk-coin-stx/test/fixtures.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { ITransactionRecipient } from '@bitgo/sdk-core';
2+
13
export const txForExplainTransfer =
24
'0x80800000000400164247d6f2b425ac5771423ae6c80c754f7172b0000000000000000000000000000000b40000ab3fcf8d6d697d01c3da3a20b06cd8b36f016755cb7637c3eb6b228980d8857c7826a8ea498e52fd73ecd8e3869881c9aba49ae598dfbf1d5af69b1c02b4f2bd03020000000000051a1ae3f911d8f1d46d7416bfbe4b593fd41eac19cb00000000000003e85468697320697320616e206578616d706c6500000000000000000000000000000000';
35

@@ -39,6 +41,7 @@ export const fungibleTokenTransferTx = {
3941
fee: '180',
4042
contractAddress: 'STAG18E45W613FZ3H4ZMF6QHH426EXM5QTSAVWYH',
4143
contractName: 'tsip6dp-token',
44+
tokenName: 'tstx:tsip6dp',
4245
functionName: 'transfer',
4346
functionArgs: [
4447
{
@@ -61,3 +64,12 @@ export const fungibleTokenTransferTx = {
6164
{ type: 2 },
6265
],
6366
};
67+
68+
export const recipients: ITransactionRecipient[] = [
69+
{
70+
address: 'SN2NN1JP9AEP5BVE19RNJ6T2MP7NDGRZYST1VDF3M',
71+
amount: '10000',
72+
memo: '1',
73+
tokenName: 'tstx:tsip6dp',
74+
},
75+
];
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import _ from 'lodash';
2+
import { BitGoAPI } from '@bitgo/sdk-api';
3+
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
4+
import { BaseCoin, ITransactionRecipient, Wallet } from '@bitgo/sdk-core';
5+
6+
import { Sip10Token } from '../../src';
7+
import * as testData from '../fixtures';
8+
9+
describe('Sip10Token:', function () {
10+
const sip10TokenName = 'tstx:tsip6dp';
11+
let bitgo: TestBitGoAPI;
12+
let basecoin: BaseCoin;
13+
let newTxPrebuild: () => { txHex: string; txInfo: Record<string, unknown> };
14+
let newTxParams: () => { recipients: ITransactionRecipient[] };
15+
let wallet: Wallet;
16+
17+
const txPreBuild = {
18+
txHex: testData.txForExplainFungibleTokenTransfer,
19+
txInfo: {},
20+
};
21+
22+
const txParams = {
23+
recipients: testData.recipients,
24+
};
25+
26+
before(function () {
27+
bitgo = TestBitGo.decorate(BitGoAPI, {
28+
env: 'mock',
29+
});
30+
bitgo.initializeTestVars();
31+
Sip10Token.createTokenConstructors().forEach(({ name, coinConstructor }) => {
32+
bitgo.safeRegister(name, coinConstructor);
33+
});
34+
newTxPrebuild = () => {
35+
return _.cloneDeep(txPreBuild);
36+
};
37+
newTxParams = () => {
38+
return _.cloneDeep(txParams);
39+
};
40+
basecoin = bitgo.coin(sip10TokenName);
41+
wallet = new Wallet(bitgo, basecoin, {});
42+
});
43+
44+
describe('Verify Transaction', function () {
45+
it('should succeed to verify transaction', async function () {
46+
const txPrebuild = newTxPrebuild();
47+
const txParams = newTxParams();
48+
const verification = {};
49+
const isTransactionVerified = await basecoin.verifyTransaction({
50+
txParams,
51+
txPrebuild,
52+
verification,
53+
wallet,
54+
});
55+
isTransactionVerified.should.equal(true);
56+
});
57+
58+
it('should succeed to verify transaction when recipients amount are numbers', async function () {
59+
const txPrebuild = newTxPrebuild();
60+
const txParamsWithNumberAmounts = newTxParams();
61+
txParamsWithNumberAmounts.recipients = txParamsWithNumberAmounts.recipients.map(
62+
({ address, amount, memo, tokenName }) => {
63+
return { address, amount: Number(amount), memo, tokenName };
64+
}
65+
);
66+
const verification = {};
67+
const isTransactionVerified = await basecoin.verifyTransaction({
68+
txParams: txParamsWithNumberAmounts,
69+
txPrebuild,
70+
verification,
71+
wallet,
72+
});
73+
isTransactionVerified.should.equal(true);
74+
});
75+
76+
it('should fail to verify transaction with no recipients', async function () {
77+
const txPrebuild = {};
78+
const txParams = newTxParams();
79+
txParams.recipients = [];
80+
await basecoin
81+
.verifyTransaction({
82+
txParams,
83+
txPrebuild,
84+
wallet,
85+
})
86+
.should.rejectedWith('missing required tx prebuild property txHex');
87+
});
88+
89+
it('should fail when more than 1 recipients are passed', async function () {
90+
const txPrebuild = newTxPrebuild();
91+
const txParams = newTxParams();
92+
txParams.recipients.push({
93+
address: 'SN2NN1JP9AEP5BVE19RNJ6T2MP7NDGRZYST1VDF3N',
94+
amount: '10000',
95+
memo: '1',
96+
tokenName: 'tsip6dp-token',
97+
});
98+
await basecoin
99+
.verifyTransaction({
100+
txParams,
101+
txPrebuild,
102+
wallet,
103+
})
104+
.should.rejectedWith(
105+
"tstx:tsip6dp doesn't support sending to more than 1 destination address within a single transaction. Try again, using only a single recipient."
106+
);
107+
});
108+
109+
it('should fail to verify transaction with wrong address', async function () {
110+
const txPrebuild = newTxPrebuild();
111+
const txParams = newTxParams();
112+
txParams.recipients[0].address = 'SN2NN1JP9AEP5BVE19RNJ6T2MP7NDGRZYST1VDF3N';
113+
const verification = {};
114+
await basecoin
115+
.verifyTransaction({
116+
txParams,
117+
txPrebuild,
118+
verification,
119+
wallet,
120+
})
121+
.should.rejectedWith('Tx outputs does not match with expected txParams recipients');
122+
});
123+
124+
it('should fail to verify transaction with wrong amount', async function () {
125+
const txPrebuild = newTxPrebuild();
126+
const txParams = newTxParams();
127+
txParams.recipients[0].amount = '100';
128+
const verification = {};
129+
await basecoin
130+
.verifyTransaction({
131+
txParams,
132+
txPrebuild,
133+
verification,
134+
wallet,
135+
})
136+
.should.rejectedWith('Tx outputs does not match with expected txParams recipients');
137+
});
138+
139+
it('should fail to verify transaction with wrong memo', async function () {
140+
const txPrebuild = newTxPrebuild();
141+
const txParams = newTxParams();
142+
txParams.recipients[0].memo = '2';
143+
const verification = {};
144+
await basecoin
145+
.verifyTransaction({
146+
txParams,
147+
txPrebuild,
148+
verification,
149+
wallet,
150+
})
151+
.should.rejectedWith('Tx outputs does not match with expected txParams recipients');
152+
});
153+
154+
it('should fail to verify transaction with wrong token', async function () {
155+
const txPrebuild = newTxPrebuild();
156+
const txParams = newTxParams();
157+
txParams.recipients[0].tokenName = 'tstx:tsip8dp';
158+
const verification = {};
159+
await basecoin
160+
.verifyTransaction({
161+
txParams,
162+
txPrebuild,
163+
verification,
164+
wallet,
165+
})
166+
.should.rejectedWith('Tx outputs does not match with expected txParams recipients');
167+
});
168+
});
169+
});

modules/sdk-coin-stx/test/unit/stx.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ describe('STX:', function () {
205205
explain.outputs[0].amount.should.equal(testData.fungibleTokenTransferTx.functionArgs[2].value);
206206
explain.outputs[0].address.should.equal(cvToString(testData.fungibleTokenTransferTx.functionArgs[1]));
207207
explain.outputs[0].memo.should.equal('1');
208+
explain.outputs[0].tokenName.should.equal(testData.fungibleTokenTransferTx.tokenName);
208209
});
209210

210211
describe('Keypairs:', () => {

0 commit comments

Comments
 (0)