Skip to content

Commit 69cca5b

Browse files
committed
feat(sdk-coin-apt): addition of test case for apt transaction builder
Ticket: COIN-2258
1 parent 6857221 commit 69cca5b

File tree

5 files changed

+347
-5
lines changed

5 files changed

+347
-5
lines changed

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

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@ import {
1111
VerifyAddressOptions,
1212
VerifyTransactionOptions,
1313
} from '@bitgo/sdk-core';
14-
import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
15-
import { KeyPair as AptKeyPair } from './lib';
14+
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
15+
import { KeyPair as AptKeyPair, TransactionBuilderFactory, TransferTransaction } from './lib';
1616
import utils from './lib/utils';
17+
import * as _ from 'lodash';
18+
import BigNumber from 'bignumber.js';
19+
20+
export interface AptParseTransactionOptions extends ParseTransactionOptions {
21+
txHex: string;
22+
}
1723

1824
export class Apt extends BaseCoin {
1925
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
@@ -64,7 +70,40 @@ export class Apt extends BaseCoin {
6470
}
6571

6672
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
67-
throw new Error('Method not implemented.');
73+
const coinConfig = coins.get(this.getChain());
74+
const { txPrebuild: txPrebuild, txParams: txParams } = params;
75+
const transaction = new TransferTransaction(coinConfig);
76+
const rawTx = txPrebuild.txHex;
77+
if (!rawTx) {
78+
throw new Error('missing required tx prebuild property txHex');
79+
}
80+
transaction.fromRawTransaction(rawTx);
81+
const explainedTx = transaction.explainTransaction();
82+
if (txParams.recipients !== undefined) {
83+
const filteredRecipients = txParams.recipients?.map((recipient) => {
84+
return {
85+
address: recipient.address, // TODO: check this
86+
amount: BigInt(recipient.amount),
87+
};
88+
});
89+
const filteredOutputs = explainedTx.outputs.map((output) => {
90+
return {
91+
address: output.address,
92+
amount: BigInt(output.amount),
93+
};
94+
});
95+
if (!_.isEqual(filteredOutputs, filteredRecipients)) {
96+
throw new Error('Tx outputs does not match with expected txParams recipients');
97+
}
98+
let totalAmount = new BigNumber(0);
99+
for (const recipients of txParams.recipients) {
100+
totalAmount = totalAmount.plus(recipients.amount);
101+
}
102+
if (!totalAmount.isEqualTo(explainedTx.outputAmount)) {
103+
throw new Error('Tx total amount does not match with expected total amount field');
104+
}
105+
}
106+
return true;
68107
}
69108

70109
async isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
@@ -76,8 +115,26 @@ export class Apt extends BaseCoin {
76115
return true;
77116
}
78117

