UNPKG

@ledgerhq/coin-aptos

Version:
1,018 lines (973 loc) 32.1 kB
import { EntryFunctionPayloadResponse } from "@aptos-labs/ts-sdk"; import BigNumber from "bignumber.js"; import { APTOS_COIN_CHANGE, OP_TYPE } from "../../constants"; import { getMaxSendBalance, getBlankOperation, txsToOps } from "../../bridge/logic"; import type { AptosTransaction, TransactionOptions } from "../../types"; import { createFixtureAccount, createFixtureAccountWithSubAccount, } from "../../bridge/bridge.fixture"; import { decodeTokenAccountId, encodeTokenAccountId } from "@ledgerhq/coin-framework/account/index"; import { normalizeTransactionOptions } from "../../logic/normalizeTransactionOptions"; import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets"; jest.mock("@ledgerhq/cryptoassets"); jest.mock("@ledgerhq/coin-framework/account/index"); import { setupMockCryptoAssetsStore } from "@ledgerhq/cryptoassets/cal-client/test-helpers"; setupMockCryptoAssetsStore(); describe("Aptos logic ", () => { describe("getMaxSendBalance", () => { it("should return the correct max send balance when amount is greater than total gas", () => { const amount = new BigNumber(1000000); const account = createFixtureAccount({ balance: amount, spendableBalance: amount }); const gas = new BigNumber(200); const gasPrice = new BigNumber(100); const result = getMaxSendBalance(account, undefined, gas, gasPrice); expect(result.isEqualTo(amount.minus(gas.multipliedBy(gasPrice)))).toBe(true); }); it("should return zero when amount is less than total gas", () => { const account = createFixtureAccount(); const gas = new BigNumber(200); const gasPrice = new BigNumber(100); const result = getMaxSendBalance(account, undefined, gas, gasPrice); expect(result.isEqualTo(new BigNumber(0))).toBe(true); }); it("should return zero when amount is equal to total gas", () => { const account = createFixtureAccount(); const gas = new BigNumber(200); const gasPrice = new BigNumber(100); const result = getMaxSendBalance(account, undefined, gas, gasPrice); expect(result.isEqualTo(new BigNumber(0))).toBe(true); }); it("should handle zero amount", () => { const account = createFixtureAccount(); const gas = new BigNumber(200); const gasPrice = new BigNumber(100); const result = getMaxSendBalance(account, undefined, gas, gasPrice); expect(result.isEqualTo(new BigNumber(0))).toBe(true); }); it("should handle zero gas and gas price", () => { const amount = new BigNumber(1000000); const account = createFixtureAccount({ balance: amount, spendableBalance: amount }); const gas = new BigNumber(0); const gasPrice = new BigNumber(0); const result = getMaxSendBalance(account, undefined, gas, gasPrice); expect(result.isEqualTo(amount)).toBe(true); }); it("should return max spendable amount for the token account", () => { const account = createFixtureAccountWithSubAccount("coin"); const gas = new BigNumber(200); const gasPrice = new BigNumber(100); const result = getMaxSendBalance(account.subAccounts![0], account, gas, gasPrice); expect(result.isEqualTo(BigNumber(1000))).toBe(true); }); }); describe("normalizeTransactionOptions", () => { it("should normalize transaction options", () => { const options: TransactionOptions = { maxGasAmount: "1000", gasUnitPrice: "10", }; const result = normalizeTransactionOptions(options); expect(result).toEqual(options); }); it("should return undefined for empty values", () => { const options: TransactionOptions = { maxGasAmount: "", gasUnitPrice: "", }; const result = normalizeTransactionOptions(options); expect(result).toEqual({ maxGasAmount: undefined, gasUnitPrice: undefined, }); }); }); describe("getBlankOperation", () => { it("should return a blank operation", () => { const tx: AptosTransaction = { hash: "0x123", block: { hash: "0xabc", height: 1 }, timestamp: "1000000", sequence_number: "1", version: "1", } as unknown as AptosTransaction; const id = "test_id"; const result = getBlankOperation(tx, id); expect(result).toEqual({ id: "", hash: "0x123", type: "", value: new BigNumber(0), fee: new BigNumber(0), blockHash: "0xabc", blockHeight: 1, senders: [], recipients: [], accountId: id, date: new Date(1000), extra: { version: "1" }, transactionSequenceNumber: new BigNumber(1), hasFailed: false, }); }); it("should return a blank operation even when some transaction fields are missing", () => { const tx: AptosTransaction = { hash: "0x123", timestamp: "1000000", sequence_number: "1", } as unknown as AptosTransaction; const id = "test_id"; const result = getBlankOperation(tx, id); expect(result).toEqual({ id: "", hash: "0x123", type: "", value: new BigNumber(0), fee: new BigNumber(0), blockHash: undefined, blockHeight: undefined, senders: [], recipients: [], accountId: id, date: new Date(1000), extra: { version: undefined }, transactionSequenceNumber: new BigNumber(1), hasFailed: false, }); }); }); }); describe("Aptos sync logic ", () => { describe("txsToOps", () => { it("should convert Aptos transactions to operations correctly", async () => { const address = "0x11"; const id = "test_id"; const txs: AptosTransaction[] = [ { hash: "0x123", sender: "0x11", gas_used: "200", gas_unit_price: "100", success: true, payload: { type: "entry_function_payload", function: "0x1::coin::transfer", type_arguments: [], arguments: ["0x12", 100], } as EntryFunctionPayloadResponse, events: [ { type: "0x1::coin::WithdrawEvent", guid: { account_address: "0x11", creation_number: "1", }, data: { amount: "100", }, }, { type: "0x1::coin::DepositEvent", guid: { account_address: "0x12", creation_number: "2", }, data: { amount: "100", }, }, ], changes: [ { type: "write_resource", data: { type: APTOS_COIN_CHANGE, data: { withdraw_events: { guid: { id: { addr: "0x11", creation_num: "1", }, }, }, deposit_events: { guid: { id: { addr: "0x12", creation_num: "2", }, }, }, }, }, }, ], block: { hash: "0xabc", height: 1 }, timestamp: "1000000", sequence_number: "1", } as unknown as AptosTransaction, ]; const [result] = await txsToOps({ address }, id, txs); expect(result).toHaveLength(1); expect(result[0]).toEqual({ id: expect.any(String), hash: "0x123", type: OP_TYPE.OUT, value: new BigNumber(100), fee: new BigNumber(20000), blockHash: "0xabc", blockHeight: 1, senders: ["0x11"], recipients: ["0x0000000000000000000000000000000000000000000000000000000000000012"], accountId: id, date: new Date(1000), extra: { version: undefined }, transactionSequenceNumber: new BigNumber(1), hasFailed: false, }); }); it("should skip transactions without functions in payload", async () => { const address = "0x11"; const id = "test_id"; const txs: AptosTransaction[] = [ { hash: "0x123", sender: "0x11", gas_used: "200", gas_unit_price: "100", success: true, payload: {} as EntryFunctionPayloadResponse, events: [], changes: [], block: { hash: "0xabc", height: 1 }, timestamp: "1000000", sequence_number: "1", } as unknown as AptosTransaction, ]; const [result] = await txsToOps({ address }, id, txs); expect(result).toHaveLength(0); }); it("should skip transactions that result in no Aptos change", async () => { const address = "0x11"; const id = "test_id"; const txs: AptosTransaction[] = [ { hash: "0x123", sender: "0x12", gas_used: "200", gas_unit_price: "100", success: true, payload: { type: "entry_function_payload", function: "0x1::coin::transfer", type_arguments: [], arguments: ["0x11", 100], } as EntryFunctionPayloadResponse, events: [], changes: [], block: { hash: "0xabc", height: 1 }, timestamp: "1000000", sequence_number: "1", } as unknown as AptosTransaction, ]; const [result] = await txsToOps({ address }, id, txs); expect(result).toHaveLength(0); }); it("should handle failed transactions", async () => { const address = "0xa0d8"; const id = "test_id"; const txs: AptosTransaction[] = [ { hash: "0x0189", sender: "0xa0d8", gas_used: "200", gas_unit_price: "100", success: false, payload: { function: "0x1::coin::transfer", type_arguments: ["0xd111::staked_coin::StakedAptos"], arguments: ["0x4e5e", "50000000"], type: "entry_function_payload", } as EntryFunctionPayloadResponse, events: [ { guid: { creation_number: "0", account_address: "0x0", }, sequence_number: "0", type: "0x1::transaction_fee::FeeStatement", data: { execution_gas_units: "5", io_gas_units: "4", storage_fee_octas: "0", storage_fee_refund_octas: "0", total_charge_gas_units: "8", }, }, ], changes: [ { address: "0xa0d8", state_key_hash: "0x1709", data: { type: "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", data: { coin: { value: "573163341", }, deposit_events: { counter: "45", guid: { id: { addr: "0xa0d8", creation_num: "2", }, }, }, frozen: false, withdraw_events: { counter: "82", guid: { id: { addr: "0xa0d8", creation_num: "3", }, }, }, }, }, type: "write_resource", }, { address: "0xa0d8", state_key_hash: "0x6f1e", data: { type: "0x1::account::Account", data: { authentication_key: "0xa0d8", coin_register_events: { counter: "5", guid: { id: { addr: "0xa0d8", creation_num: "0", }, }, }, guid_creation_num: "12", key_rotation_events: { counter: "0", guid: { id: { addr: "0xa0d8", creation_num: "1", }, }, }, rotation_capability_offer: { for: { vec: [], }, }, sequence_number: "83", signer_capability_offer: { for: { vec: [], }, }, }, }, type: "write_resource", }, { state_key_hash: "0x6e4b", handle: "0x1b85", key: "0x0619", value: "0x72c5e483c25c96010000000000000000", data: null, type: "write_table_item", }, ], block: { hash: "0xc496", height: 1, }, timestamp: "1000000", sequence_number: "1", } as unknown as AptosTransaction, ]; const [result] = await txsToOps({ address }, id, txs); expect(result).toHaveLength(1); expect(result[0]).toEqual({ id: expect.any(String), hash: "0x0189", type: OP_TYPE.OUT, value: new BigNumber(20000), fee: new BigNumber(20000), blockHash: "0xc496", blockHeight: 1, senders: ["0xa0d8"], recipients: ["0x0000000000000000000000000000000000000000000000000000000000004e5e"], accountId: id, date: new Date(1000), extra: { version: undefined }, transactionSequenceNumber: new BigNumber(1), hasFailed: true, }); }); it("should convert Aptos token transactions to operations correctly", async () => { (encodeTokenAccountId as jest.Mock).mockReturnValue("token_account_id"); setupMockCryptoAssetsStore({ findTokenByAddressInCurrency: async (address: string, _currencyId: string) => { // coin_id is converted to lowercase in txsToOps if (address === "0xd111::staked_coin::stakedaptos") { return { type: "TokenCurrency" as const, id: "aptos/coin/dstapt::staked_coin::stakedaptos", contractAddress: "0xd111::staked_coin::StakedAptos", parentCurrency: getCryptoCurrencyById("aptos"), name: "dstAPT", tokenType: "coin", ticker: "dstAPT", disableCountervalue: false, delisted: false, units: [ { name: "dstAPT", code: "dstAPT", magnitude: 8, }, ], }; } return undefined; }, getTokensSyncHash: async () => "0", }); jest.mock("../../bridge/logic", () => ({ ...jest.requireActual("../../bridge/logic"), getResourceAddress: jest.fn().mockReturnValue("0xd111::staked_coin::StakedAptos"), })); (decodeTokenAccountId as jest.Mock).mockReturnValue({ accountId: "token_account_id", }); const address = "0xa0d"; const id = "test_id"; const txs: AptosTransaction[] = [ { hash: "0x123", sender: address, gas_used: "200", gas_unit_price: "100", success: true, payload: { function: "0x1::aptos_account::transfer_coins", type_arguments: ["0xd111::staked_coin::StakedAptos"], arguments: ["0x4e5", "1500000"], type: "entry_function_payload", } as EntryFunctionPayloadResponse, events: [ { guid: { creation_number: "11", account_address: "0xa0d", }, sequence_number: "12", type: "0x1::coin::WithdrawEvent", data: { amount: "1500000", }, }, { guid: { creation_number: "4", account_address: "0x4e5", }, sequence_number: "8", type: "0x1::coin::DepositEvent", data: { amount: "1500000", }, }, { guid: { creation_number: "0", account_address: "0x0", }, sequence_number: "0", type: "0x1::transaction_fee::FeeStatement", data: { execution_gas_units: "6", io_gas_units: "6", storage_fee_octas: "0", storage_fee_refund_octas: "0", total_charge_gas_units: "12", }, }, ], changes: [ { address: "0x4e5", state_key_hash: "0x3c0", data: { type: "0x1::coin::CoinStore<0xd111::staked_coin::StakedAptos>", data: { coin: { value: "4000000", }, deposit_events: { counter: "9", guid: { id: { addr: "0x4e5", creation_num: "4", }, }, }, frozen: false, withdraw_events: { counter: "6", guid: { id: { addr: "0x4e5", creation_num: "5", }, }, }, }, }, type: "write_resource", }, { address: "0xa0d", state_key_hash: "0x1709", data: { type: "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", data: { coin: { value: "68254118", }, deposit_events: { counter: "46", guid: { id: { addr: "0xa0d", creation_num: "2", }, }, }, frozen: false, withdraw_events: { counter: "89", guid: { id: { addr: "0xa0d", creation_num: "3", }, }, }, }, }, type: "write_resource", }, { address: "0xa0d", state_key_hash: "0x5520", data: { type: "0x1::coin::CoinStore<0xd111::staked_coin::StakedAptos>", data: { coin: { value: "1000000", }, deposit_events: { counter: "7", guid: { id: { addr: "0xa0d", creation_num: "10", }, }, }, frozen: false, withdraw_events: { counter: "13", guid: { id: { addr: "0xa0d", creation_num: "11", }, }, }, }, }, type: "write_resource", }, { address: "0xa0d", state_key_hash: "0x6f1e", data: { type: "0x1::account::Account", data: { authentication_key: "0xa0d", coin_register_events: { counter: "5", guid: { id: { addr: "0xa0d", creation_num: "0", }, }, }, guid_creation_num: "12", key_rotation_events: { counter: "0", guid: { id: { addr: "0xa0d", creation_num: "1", }, }, }, rotation_capability_offer: { for: { vec: [], }, }, sequence_number: "122", signer_capability_offer: { for: { vec: [], }, }, }, }, type: "write_resource", }, { state_key_hash: "0x6e4b", handle: "0x1b85", key: "0x0619", value: "0x1ddaf8da3b1497010000000000000000", data: null, type: "write_table_item", }, ], block: { hash: "0xabc", height: 1 }, timestamp: "1000000", sequence_number: "1", } as unknown as AptosTransaction, ]; const [ops, tokenOps] = await txsToOps({ address }, id, txs); expect(ops).toHaveLength(1); expect(ops[0]).toEqual({ id: expect.any(String), hash: "0x123", type: "FEES", value: new BigNumber(20000), fee: new BigNumber(20000), blockHash: "0xabc", blockHeight: 1, senders: ["0xa0d"], recipients: ["0x00000000000000000000000000000000000000000000000000000000000004e5"], accountId: "token_account_id", date: new Date(1000), extra: { version: undefined }, transactionSequenceNumber: new BigNumber(1), hasFailed: false, }); expect(tokenOps).toHaveLength(1); expect(tokenOps[0]).toEqual({ id: expect.any(String), accountId: "token_account_id", hash: "0x123", type: OP_TYPE.OUT, value: new BigNumber(1500000), fee: new BigNumber(20000), blockHash: "0xabc", blockHeight: 1, senders: ["0xa0d"], recipients: ["0x00000000000000000000000000000000000000000000000000000000000004e5"], date: new Date(1000), extra: { version: undefined }, transactionSequenceNumber: new BigNumber(1), hasFailed: false, }); }); it("should convert Aptos token transactions to operations correctly", async () => { setupMockCryptoAssetsStore({ findTokenByAddressInCurrency: async (address: string, _currencyId: string) => { if (address === "0x2ebb") { return { type: "TokenCurrency" as const, id: "aptos/fungible_asset/cellana_0x2ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12", contractAddress: "0x2ebb", parentCurrency: getCryptoCurrencyById("aptos"), name: "CELLANA", tokenType: "fungible_asset", ticker: "CELL", disableCountervalue: false, delisted: false, units: [ { name: "CELLANA", code: "CELL", magnitude: 8, }, ], }; } return undefined; }, getTokensSyncHash: async () => "0", }); jest.mock("../../bridge/logic", () => ({ ...jest.requireActual("../../bridge/logic"), getResourceAddress: jest.fn().mockReturnValue("0x2ebb"), })); (encodeTokenAccountId as jest.Mock).mockReturnValue("token_account_id"); const txs: AptosTransaction[] = [ { hash: "0x10c9", sender: "0xa0d8", gas_used: "200", gas_unit_price: "100", success: true, payload: { function: "0x1::primary_fungible_store::transfer", type_arguments: ["0x1::fungible_asset::Metadata"], arguments: [ { inner: "0x2ebb", }, "0x6b8c", "193", ], type: "entry_function_payload", } as EntryFunctionPayloadResponse, events: [ { guid: { creation_number: "0", account_address: "0x0", }, sequence_number: "0", type: "0x1::fungible_asset::Withdraw", data: { amount: "193", store: "0xd475", }, }, { guid: { creation_number: "0", account_address: "0x0", }, sequence_number: "0", type: "0x1::fungible_asset::Deposit", data: { amount: "193", store: "0xaaa9", }, }, { guid: { creation_number: "0", account_address: "0x0", }, sequence_number: "0", type: "0x1::transaction_fee::FeeStatement", data: { execution_gas_units: "4", io_gas_units: "6", storage_fee_octas: "0", storage_fee_refund_octas: "0", total_charge_gas_units: "10", }, }, ], changes: [ { address: "0xaaa9", state_key_hash: "0x9a17", data: { type: "0x1::fungible_asset::FungibleStore", data: { balance: "10044959", frozen: false, metadata: { inner: "0x2ebb", }, }, }, type: "write_resource", }, { address: "0xaaa9", state_key_hash: "0x9a17", data: { type: "0x1::object::ObjectCore", data: { allow_ungated_transfer: false, guid_creation_num: "1125899906842625", owner: "0x6b8c", transfer_events: { counter: "0", guid: { id: { addr: "0xaaa9", creation_num: "1125899906842624", }, }, }, }, }, type: "write_resource", }, { address: "0xa0d8", state_key_hash: "0x1709", data: { type: "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", data: { coin: { value: "98423118", }, deposit_events: { counter: "46", guid: { id: { addr: "0xa0d8", creation_num: "2", }, }, }, frozen: false, withdraw_events: { counter: "88", guid: { id: { addr: "0xa0d8", creation_num: "3", }, }, }, }, }, type: "write_resource", }, { address: "0xa0d8", state_key_hash: "0x6f1e", data: { type: "0x1::account::Account", data: { authentication_key: "0xa0d8", coin_register_events: { counter: "5", guid: { id: { addr: "0xa0d8", creation_num: "0", }, }, }, guid_creation_num: "12", key_rotation_events: { counter: "0", guid: { id: { addr: "0xa0d8", creation_num: "1", }, }, }, rotation_capability_offer: { for: { vec: [], }, }, sequence_number: "108", signer_capability_offer: { for: { vec: [], }, }, }, }, type: "write_resource", }, { address: "0xd475", state_key_hash: "0x7567", data: { type: "0x1::fungible_asset::FungibleStore", data: { balance: "14000", frozen: false, metadata: { inner: "0x2ebb", }, }, }, type: "write_resource", }, { address: "0xd475", state_key_hash: "0x7567", data: { type: "0x1::object::ObjectCore", data: { allow_ungated_transfer: false, guid_creation_num: "1125899906842625", owner: "0xa0d8", transfer_events: { counter: "0", guid: { id: { addr: "0xd475", creation_num: "1125899906842624", }, }, }, }, }, type: "write_resource", }, { state_key_hash: "0x6e4b", handle: "0x1b85", key: "0x0619", value: "0xad4388dc7daf96010000000000000000", data: null, type: "write_table_item", }, ], block: { hash: "0xabc", height: 1 }, timestamp: "1000000", sequence_number: "1", } as unknown as AptosTransaction, ]; const [ops, tokenOps] = await txsToOps({ address: "0x6b8c" }, "test_id", txs); expect(ops).toHaveLength(0); expect(tokenOps).toHaveLength(1); expect(tokenOps[0]).toEqual({ id: expect.any(String), accountId: "token_account_id", hash: "0x10c9", type: OP_TYPE.IN, value: new BigNumber(193), fee: new BigNumber(20000), blockHash: "0xabc", blockHeight: 1, senders: ["0xa0d8"], recipients: ["0x0000000000000000000000000000000000000000000000000000000000006b8c"], date: new Date(1000), extra: { version: undefined }, transactionSequenceNumber: new BigNumber(1), hasFailed: false, }); }); }); });