diff --git a/modules/sdk-coin-hash/src/lib/constants.ts b/modules/sdk-coin-hash/src/lib/constants.ts index 623835c4b0..09ad0b3f05 100644 --- a/modules/sdk-coin-hash/src/lib/constants.ts +++ b/modules/sdk-coin-hash/src/lib/constants.ts @@ -1,4 +1,4 @@ -export const validDenoms = ['nhash', 'uhash', 'mhash', 'hash']; +export const validDenoms = ['nhash', 'uhash', 'mhash', 'hash', 'uylds.fcc']; export const mainnetAccountAddressRegex = /^(pb)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38})$/; export const mainnetValidatorAddressRegex = /^(pbvaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38})$/; export const mainnetContractAddressRegex = /^(pb)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)$/; diff --git a/modules/sdk-coin-hash/test/resources/hash.ts b/modules/sdk-coin-hash/test/resources/hash.ts index b2cd9c443b..41e1e13265 100644 --- a/modules/sdk-coin-hash/test/resources/hash.ts +++ b/modules/sdk-coin-hash/test/resources/hash.ts @@ -47,6 +47,39 @@ export const TEST_SEND_TX = { }, }; +export const TEST_SEND_TOKEN_TX = { + hash: 'A5EFED6B56EACCF531C9A7FC13731A9E0AF6F7101876995035D9AD6DA40C3B2D', + signature: 'JI7RpMouIn5VAoYpLH171tGogINLbQjDsyJ33lsWDPkYVF3kc39YDBo0sRq9fXEeZedP/pEh1f7fXPxaZisIFw==', + pubKey: 'Asujzd7qXNDrdmH8ZbeVDtZbunQiYhlZNt5Qw7J3E0Me', + privateKey: 'qp/Z1b0NeHgROzRAOqSxpBLZydY89cHSgWquf5/0nOs=', + signedTxBase64: + 'CokBCoYBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEmYKKXRwMWxmbXp4bG5wZjhrNXJxOThqeGV0ajUyZW5mMGFoenF6MDY5c3kzEil0cDEzZmE3amE4ZnhyejV3aDJka3dmZjlwbGNxZ2NqYTNycHkzeXFwORoOCgl1eWxkcy5mY2MSATESbQpQCkYKHy9jb3Ntb3MuY3J5cHRvLnNlY3AyNTZrMS5QdWJLZXkSIwohAsujzd7qXNDrdmH8ZbeVDtZbunQiYhlZNt5Qw7J3E0MeEgQKAggBGBISGQoTCgVuaGFzaBIKMzgxMDAwMDAwMBDAmgwaQCSO0aTKLiJ+VQKGKSx9e9bRqICDS20Iw7Mid95bFgz5GFRd5HN/WAwaNLEavX1xHmXnT/6RIdX+31z8WmYrCBc=', + sender: 'tp1lfmzxlnpf8k5rq98jxetj52enf0ahzqz069sy3', + recipient: 'tp13fa7ja8fxrz5wh2dkwff9plcqgcja3rpy3yqp9', + chainId: 'pio-testnet-1', + accountNumber: 235524, + sequence: 18, + sendAmount: '1', + feeAmount: '3810000000', + sendMessage: { + typeUrl: '/cosmos.bank.v1beta1.MsgSend', + value: { + amount: [ + { + denom: 'uylds.fcc', + amount: '1', + }, + ], + toAddress: 'tp13fa7ja8fxrz5wh2dkwff9plcqgcja3rpy3yqp9', + fromAddress: 'tp1lfmzxlnpf8k5rq98jxetj52enf0ahzqz069sy3', + }, + }, + gasBudget: { + amount: [{ denom: 'nhash', amount: '3810000000' }], + gasLimit: 200000, + }, +}; + export const TEST_DELEGATE_TX = { hash: 'BD887ADE5E10C378B4B3DB3E4E0D5D77F0C2F3DFEE1A352BF186B22A8F022967', signature: 'FMZqHoNhxzJRvhhZbiLLybgAHdkszEstxow2oj1T1It8OyEGLJ8o2jnUo9OuCfeCKBfcWrKAi4w9dwsC8rBChQ==', @@ -194,6 +227,45 @@ export const TEST_TX_WITH_MEMO = { }, }; +export const TEST_TOKEN_TX_WITH_MEMO = { + hash: 'F4B979ECBF56EA203EA24ECE1F44AD621C5BC97955722C16AFBFC902E0001E5D', + signature: 'HkE/roZVKf3rj4RH07fPPLeSJzxSGmz+7FQJ6nom1e9pD/3QMLXVHLpswFJCNDwoS9kfeSifBFKgPc6dpOr+7w==', + pubKey: 'Asujzd7qXNDrdmH8ZbeVDtZbunQiYhlZNt5Qw7J3E0Me', + privateKey: 'qp/Z1b0NeHgROzRAOqSxpBLZydY89cHSgWquf5/0nOs=', + signedTxBase64: + 'CowBCoYBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEmYKKXRwMWxmbXp4bG5wZjhrNXJxOThqeGV0ajUyZW5mMGFoenF6MDY5c3kzEil0cDEzZmE3amE4ZnhyejV3aDJka3dmZjlwbGNxZ2NqYTNycHkzeXFwORoOCgl1eWxkcy5mY2MSATESATcSbQpQCkYKHy9jb3Ntb3MuY3J5cHRvLnNlY3AyNTZrMS5QdWJLZXkSIwohAsujzd7qXNDrdmH8ZbeVDtZbunQiYhlZNt5Qw7J3E0MeEgQKAggBGBESGQoTCgVuaGFzaBIKMzgxMDAwMDAwMBDAmgwaQB5BP66GVSn964+ER9O3zzy3kic8Uhps/uxUCep6JtXvaQ/90DC11Ry6bMBSQjQ8KEvZH3konwRSoD3OnaTq/u8=', + from: 'tp1lfmzxlnpf8k5rq98jxetj52enf0ahzqz069sy3', + to: 'tp13fa7ja8fxrz5wh2dkwff9plcqgcja3rpy3yqp9', + chainId: 'pio-testnet-1', + accountNumber: 235524, + sequence: 17, + sendAmount: '1', + feeAmount: '3810000000', + sendMessage: { + typeUrl: '/cosmos.bank.v1beta1.MsgSend', + value: { + amount: [ + { + denom: 'uylds.fcc', + amount: '1', + }, + ], + toAddress: 'tp13fa7ja8fxrz5wh2dkwff9plcqgcja3rpy3yqp9', + fromAddress: 'tp1lfmzxlnpf8k5rq98jxetj52enf0ahzqz069sy3', + }, + }, + memo: '7', + gasBudget: { + amount: [ + { + denom: 'nhash', + amount: '3810000000', + }, + ], + gasLimit: 200000, + }, +}; + export const mainnetAddress = { address1: 'pb1fmxzuzx5c4ja50vu94nt0aessnuedzmppde8qr', address2: 'pb16vmp7sz28pnvgz6f3zm6q93y39jsd33aazwg4u', diff --git a/modules/sdk-coin-hash/test/unit/tokenTransaction.ts b/modules/sdk-coin-hash/test/unit/tokenTransaction.ts new file mode 100644 index 0000000000..b0dec00080 --- /dev/null +++ b/modules/sdk-coin-hash/test/unit/tokenTransaction.ts @@ -0,0 +1,130 @@ +import { toHex, TransactionType } from '@bitgo/sdk-core'; +import { coins } from '@bitgo/statics'; +import { fromBase64 } from '@cosmjs/encoding'; +import should from 'should'; + +import { CosmosTransaction, SendMessage } from '@bitgo/abstract-cosmos'; +import { HashUtils } from '../../src/lib/utils'; +import * as testData from '../resources/hash'; + +describe('Hash Token Transaction', () => { + let tx: CosmosTransaction; + const tokenName = 'thash:ylds'; + const config = coins.get(tokenName); + const utils = new HashUtils(config.network.type); + + beforeEach(() => { + tx = new CosmosTransaction(config, utils); + }); + + describe('Empty transaction', () => { + it('should throw empty transaction', function () { + should.throws(() => tx.toBroadcastFormat(), 'Empty transaction'); + }); + }); + + describe('From raw transaction', () => { + it('should build a transfer from raw signed base64', function () { + tx.enrichTransactionDetailsFromRawTransaction(testData.TEST_SEND_TX.signedTxBase64); + const json = tx.toJson(); + should.equal(json.sequence, testData.TEST_SEND_TX.sequence); + should.deepEqual(json.gasBudget, testData.TEST_SEND_TX.gasBudget); + should.equal(json.publicKey, toHex(fromBase64(testData.TEST_SEND_TX.pubKey))); + should.equal( + (json.sendMessages[0].value as SendMessage).toAddress, + testData.TEST_SEND_TX.sendMessage.value.toAddress + ); + should.deepEqual( + (json.sendMessages[0].value as SendMessage).amount, + testData.TEST_SEND_TX.sendMessage.value.amount + ); + should.equal(Buffer.from(json.signature as any).toString('base64'), testData.TEST_SEND_TX.signature); + should.equal(tx.type, TransactionType.Send); + tx.loadInputsAndOutputs(); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_SEND_TX.sender, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: tokenName, + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_SEND_TX.sendMessage.value.toAddress, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: tokenName, + }, + ]); + }); + + it('should build a transfer from raw signed hex', function () { + tx.enrichTransactionDetailsFromRawTransaction(toHex(fromBase64(testData.TEST_SEND_TX.signedTxBase64))); + const json = tx.toJson(); + should.equal(json.sequence, testData.TEST_SEND_TX.sequence); + should.deepEqual(json.gasBudget, testData.TEST_SEND_TX.gasBudget); + should.equal(json.publicKey, toHex(fromBase64(testData.TEST_SEND_TX.pubKey))); + should.equal( + (json.sendMessages[0].value as SendMessage).toAddress, + testData.TEST_SEND_TX.sendMessage.value.toAddress + ); + should.deepEqual( + (json.sendMessages[0].value as SendMessage).amount, + testData.TEST_SEND_TX.sendMessage.value.amount + ); + should.equal(Buffer.from(json.signature as any).toString('base64'), testData.TEST_SEND_TX.signature); + should.equal(tx.type, TransactionType.Send); + tx.loadInputsAndOutputs(); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_SEND_TX.sender, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: tokenName, + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_SEND_TX.sendMessage.value.toAddress, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: tokenName, + }, + ]); + }); + + it('should fail to build a transfer from incorrect raw hex', function () { + should.throws( + () => tx.enrichTransactionDetailsFromRawTransaction('random' + testData.TEST_SEND_TX.signedTxBase64), + 'incorrect raw data' + ); + }); + + it('should fail to explain transaction with invalid raw hex', function () { + should.throws(() => tx.enrichTransactionDetailsFromRawTransaction('randomString'), 'Invalid transaction'); + }); + }); + + describe('Explain transaction', () => { + it('should explain a transfer pay transaction', function () { + tx.enrichTransactionDetailsFromRawTransaction(testData.TEST_SEND_TX.signedTxBase64); + const explainedTransaction = tx.explainTransaction(); + explainedTransaction.should.deepEqual({ + displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'type'], + id: testData.TEST_SEND_TX.hash, + outputs: [ + { + address: testData.TEST_SEND_TX.recipient, + amount: testData.TEST_SEND_TX.sendAmount, + }, + ], + outputAmount: testData.TEST_SEND_TX.sendAmount, + changeOutputs: [], + changeAmount: '0', + fee: { fee: testData.TEST_SEND_TX.feeAmount }, + type: 0, + }); + }); + + it('should fail to explain transaction with invalid raw base64 string', function () { + should.throws(() => tx.enrichTransactionDetailsFromRawTransaction('randomString'), 'Invalid transaction'); + }); + }); +}); diff --git a/modules/sdk-coin-hash/test/unit/transactionBuilder/transactionBuilder.ts b/modules/sdk-coin-hash/test/unit/transactionBuilder/transactionBuilder.ts index 893a64c302..3cef8595e4 100644 --- a/modules/sdk-coin-hash/test/unit/transactionBuilder/transactionBuilder.ts +++ b/modules/sdk-coin-hash/test/unit/transactionBuilder/transactionBuilder.ts @@ -20,13 +20,19 @@ describe('Hash Transaction Builder', async () => { }); const testTxData = testData.TEST_SEND_TX; + const tokenTestTxData = testData.TEST_SEND_TOKEN_TX; let data; beforeEach(() => { data = [ { type: TransactionType.Send, - testTx: testData.TEST_SEND_TX, + testTx: testTxData, + builder: factory.getTransferBuilder(), + }, + { + type: TransactionType.Send, + testTx: tokenTestTxData, builder: factory.getTransferBuilder(), }, { @@ -56,6 +62,15 @@ describe('Hash Transaction Builder', async () => { should.equal(rawTx, testTxData.signedTxBase64); }); + it('should build a signed token tx from signed token tx data', async function () { + const txBuilder = factory.from(tokenTestTxData.signedTxBase64); + const tx = await txBuilder.build(); + should.equal(tx.type, TransactionType.Send); + // Should recreate the same raw tx data when re-build and turned to broadcast format + const rawTx = tx.toBroadcastFormat(); + should.equal(rawTx, tokenTestTxData.signedTxBase64); + }); + describe('gasBudget tests', async () => { it('should succeed for valid gasBudget', function () { for (const { builder } of data) { diff --git a/modules/sdk-coin-hash/test/unit/transactionBuilder/transferBuilder.ts b/modules/sdk-coin-hash/test/unit/transactionBuilder/transferBuilder.ts index ca2b4aab26..c4686e8672 100644 --- a/modules/sdk-coin-hash/test/unit/transactionBuilder/transferBuilder.ts +++ b/modules/sdk-coin-hash/test/unit/transactionBuilder/transferBuilder.ts @@ -158,3 +158,156 @@ describe('Hash Transfer Builder', () => { ]); }); }); + +describe('Hash Token Transfer Builder', () => { + let bitgo: TestBitGoAPI; + let basecoin; + let factory; + let testTx; + let testTxWithMemo; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('hash:ylds', Hash.createInstance); + bitgo.safeRegister('thash:ylds', Thash.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('thash:ylds'); + factory = basecoin.getBuilder(); + testTx = testData.TEST_SEND_TOKEN_TX; + testTxWithMemo = testData.TEST_TOKEN_TX_WITH_MEMO; + }); + + it('should build a Transfer tx with signature', async function () { + const txBuilder = factory.getTransferBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.publicKey(toHex(fromBase64(testTx.pubKey))); + txBuilder.addSignature({ pub: toHex(fromBase64(testTx.pubKey)) }, Buffer.from(testTx.signature, 'base64')); + + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.Send); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + const rawTx = tx.toBroadcastFormat(); + should.equal(rawTx, testTx.signedTxBase64); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_SEND_TOKEN_TX.sender, + value: testData.TEST_SEND_TOKEN_TX.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_SEND_TOKEN_TX.sendMessage.value.toAddress, + value: testData.TEST_SEND_TOKEN_TX.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + }); + + it('should build a Transfer tx with signature and memo', async function () { + const txBuilder = factory.getTransferBuilder(); + txBuilder.sequence(testTxWithMemo.sequence); + txBuilder.gasBudget(testTxWithMemo.gasBudget); + txBuilder.messages([testTxWithMemo.sendMessage.value]); + txBuilder.publicKey(toHex(fromBase64(testTxWithMemo.pubKey))); + txBuilder.memo(testTxWithMemo.memo); + txBuilder.addSignature( + { pub: toHex(fromBase64(testTxWithMemo.pubKey)) }, + Buffer.from(testTxWithMemo.signature, 'base64') + ); + + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.Send); + should.deepEqual(json.gasBudget, testTxWithMemo.gasBudget); + should.deepEqual(json.sendMessages, [testTxWithMemo.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTxWithMemo.pubKey))); + should.deepEqual(json.sequence, testTxWithMemo.sequence); + should.equal(json.memo, testTxWithMemo.memo); + const rawTx = tx.toBroadcastFormat(); + should.equal(rawTx, testTxWithMemo.signedTxBase64); + should.deepEqual(tx.inputs, [ + { + address: testTxWithMemo.sendMessage.value.fromAddress, + value: testTxWithMemo.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testTxWithMemo.sendMessage.value.toAddress, + value: testTxWithMemo.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + }); + + it('should build a Transfer tx without signature', async function () { + const txBuilder = factory.getTransferBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.publicKey(toHex(fromBase64(testTx.pubKey))); + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.Send); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + tx.toBroadcastFormat(); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_SEND_TOKEN_TX.sender, + value: testData.TEST_SEND_TOKEN_TX.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_SEND_TOKEN_TX.sendMessage.value.toAddress, + value: testData.TEST_SEND_TOKEN_TX.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + }); + + it('should sign a Transfer tx', async function () { + const txBuilder = factory.getTransferBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.accountNumber(testTx.accountNumber); + txBuilder.chainId(testTx.chainId); + txBuilder.sign({ key: toHex(fromBase64(testTx.privateKey)) }); + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.Send); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + const rawTx = tx.toBroadcastFormat(); + should.equal(tx.signature[0], toHex(fromBase64(testTx.signature))); + should.equal(rawTx, testTx.signedTxBase64); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_SEND_TOKEN_TX.sender, + value: testData.TEST_SEND_TOKEN_TX.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_SEND_TOKEN_TX.sendMessage.value.toAddress, + value: testData.TEST_SEND_TOKEN_TX.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + }); +});