79-
parseTransaction(params: ParseTransactionOptions): Promise<ParsedTransaction> {
80-
throw new Error('Method not implemented.');
118+
async parseTransaction(params: AptParseTransactionOptions): Promise<ParsedTransaction> {
119+
const coinConfig = coins.get(this.getChain());
120+
const factory = new TransactionBuilderFactory(coinConfig);
121+
const transactionBuilder = factory.from(params.txHex);
122+
const rebuiltTransaction = await transactionBuilder.build();
123+
const parsedTransaction = rebuiltTransaction.toJson();
124+
return {
125+
inputs: [
126+
{
127+
address: parsedTransaction.sender,
128+
value: parsedTransaction.recipient.amount,
129+
},
130+
],
131+
outputs: [
132+
{
133+
address: parsedTransaction.recipient.address,
134+
value: parsedTransaction.recipient.amount,
135+
},
136+
],
137+
};
81138
}
82139

83140
generateKeyPair(seed?: Buffer): KeyPair {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export abstract class Transaction extends BaseTransaction {
155155

156156
/** @inheritDoc */
157157
explainTransaction(): TransactionExplanation {
158+
//TODO: explain transaction to take input as params then take build txn and return the explanation
158159
const displayOrder = ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'withdrawAmount'];
159160

160161
const outputs: TransactionRecipient[] = [this.recipient];
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Recipient } from '@bitgo/sdk-core';
2+
3+
export const AMOUNT = 100000000;
4+
5+
export const addresses = {
6+
validAddresses: [
7+
'0xf7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad9',
8+
'0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a372449',
9+
],
10+
invalidAddresses: [
11+
'randomString',
12+
'0xc4173a804406a365e69dfb297ddfgsdcvf',
13+
'5ne7phA48Jrvpn39AtupB8ZkCCAy8gLTfpGihZPuDqen',
14+
],
15+
};
16+
17+
export const sender = {
18+
address: '0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a372449',
19+
privateKey: '0x54e1a88eb2b881fe6c222985d0fabc9ba3aca35743dd1d19db18a9a355acbaaa',
20+
publicKey: '0xf73836f42257240e43d439552471fc9dbcc3f1af5bd0b4ed83f44b5f66146442',
21+
};
22+
23+
export const recipients: Recipient[] = [
24+
{
25+
address: addresses.validAddresses[0],
26+
amount: AMOUNT.toString(),
27+
},
28+
];
29+
30+
export const TRANSFER =
31+
'0xc8f02d25aa698b3e9fbd8a08e8da4c8ee261832a25a4cde8731b5ec356537d09170000000000000002000000000000000000000000000000000000000000000000000000000000000104636f696e087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010a6170746f735f636f696e094170746f73436f696e000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d0300000000006400000000000000207c7667000000000200202121dcd098069ae535697dd019cfd8677ca7aba0adac1d1959cbce6dc54b12594010f340ec153b724c4dc1c9a435d0fafed1775d851c1e8d965925a7879550c69a4677925d9198334a72ae7ce8998226ff0a83743c7ba8a2831136c072bf21c404';

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

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,208 @@ import 'should';
22
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
33
import { BitGoAPI } from '@bitgo/sdk-api';
44
import { Apt, Tapt } from '../../src';
5+
import * as testData from '../resources/apt';
6+
import _ from 'lodash';
7+
import BigNumber from 'bignumber.js';
8+
import sinon from 'sinon';
9+
import assert from 'assert';
510

611
describe('APT:', function () {
712
let bitgo: TestBitGoAPI;
13+
let basecoin;
14+
let newTxPrebuild;
15+
let newTxParams;
16+
17+
const txPreBuild = {
18+
txHex: testData.TRANSFER,
19+
txInfo: {},
20+
};
21+
22+
const txParams = {
23+
recipients: testData.recipients,
24+
};
825

926
before(function () {
1027
bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' });
1128
bitgo.safeRegister('apt', Apt.createInstance);
1229
bitgo.safeRegister('tapt', Tapt.createInstance);
1330
bitgo.initializeTestVars();
31+
basecoin = bitgo.coin('tapt');
32+
newTxPrebuild = () => {
33+
return _.cloneDeep(txPreBuild);
34+
};
35+
newTxParams = () => {
36+
return _.cloneDeep(txParams);
37+
};
38+
});
39+
40+
it('should return the right info', function () {
41+
const apt = bitgo.coin('apt');
42+
const tapt = bitgo.coin('tapt');
43+
44+
apt.getChain().should.equal('apt');
45+
apt.getFamily().should.equal('apt');
46+
apt.getFullName().should.equal('Aptos');
47+
apt.getBaseFactor().should.equal(1e8);
48+
49+
tapt.getChain().should.equal('tapt');
50+
tapt.getFamily().should.equal('apt');
51+
tapt.getFullName().should.equal('Testnet Aptos');
52+
tapt.getBaseFactor().should.equal(1e8);
53+
});
54+
55+
describe('Verify transaction: ', () => {
56+
it('should succeed to verify transaction', async function () {
57+
const txPrebuild = newTxPrebuild();
58+
const txParams = newTxParams();
59+
const verification = {};
60+
const isTransactionVerified = await basecoin.verifyTransaction({ txParams, txPrebuild, verification });
61+
isTransactionVerified.should.equal(true);
62+
});
63+
64+
it('should succeed to verify transaction when recipients amount are numbers', async function () {
65+
const txPrebuild = newTxPrebuild();
66+
const txParamsWithNumberAmounts = newTxParams();
67+
txParamsWithNumberAmounts.recipients = txParamsWithNumberAmounts.recipients.map(({ address, amount }) => {
68+
return { address, amount: Number(amount) };
69+
});
70+
const verification = {};
71+
const isTransactionVerified = await basecoin.verifyTransaction({
72+
txParams: txParamsWithNumberAmounts,
73+
txPrebuild,
74+
verification,
75+
});
76+
isTransactionVerified.should.equal(true);
77+
});
78+
79+
it('should fail to verify transaction with invalid param', async function () {
80+
const txPrebuild = {};
81+
const txParams = newTxParams();
82+
txParams.recipients = undefined;
83+
await basecoin
84+
.verifyTransaction({
85+
txParams,
86+
txPrebuild,
87+
})
88+
.should.rejectedWith('missing required tx prebuild property txHex');
89+
});
90+
});
91+
describe('Parse Transactions: ', () => {
92+
const transferInputsResponse = [
93+
{
94+
address: testData.recipients[0].address,
95+
amount: new BigNumber(testData.AMOUNT),
96+
},
97+
];
98+
99+
const transferOutputsResponse = [
100+
{
101+
address: testData.recipients[0].address,
102+
amount: testData.recipients[0].amount,
103+
},
104+
];
105+
106+
it('should parse a transfer transaction', async function () {
107+
const parsedTransaction = await basecoin.parseTransaction({
108+
txHex: testData.TRANSFER,
109+
});
110+
111+
parsedTransaction.should.deepEqual({
112+
inputs: transferInputsResponse,
113+
outputs: transferOutputsResponse,
114+
});
115+
});
116+
117+
it('should fail to parse a transfer transaction when explainTransaction response is undefined', async function () {
118+
const stub = sinon.stub(Apt.prototype, 'explainTransaction');
119+
stub.resolves(undefined);
120+
await basecoin.parseTransaction({ txHex: testData.TRANSFER }).should.be.rejectedWith('Invalid transaction');
121+
stub.restore();
122+
});
123+
});
124+
125+
describe('Address Validation', () => {
126+
let keychains;
127+
let commonKeychain;
128+
129+
before(function () {
130+
commonKeychain =
131+
'19bdfe2a4b498a05511381235a8892d54267807c4a3f654e310b938b8b424ff4adedbe92f4c146de641c67508a961324c8504cdf8e0c0acbb68d6104ccccd781';
132+
keychains = [
133+
{
134+
id: '6424c353eaf78d000766e95949868468',
135+
source: 'user',
136+
type: 'tss',
137+
commonKeychain:
138+
'19bdfe2a4b498a05511381235a8892d54267807c4a3f654e310b938b8b424ff4adedbe92f4c146de641c67508a961324c8504cdf8e0c0acbb68d6104ccccd781',
139+
encryptedPrv:
140+
'{"iv":"cZd5i7L4RxtwrALW2rK7UA==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"5zgoH1Bd3Fw=","ct":"9vVlnXFRtrM9FVEo+d2chbGHlM9lFZemueBuAs3BIkPo33Fo7jzwwNK/kIWkEyg+NmEBd5IaqAS157nvvvwzzsmMWlQdUz9qbmXNv3pg987cXFR08exS+4uhwP1YNOjJTRvRNcO9ZqHb46d4fmyJ/yC9/susCge7r/EsbaN5C3afv1dzybuq912FwaQElZLYYp5BICudFOMZ9k0UDMfKM/PMDkH7WexoGHr9GKq/bgCH2B39TZZyHKU6Uy47lXep2s6h0DrMwHOrnmiL3DZjOj88Ynvphlzxuo4eOlD2UHia2+nvIaISYs29Pr0DAvREutchvcBpExj1kWWPv7hQYrv8F0NAdatsbWl3w+xKyfiMKo1USlrwyJviypGtQtXOJyw0XPN0rv2+L5lW8BbjpzHfYYN13fJTedlGTFhhkzVtbbPAKE02kx7zCJcjYaiexdSTsrDLScYNT9/Jhdt27KpsooehwVohLfSKz4vbFfRu2MPZw3/+c/hfiJNgtz6esWbnxGrcE8U2IwPYCaK+Ghk4DcqWNIni59RI5B5kAsQOToII40qPN510uTgxBSPO7q7MHgkxdd4CqBq+ojr9j0P7oao8E5Y+CBDJrojDoCh1oCCDW9vo2dXlVcD8SIbw7U/9AfvEbA4xyE/5md1M7CIwLnWs2Ynv0YtaKoqhdS9x6FmHlMDhN/DKHinrwmowtrTT82fOkpO5g9saSmgU7Qy3gLt8t+VwdEyeFeQUKRSyci8qgqXQaZIg4+aXgaSOnlCFMtmB8ekYxEhTY5uzRfrNgS4s1QeqFBpNtUF+Ydi297pbVXnJoXAN+SVWd80GCx+yI2dpVC89k3rOWK9WeyqlnzuLJWp2RIOB9cdW8GFv/fN+QAJpYeVxOE4+nZDsKnsj8nKcg9t4Dlx1G6gLM1/Vq9YxNLbuzuRC0asUYvdMnoMvszmpm++TxndYisgNYscpZSoz7wvcazJNEPfhPVjEkd6tUUuN4GM35H0DmKCUQNT+a6B6hmHlTZvjxiyGAg5bY59hdjvJ+22QduazlEEC6LI3HrA7uK0TpplWzS1tCIFvTMUhj65DEZmNJ2+ZY9bQ4vsMf+DRR3OOG4t+DMlNfjOd3zNv3QoY95BjfWpryFwPzDq7bCP67JDsoj7j2TY5FRSrRkD77H0Ewlux2cWfjRTwcMHcdQxxuV0OP0aNjGDjybFN"}',
141+
},
142+
{
143+
id: '6424c353eaf78d000766e96137d4404b',
144+
source: 'backup',
145+
type: 'tss',
146+
commonKeychain:
147+
'19bdfe2a4b498a05511381235a8892d54267807c4a3f654e310b938b8b424ff4adedbe92f4c146de641c67508a961324c8504cdf8e0c0acbb68d6104ccccd781',
148+
encryptedPrv:
149+
'{"iv":"vi0dPef/Rx7kG/pRySQi6Q==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"9efhQsiEvVs=","ct":"Gw6atvf6gxKzsjtl3xseipO3rAxp1mAz7Yu1ihFsi5/lf2vMZegApgZx+pyILFS9KKLHbNF3U6WgSYdrr2t4vzdLsXkH1WIxfHS+cd2C5N59yADZDnPJBT6pv/IRvaYelP0Ck3nIYQ2hSMm8op+VOWC/SzHeh7slYDqwEHTGan0Wigfvk1yRd7CCJTaEAomnc/4eFi2NY3X3gt/3opy9IAgknnwUFohn96EWpEQ0F6pbzH/Z8VF6gF+DUcrrByAxExUPnHQZiFk3YHU/vVV4FxBU/mVAE8xBsBn5ul5e5SUMPfc7TBuJWv4BByTNg9xDShF/91Yx2nbfUm5d9QmM8lpKgzzQvcK8POAPk87gRCuKnsGh5vNS0UppkHc+ocfzRQlGA6jze7QyyQO0rMj5Ly8kWjwk2vISvKYHYS1NR7VU549UIXo7NXjatunKSc3+IreoRUHIshiaLg6hl+pxCCuc0qQ43V0mdIfCjTN8gkGWLNk8R7tAGPz9jyapQPcPEGHgEz0ATIi6yMNWCsibS2eLiE1uVEJONoM4lk6FPl3Q2CHbW2MeEbqjY8hbaw18mNb2xSBH/Fwpiial+Tvi2imqgnCO4ZpO9bllKftZPcQy0stN+eGBlb5ufyflKkDSiChHYroGjEpmiFicdde48cJszF52uKNnf1q67fA9/S2FAHQab3EXojxH2Gbk+kkV2h/TYKFFZSWC3vi4e8mO+vjMUcR0AdsgPFyEIz0SCGuba3CnTLNdEuZwsauAeHkx2vUTnRgJPVgNeeuXmsVG76Sy2ggJHuals0Hj8U2Xda0qO1RuFfoCWfss9wn6HGRwPPkhSB/8oNguAqmRVGKkd8Zwt3IvrTd9fk0/rFFDJKGz7WyNHkYgUmNiGcItD12v0jx7FZ52EJzl3Av1RyJUQK18+8EYPh3SGiU9dt7VX0aF0uo6JouKhOeldUvMP+AugQz8fUclwTQsbboVg27Yxo0DyATVwThW5a56R6Qf5ZiQJluFuzs5y98rq0S5q046lE6o3vVmJpEdwjeSCJoET5CL4nTgkXyWvhm4eB8u/e66l3o0qbaSx8q9YYmT9EpRcl5TP4ThLBKETYdzVvg4exjQfektMatk5EyUpEIhZPXh5vXpJZesdfO9LJ8zTaHBsBjDPU7cdNgQMbebpataRi8A0el2/IJXl+E+olgAz5zC4i2O1Q=="}',
150+
},
151+
{
152+
id: '6424c353eaf78d000766e9510b125fba',
153+
source: 'bitgo',
154+
type: 'tss',
155+
commonKeychain:
156+
'19bdfe2a4b498a05511381235a8892d54267807c4a3f654e310b938b8b424ff4adedbe92f4c146de641c67508a961324c8504cdf8e0c0acbb68d6104ccccd781',
157+
verifiedVssProof: true,
158+
isBitGo: true,
159+
},
160+
];
161+
});
162+
163+
it('should return true when validating a well formatted address', async function () {
164+
const address = 'f941ae3cbe5645dccc15da8346b533f7f91f202089a5521653c062b2ff10b304';
165+
basecoin.isValidAddress(address).should.equal(true);
166+
});
167+
168+
it('should return true when validating a well formatted address prefixed with 0x', async function () {
169+
const address = '0xf941ae3cbe5645dccc15da8346b533f7f91f202089a5521653c062b2ff10b304';
170+
basecoin.isValidAddress(address).should.equal(true);
171+
});
172+
173+
it('should return false when validating an old address', async function () {
174+
const address = '0x2959bfc3fdb7dc23fed8deba2fafb70f3e606a59';
175+
basecoin.isValidAddress(address).should.equal(false);
176+
});
177+
178+
it('should return false when validating an incorrectly formatted', async function () {
179+
const address = 'wrongaddress';
180+
basecoin.isValidAddress(address).should.equal(false);
181+
});
182+
183+
it('should return true for isWalletAddress with valid address for index 4', async function () {
184+
const newAddress = '0x8b3c7807730d75792dd6c49732cf9f014d6984a9c77d386bdb1072a9e537d8d8';
185+
const index = 4;
186+
187+
const params = { commonKeychain, address: newAddress, index, keychains };
188+
(await basecoin.isWalletAddress(params)).should.equal(true);
189+
});
190+
191+
it('should throw error for isWalletAddress when keychains is missing', async function () {
192+
const address = '0x2959bfc3fdb7dc23fed8deba2fafb70f3e606a59';
193+
const index = 0;
194+
195+
const params = { commonKeychain, address, index };
196+
await assert.rejects(async () => basecoin.isWalletAddress(params));
197+
});
198+
199+
it('should throw error for isWalletAddress when new address is invalid', async function () {
200+
const wrongAddress = 'badAddress';
201+
const index = 0;
202+
203+
const params = { commonKeychain, address: wrongAddress, index };
204+
await assert.rejects(async () => basecoin.isWalletAddress(params), {
205+
message: `invalid address: ${wrongAddress}`,
206+
});
207+
});
14208
});
15209
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import assert from 'assert';
2+
import { KeyPair } from '../../src';
3+
import utils from '../../src/lib/utils';
4+
import should from 'should';
5+
import { Eddsa } from '@bitgo/sdk-core';
6+
import { Ed25519Bip32HdTree, HDTree } from '@bitgo/sdk-lib-mpc';
7+
8+
describe('Apt KeyPair', function () {
9+
let rootKeychain;
10+
let rootPublicKey;
11+
let MPC: Eddsa;
12+
let hdTree: HDTree;
13+
14+
before(async () => {
15+
hdTree = await Ed25519Bip32HdTree.initialize();
16+
MPC = await Eddsa.initialize(hdTree);
17+
const A = MPC.keyShare(1, 2, 3);
18+
const B = MPC.keyShare(2, 2, 3);
19+
const C = MPC.keyShare(3, 2, 3);
20+
21+
const A_combine = MPC.keyCombine(A.uShare, [B.yShares[1], C.yShares[1]]);
22+
23+
const commonKeychain = A_combine.pShare.y + A_combine.pShare.chaincode;
24+
rootKeychain = MPC.deriveUnhardened(commonKeychain, 'm/0');
25+
rootPublicKey = Buffer.from(rootKeychain, 'hex').slice(0, 32).toString('hex');
26+
});
27+
28+
describe('should create a valid KeyPair', () => {
29+
it('from an empty value', async () => {
30+
const keyPair = new KeyPair();
31+
should.exists(keyPair.getKeys().prv);
32+
should.exists(keyPair.getKeys().pub);
33+
const address = await utils.getAddressFromPublicKey(keyPair.getKeys().pub);
34+
console.log('address:', address);
35+
should.exists(address);
36+
});
37+
});
38+
39+
describe('Keypair from derived Public Key', () => {
40+
it('should create keypair with just derived public key', () => {
41+
const keyPair = new KeyPair({ pub: rootPublicKey });
42+
keyPair.getKeys().pub.should.equal(rootPublicKey);
43+
});
44+
45+
it('should derived ed25519 public key should be valid', () => {
46+
utils.isValidPublicKey(rootPublicKey).should.be.true();
47+
});
48+
});
49+
50+
describe('should fail to create a KeyPair', function () {
51+
it('from an invalid public key', () => {
52+
const source = {
53+
pub: '01D63D',
54+
};
55+
56+
assert.throws(() => new KeyPair(source));
57+
});
58+
});
59+
});

0 commit comments

Comments
 (0)