Skip to content

Commit 19cb74c

Browse files
committed
feat(sdk-coin-near): added verify transaction for near token & storage
deposit transactions Ticket: COIN-4150
1 parent 5f8e582 commit 19cb74c

File tree

9 files changed

+382
-51
lines changed

9 files changed

+382
-51
lines changed

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

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -341,12 +341,12 @@ export class Transaction extends BaseTransaction {
341341
if (action.functionCall.methodName === 'ft_transfer') {
342342
const parsedArgs = JSON.parse(Buffer.from(action.functionCall.args).toString());
343343
inputs.push({
344-
address: parsedArgs.receiver_id,
344+
address: this._nearTransaction.signerId,
345345
value: parsedArgs.amount,
346346
coin: this._coinConfig.name,
347347
});
348348
outputs.push({
349-
address: this._nearTransaction.signerId,
349+
address: parsedArgs.receiver_id,
350350
value: parsedArgs.amount,
351351
coin: this._coinConfig.name,
352352
});
@@ -355,15 +355,15 @@ export class Transaction extends BaseTransaction {
355355
const parsedArgs = JSON.parse(Buffer.from(action.functionCall.args).toString());
356356
const receiverId = parsedArgs.account_id ? parsedArgs.account_id : this._nearTransaction.signerId;
357357
inputs.push({
358-
address: receiverId,
358+
address: this._nearTransaction.signerId,
359359
value: action.functionCall.deposit.toString(),
360360
coin:
361361
this._coinConfig.network.type === NetworkType.TESTNET
362362
? `t${this._coinConfig.family}`
363363
: this._coinConfig.family,
364364
});
365365
outputs.push({
366-
address: this._nearTransaction.signerId,
366+
address: receiverId,
367367
value: action.functionCall.deposit.toString(),
368368
coin:
369369
this._coinConfig.network.type === NetworkType.TESTNET
@@ -408,15 +408,15 @@ export class Transaction extends BaseTransaction {
408408
const parsedArgs = JSON.parse(Buffer.from(action.functionCall.args).toString());
409409
const receiverId = parsedArgs.account_id ? parsedArgs.account_id : this._nearTransaction.signerId;
410410
inputs.push({
411-
address: receiverId,
411+
address: this._nearTransaction.signerId,
412412
value: action.functionCall.deposit.toString(),
413413
coin:
414414
this._coinConfig.network.type === NetworkType.TESTNET
415415
? `t${this._coinConfig.family}`
416416
: this._coinConfig.family,
417417
});
418418
outputs.push({
419-
address: this._nearTransaction.signerId,
419+
address: receiverId,
420420
value: action.functionCall.deposit.toString(),
421421
coin:
422422
this._coinConfig.network.type === NetworkType.TESTNET
@@ -522,9 +522,9 @@ export class Transaction extends BaseTransaction {
522522
}
523523
}
524524
}
525-
if (hasFtTransfer && hasStorageDeposit) {
525+
if (hasFtTransfer) {
526526
return totalTokenAmount.toString();
527-
} else if (!hasFtTransfer && hasStorageDeposit) {
527+
} else if (hasStorageDeposit) {
528528
return totalNearDeposit.toString();
529529
}
530530
return '';
@@ -545,10 +545,17 @@ export class Transaction extends BaseTransaction {
545545
const functionCall = action.functionCall;
546546
if (functionCall.methodName === FT_TRANSFER) {
547547
const amountStr = functionCall.args['amount'] as string;
548-
outputs.push({
549-
address: json.receiverId,
548+
const receiverId = functionCall.args['receiver_id'] as string;
549+
// in ft transfer, the outer receiver id will be contract address of the token
550+
const tokenName = utils.findTokenNameFromContractAddress(json.receiverId);
551+
const output: ITransactionRecipient = {
552+
address: receiverId,
550553
amount: amountStr,
551-
});
554+
};
555+
if (tokenName) {
556+
output.tokenName = tokenName;
557+
}
558+
outputs.push(output);
552559
}
553560
}
554561
});
@@ -573,10 +580,20 @@ export class Transaction extends BaseTransaction {
573580
if (action.functionCall) {
574581
const functionCall = action.functionCall;
575582
if (functionCall.methodName === STORAGE_DEPOSIT) {
576-
outputs.push({
577-
address: json.receiverId,
583+
const receiverId =
584+
functionCall.args && functionCall.args['account_id']
585+
? (functionCall.args['account_id'] as string)
586+
: json.signerId;
587+
// in storage deposit, the outer receiver id will be contract address of the token
588+
const tokenName = utils.findTokenNameFromContractAddress(json.receiverId);
589+
const output: ITransactionRecipient = {
590+
address: receiverId,
578591
amount: functionCall.deposit,
579-
});
592+
};
593+
if (tokenName) {
594+
output.tokenName = tokenName;
595+
}
596+
outputs.push(output);
580597
}
581598
}
582599
});

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import bs58 from 'bs58';
2+
13
import { BaseUtils, isBase58 } from '@bitgo/sdk-core';
4+
import { coins, Nep141Token } from '@bitgo/statics';
5+
26
import { KeyPair } from './keyPair';
3-
import bs58 from 'bs58';
47

58
export class Utils implements BaseUtils {
69
/** @inheritdoc */
@@ -82,6 +85,32 @@ export class Utils implements BaseUtils {
8285
return false;
8386
}
8487
}
88+
89+
/**
90+
* Find the bitgo token name using contract address
91+
*
92+
* @param {String} contractAddress the token contract address
93+
* @returns {String} token name
94+
*/
95+
findTokenNameFromContractAddress(contractAddress: string): string | undefined {
96+
const token = coins
97+
.filter((coin) => coin instanceof Nep141Token && coin.contractAddress === contractAddress)
98+
.map((coin) => coin as Nep141Token);
99+
return token ? token[0].name : undefined;
100+
}
101+
102+
/**
103+
* Find the token instance using the bitgo token name
104+
*
105+
* @param {String} tokenName the bitgo name of the token
106+
* @returns {Nep141Token|undefined} token instance if found
107+
*/
108+
getTokenInstanceFromTokenName(tokenName: string): Nep141Token | undefined {
109+
const token = coins
110+
.filter((coin) => coin instanceof Nep141Token && coin.name === tokenName)
111+
.map((coin) => coin as Nep141Token);
112+
return token ? token[0] : undefined;
113+
}
85114
}
86115

87116
const utils = new Utils();

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

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -755,16 +755,42 @@ export class Near extends BaseCoin {
755755

756756
// users do not input recipients for consolidation requests as they are generated by the server
757757
if (txParams.recipients !== undefined) {
758-
const filteredRecipients = txParams.recipients?.map((recipient) => _.pick(recipient, ['address', 'amount']));
759-
const filteredOutputs = explainedTx.outputs.map((output) => _.pick(output, ['address', 'amount']));
758+
if (txParams.type === 'enabletoken') {
759+
const tokenName = explainedTx.outputs[0].tokenName;
760+
if (tokenName) {
761+
const nepToken = nearUtils.getTokenInstanceFromTokenName(tokenName);
762+
if (nepToken) {
763+
explainedTx.outputs.forEach((output) => {
764+
if (output.amount !== nepToken.storageDepositAmount) {
765+
throw new Error('Storage deposit amount not matching!');
766+
}
767+
});
768+
}
769+
}
770+
}
771+
772+
const filteredRecipients = txParams.recipients?.map((recipient) => {
773+
if (txParams.type !== 'enabletoken') {
774+
return _.pick(recipient, ['address', 'amount']);
775+
} else {
776+
return _.pick(recipient, ['address', 'tokenName']);
777+
}
778+
});
779+
const filteredOutputs = explainedTx.outputs.map((output) => {
780+
if (txParams.type !== 'enabletoken') {
781+
return _.pick(output, ['address', 'amount']);
782+
} else {
783+
return _.pick(output, ['address', 'tokenName']);
784+
}
785+
});
760786

761787
if (!_.isEqual(filteredOutputs, filteredRecipients)) {
762788
throw new Error('Tx outputs does not match with expected txParams recipients');
763789
}
764790
for (const recipients of txParams.recipients) {
765-
totalAmount = totalAmount.plus(recipients.amount);
791+
totalAmount = txParams.type !== 'enabletoken' ? totalAmount.plus(recipients.amount) : BigNumber(0);
766792
}
767-
if (!totalAmount.isEqualTo(explainedTx.outputAmount)) {
793+
if (!totalAmount.isEqualTo(explainedTx.outputAmount) && txParams.type !== 'enabletoken') {
768794
throw new Error('Tx total amount does not match with expected total amount field');
769795
}
770796
}

modules/sdk-coin-near/src/nep141Token.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { BitGoBase, CoinConstructor, NamedCoinConstructor } from '@bitgo/sdk-core';
1+
import BigNumber from 'bignumber.js';
2+
import { BitGoBase, CoinConstructor, NamedCoinConstructor, VerifyTransactionOptions } from '@bitgo/sdk-core';
23
import { coins, Nep141TokenConfig, NetworkType, tokens } from '@bitgo/statics';
34

5+
import { Transaction } from './lib';
46
import { Near } from './near';
57

68
export class Nep141Token extends Near {
@@ -62,4 +64,46 @@ export class Nep141Token extends Near {
6264
getBaseFactor(): number {
6365
return Math.pow(10, this.tokenConfig.decimalPlaces);
6466
}
67+
68+
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
69+
const { txPrebuild: txPrebuild, txParams: txParams } = params;
70+
const rawTx = txPrebuild.txHex;
71+
let totalAmount = new BigNumber(0);
72+
if (!rawTx) {
73+
throw new Error('missing required tx prebuild property txHex');
74+
}
75+
const coinConfig = coins.get(this.getChain());
76+
const transaction = new Transaction(coinConfig);
77+
transaction.fromRawTransaction(rawTx);
78+
const explainedTx = transaction.explainTransaction();
79+
if (txParams.recipients !== undefined) {
80+
txParams.recipients.forEach((recipient) => {
81+
if (recipient.tokenName && recipient.tokenName !== coinConfig.name) {
82+
throw new Error('incorrect token name specified in recipients');
83+
}
84+
recipient.tokenName = coinConfig.name;
85+
});
86+
const filteredRecipients = txParams.recipients?.map((recipient) => ({
87+
address: recipient.address,
88+
amount: recipient.amount,
89+
tokenName: recipient.tokenName,
90+
}));
91+
const filteredOutputs = explainedTx.outputs.map((output) => ({
92+
address: output.address,
93+
amount: output.amount,
94+
tokenName: output.tokenName,
95+
}));
96+
const outputsMatch = JSON.stringify(filteredRecipients) === JSON.stringify(filteredOutputs);
97+
if (!outputsMatch) {
98+
throw new Error('Tx outputs does not match with expected txParams recipients');
99+
}
100+
for (const recipient of txParams.recipients) {
101+
totalAmount = totalAmount.plus(recipient.amount);
102+
}
103+
if (!totalAmount.isEqualTo(explainedTx.outputAmount)) {
104+
throw new Error('Tx total amount does not match with expected total amount field');
105+
}
106+
}
107+
return true;
108+
}
65109
}

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,30 @@ export const rawTx = {
9898
unsigned:
9999
'QAAAADYxYjE4YzZkYzAyZGRjYWJkZWFjNTZjYjRmMjFhOTcxY2M0MWNjOTc2NDBmNmY4NWIwNzM0ODAwMDhjNTNhMGQAYbGMbcAt3KverFbLTyGpccxBzJdkD2+FsHNIAAjFOg0BAAAAAAAAABMAAABmdC10bmVwMjRkcC50ZXN0bmV0ppNL00/j8LLRb+dQg6da599fp9XXZsr3QyxL4aKNJmABAAAAAgsAAABmdF90cmFuc2Zlcm8AAAB7InJlY2VpdmVyX2lkIjoiOWY3YjA2NzVkYjU5ZDE5YjRiZDljOGM3MmVhYWJiYTc1YTk4NjNkMDJiMzAxMTViOGIzYzNjYTVjMjBmMDI1NCIsImFtb3VudCI6IjEwMCIsIm1lbW8iOiJ0ZXN0In0A0JjUr3EAAAEAAAAAAAAAAAAAAAAAAAA=',
100100
},
101-
storageDeposit: {
101+
fungibleTokenTransferWithStorageDeposit: {
102+
signed:
103+
'QAAAADYxYjE4YzZkYzAyZGRjYWJkZWFjNTZjYjRmMjFhOTcxY2M0MWNjOTc2NDBmNmY4NWIwNzM0ODAwMDhjNTNhMGQAYbGMbcAt3KverFbLTyGpccxBzJdkD2+FsHNIAAjFOg0BAAAAAAAAABMAAABmdC10bmVwMjRkcC50ZXN0bmV0ppNL00/j8LLRb+dQg6da599fp9XXZsr3QyxL4aKNJmACAAAAAg8AAABzdG9yYWdlX2RlcG9zaXRRAAAAeyJhY2NvdW50X2lkIjoiOWY3YjA2NzVkYjU5ZDE5YjRiZDljOGM3MmVhYWJiYTc1YTk4NjNkMDJiMzAxMTViOGIzYzNjYTVjMjBmMDI1NCJ9ANCY1K9xAAAAAEhWNxk8w0MAAAAAAAAAAgsAAABmdF90cmFuc2Zlcm8AAAB7InJlY2VpdmVyX2lkIjoiOWY3YjA2NzVkYjU5ZDE5YjRiZDljOGM3MmVhYWJiYTc1YTk4NjNkMDJiMzAxMTViOGIzYzNjYTVjMjBmMDI1NCIsImFtb3VudCI6IjEwMCIsIm1lbW8iOiJ0ZXN0In0A0JjUr3EAAAEAAAAAAAAAAAAAAAAAAAAAZrNr09FzkuyOL31vzY7AgLbkIGn1DkoQ4TkljJFrMk6zBP/eC9qzRl4sWhbwR6WbUDoA3DYPApkW5wwCG07CAw==',
104+
unsigned:
105+
'QAAAADYxYjE4YzZkYzAyZGRjYWJkZWFjNTZjYjRmMjFhOTcxY2M0MWNjOTc2NDBmNmY4NWIwNzM0ODAwMDhjNTNhMGQAYbGMbcAt3KverFbLTyGpccxBzJdkD2+FsHNIAAjFOg0BAAAAAAAAABMAAABmdC10bmVwMjRkcC50ZXN0bmV0ppNL00/j8LLRb+dQg6da599fp9XXZsr3QyxL4aKNJmACAAAAAg8AAABzdG9yYWdlX2RlcG9zaXRRAAAAeyJhY2NvdW50X2lkIjoiOWY3YjA2NzVkYjU5ZDE5YjRiZDljOGM3MmVhYWJiYTc1YTk4NjNkMDJiMzAxMTViOGIzYzNjYTVjMjBmMDI1NCJ9ANCY1K9xAAAAAEhWNxk8w0MAAAAAAAAAAgsAAABmdF90cmFuc2Zlcm8AAAB7InJlY2VpdmVyX2lkIjoiOWY3YjA2NzVkYjU5ZDE5YjRiZDljOGM3MmVhYWJiYTc1YTk4NjNkMDJiMzAxMTViOGIzYzNjYTVjMjBmMDI1NCIsImFtb3VudCI6IjEwMCIsIm1lbW8iOiJ0ZXN0In0A0JjUr3EAAAEAAAAAAAAAAAAAAAAAAAA=',
106+
},
107+
fungibleTokenTransferWithSelfStorageDeposit: {
108+
signed:
109+
'QAAAADYxYjE4YzZkYzAyZGRjYWJkZWFjNTZjYjRmMjFhOTcxY2M0MWNjOTc2NDBmNmY4NWIwNzM0ODAwMDhjNTNhMGQAYbGMbcAt3KverFbLTyGpccxBzJdkD2+FsHNIAAjFOg0BAAAAAAAAABMAAABmdC10bmVwMjRkcC50ZXN0bmV0ppNL00/j8LLRb+dQg6da599fp9XXZsr3QyxL4aKNJmACAAAAAg8AAABzdG9yYWdlX2RlcG9zaXQCAAAAe30A0JjUr3EAAAAASFY3GTzDQwAAAAAAAAACCwAAAGZ0X3RyYW5zZmVybwAAAHsicmVjZWl2ZXJfaWQiOiI5ZjdiMDY3NWRiNTlkMTliNGJkOWM4YzcyZWFhYmJhNzVhOTg2M2QwMmIzMDExNWI4YjNjM2NhNWMyMGYwMjU0IiwiYW1vdW50IjoiMTAwIiwibWVtbyI6InRlc3QifQDQmNSvcQAAAQAAAAAAAAAAAAAAAAAAAADSMrQ6WXa5kQmbCXifpMwCf+umrU4ckIHRrFJYLUQkCrnu3PXB10ehMmej1eGBIs3Jbu3PWl+CTRFfgAJk2FUC',
110+
unsigned:
111+
'QAAAADYxYjE4YzZkYzAyZGRjYWJkZWFjNTZjYjRmMjFhOTcxY2M0MWNjOTc2NDBmNmY4NWIwNzM0ODAwMDhjNTNhMGQAYbGMbcAt3KverFbLTyGpccxBzJdkD2+FsHNIAAjFOg0BAAAAAAAAABMAAABmdC10bmVwMjRkcC50ZXN0bmV0ppNL00/j8LLRb+dQg6da599fp9XXZsr3QyxL4aKNJmACAAAAAg8AAABzdG9yYWdlX2RlcG9zaXQCAAAAe30A0JjUr3EAAAAASFY3GTzDQwAAAAAAAAACCwAAAGZ0X3RyYW5zZmVybwAAAHsicmVjZWl2ZXJfaWQiOiI5ZjdiMDY3NWRiNTlkMTliNGJkOWM4YzcyZWFhYmJhNzVhOTg2M2QwMmIzMDExNWI4YjNjM2NhNWMyMGYwMjU0IiwiYW1vdW50IjoiMTAwIiwibWVtbyI6InRlc3QifQDQmNSvcQAAAQAAAAAAAAAAAAAAAAAAAA==',
112+
},
113+
selfStorageDeposit: {
102114
signed:
103115
'QAAAADYxYjE4YzZkYzAyZGRjYWJkZWFjNTZjYjRmMjFhOTcxY2M0MWNjOTc2NDBmNmY4NWIwNzM0ODAwMDhjNTNhMGQAYbGMbcAt3KverFbLTyGpccxBzJdkD2+FsHNIAAjFOg0BAAAAAAAAABMAAABmdC10bmVwMjRkcC50ZXN0bmV0ppNL00/j8LLRb+dQg6da599fp9XXZsr3QyxL4aKNJmABAAAAAg8AAABzdG9yYWdlX2RlcG9zaXQCAAAAe30A0JjUr3EAAAAASFY3GTzDQwAAAAAAAAAAqH6xLHXPvsld56HLmtj16VGVHtxSY3TSUyVKpKOy2IBgRr4A2c7eyo8l/v6/KfXS3bOIFrIPX/zWw4qqaL7mAw==',
104116
unsigned:
105117
'QAAAADYxYjE4YzZkYzAyZGRjYWJkZWFjNTZjYjRmMjFhOTcxY2M0MWNjOTc2NDBmNmY4NWIwNzM0ODAwMDhjNTNhMGQAYbGMbcAt3KverFbLTyGpccxBzJdkD2+FsHNIAAjFOg0BAAAAAAAAABMAAABmdC10bmVwMjRkcC50ZXN0bmV0ppNL00/j8LLRb+dQg6da599fp9XXZsr3QyxL4aKNJmABAAAAAg8AAABzdG9yYWdlX2RlcG9zaXQCAAAAe30A0JjUr3EAAAAASFY3GTzDQwAAAAAAAAA=',
106118
},
119+
storageDeposit: {
120+
signed:
121+
'QAAAADYxYjE4YzZkYzAyZGRjYWJkZWFjNTZjYjRmMjFhOTcxY2M0MWNjOTc2NDBmNmY4NWIwNzM0ODAwMDhjNTNhMGQAYbGMbcAt3KverFbLTyGpccxBzJdkD2+FsHNIAAjFOg0BAAAAAAAAABMAAABmdC10bmVwMjRkcC50ZXN0bmV0ppNL00/j8LLRb+dQg6da599fp9XXZsr3QyxL4aKNJmABAAAAAg8AAABzdG9yYWdlX2RlcG9zaXRRAAAAeyJhY2NvdW50X2lkIjoiOWY3YjA2NzVkYjU5ZDE5YjRiZDljOGM3MmVhYWJiYTc1YTk4NjNkMDJiMzAxMTViOGIzYzNjYTVjMjBmMDI1NCJ9ANCY1K9xAAAAAEhWNxk8w0MAAAAAAAAAAHdbWPSf9BSFVxv1j5g4JdnRxPXuz7UFNAttCDM7KzwH0UW0uUvBsRd4OikOUQD+RSocvwstCGh2kwNc738xSgs=',
122+
unsigned:
123+
'QAAAADYxYjE4YzZkYzAyZGRjYWJkZWFjNTZjYjRmMjFhOTcxY2M0MWNjOTc2NDBmNmY4NWIwNzM0ODAwMDhjNTNhMGQAYbGMbcAt3KverFbLTyGpccxBzJdkD2+FsHNIAAjFOg0BAAAAAAAAABMAAABmdC10bmVwMjRkcC50ZXN0bmV0ppNL00/j8LLRb+dQg6da599fp9XXZsr3QyxL4aKNJmABAAAAAg8AAABzdG9yYWdlX2RlcG9zaXRRAAAAeyJhY2NvdW50X2lkIjoiOWY3YjA2NzVkYjU5ZDE5YjRiZDljOGM3MmVhYWJiYTc1YTk4NjNkMDJiMzAxMTViOGIzYzNjYTVjMjBmMDI1NCJ9ANCY1K9xAAAAAEhWNxk8w0MAAAAAAAAA',
124+
},
107125
};
108126

109127
export const AMOUNT = '1000000000000000000000000';

0 commit comments

Comments
 (0)