From 89aea3a0423a709e4a3ed3be332ecd5438cf67fa Mon Sep 17 00:00:00 2001 From: Pedro Semeano Date: Fri, 3 Jan 2025 10:02:18 +0000 Subject: [PATCH] test(aptos): index api (#8778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP: ledger-live boilerplate * Add currency * Add JS Bindings * Add 'getAddress' * TODO: Accounts, Send * Implement base functionality for aptos * Accounts synchronization * Recieve * Send TODO: * Implement additional settings for token sending * Add transaction configuration * Gas fee (gas price, gas limit) * Additional settings (sequence number, exp. timestamp) * TODO: add texts to the dictionary * Fix issues * Update dictionary * update `hw-app-aptos` package * Rework transaction receiving - Get transactions from the indexer by its version - Get both transaction types Receive/Send - Use Aptos API * Fix dependencies * LLD small fixes * Implement Aptos in LLM * Change transaction praparation, update operation signing * Update `pnpm-lock.yaml` after merge with `develop` * Calculate staked balance; display balance on account page * Rename `xpub` field to `publicKey` * Remove unused field * Fix staked amount * Fix delegation dictionary * Move `compareAddress` helper from `utils` to `logic` * Rename `delegatedBalance` to `delegatedAmount` * Fix typo in dictionary * Use mainnet * Revert "Rename `xpub` field to `publicKey`" This reverts commit 3e2d8d82155dfaf4ff7f3fb3cd9c807d1023d734. * Handle the case when account's field `freshAddresses` is empty * Get public key from account id if it can not be retrieved from the device * Fix condition with xpub * Implement `getStake` function to calculate stake rewards * Use testnet endpoint if testnet selected * Change transaction `function`; add value to `type_arguments` * Fix bug with the `recepient` field * Revert "Change transaction `function`; add value to `type_arguments`" This reverts commit 00b2ab0dc99aa6cea547d247baa16015f6587c52. * Remove redundant translations * Add Aptos to CLI app * Add changeset * Fix linter and types problems * Remove redundant dependency * support/fix pnpm-lock.yaml after merge conflict * support(aptos): update pnpm-lock.yaml after merge conflict * feat(aptos): update transfer function * fix rebase issues * fix remaining getAccountUnit imports * hw-app-aptos.js moved to lib/ledgerjs/packages/hw-app-aptos. Test file has been copied, but is still not active. * Repurposed README.md from algorand. removed some unused dependencies from the package. * Small cosmetic changes. * Managed to make tests work. * Aptos fixing: History isn't full (#8512) extending tx to ops with delegation batches and events * fix(aptos): amount balance (#8462) * Removing Delegated Balance from the Aptos Footer. * Prettify addition. * Removing unecessary code. * Removing uneeded imports * Fixing prettier issues. * Removing the footer and the translation units. * feat(aptos): add coin feature flag (#8498) * feat(aptos): add coin feature flag * feat(aptos): add coin feature flag for testnet * Removing fees from LLD * Feedback from PR. * Removed the custom fees from Ledger Live Mobile. * Removed a few files that are not needed after the custom fees removal. * fix(aptos): remove inexistent dependency (#8644) * Removing unused imports. * Feat/live 15120 aptos bst fix fees visibility for unparsed transaction (#8618) * Implement generalised parsing of aptos transactions + logic unit tests * feat(aptos): update aptos package (#8661) * feat(aptos): replace deprecated aptos package * feat(aptos): update aptos types * chore: update aptos api * feat(aptos): update aptos package * feat(aptos): update aptos package * fix(aptos): get network and indexer api url from network * fix(aptos): account address issues * refactor: set private methods * refactor: build transaction module and api * refactor: fix logic and constants * fix: set aptos client with our url paths. * test: fix imports from old package * fix: payload type * fix: logic code and tests * [QAA] adding Aptos send test (#8450) * test: adding aptos e2e send test * feat(aptos): send test * feat(aptos): uncomment previous tests * Aptos fixing: History isn't full (#8512) extending tx to ops with delegation batches and events * fix(aptos): amount balance (#8462) * Removing Delegated Balance from the Aptos Footer. * Prettify addition. * Removing unecessary code. * Removing uneeded imports * Fixing prettier issues. * Removing the footer and the translation units. * feat(aptos): add coin feature flag (#8498) * feat(aptos): add coin feature flag * feat(aptos): add coin feature flag for testnet * hw-app-aptos.js moved to lib/ledgerjs/packages/hw-app-aptos. Test file has been copied, but is still not active. * Repurposed README.md from algorand. removed some unused dependencies from the package. * Small cosmetic changes. * Managed to make tests work. * Feedback from PR. * chore: add xray ticket id --------- Co-authored-by: Pedro Semeano Co-authored-by: Oleksii Co-authored-by: João Martins <63311271+joaoccmartins@users.noreply.github.com> Co-authored-by: João Guimarães Co-authored-by: João Martins * Fix transaction parsing, after removing sender form aptos input argument (#8691) * fix(aptos): add amount validation (#8481) * test(aptos): Unit tests for Aptos API (#8693) * chore: save work * chore: save work * test: index api * Added a test file for the LedgerAccount based on Jest. * Feat/live 15457 aptos send receive verify get max send balance logic (#8725) Fix logic for getMaxSendBalance to use hardcoded values * chore: update dependencies * Removed additional settings from Ledger Live Desktop. * fix merge issues * feat: add aptos bot tests (#8598) * feat: add aptos bot testing --------- Co-authored-by: Samy RABAH-MONTAROU * feat(aptos): update node and indexer endpoints (#8773) feat: update aptos endpoints * test(aptos): bridge integration tests (#8721) * test: bridge integration test for aptos * test: bridge integration test for aptos working * test: disabled flaky test * test: create new snapshot * docs: add comment to flaky test * test: update to frozen accounts * test: update to frozen accounts * fix: remove commented import * fix: change expireTimestamp to milliseconds * fix: remove comments * fix: remove testSignedOperation * test: add burn address * test: debugging ci via console log * chore: remove console logs * test: fix estimateMaxSpendable assertion * fix: add missing import * fix: add missing import * fix: add burn address for aptos testnet * fix: prettier rule * fix: remove unused files * fix: abandon addresses * fix: dependencies * fix: update burn address * fix: resolver * chore: undo changes to pnpm-lock file * fix: imports * Feat/live 15540 aptos send receive fix when send max is toggled on the amount sent is different in ll and device (#8763) * Fix the transaction amount that is sent to the device for signing * remove Gas buffer for maxGasAmount * chore: save work * wip generate transaction test * test: add unit tests for generate transaction * chore: save work * test: unit tests for index api --------- Co-authored-by: Vladyslav Belyokhin Co-authored-by: Vladyslav Belyokhin Co-authored-by: Vladyslav Makarian Co-authored-by: Hedi EDELBLOUTE Co-authored-by: João Martins Co-authored-by: Oleksii Co-authored-by: João Martins <63311271+joaoccmartins@users.noreply.github.com> Co-authored-by: João Guimarães Co-authored-by: Victor <162306106+VicAlbr@users.noreply.github.com> Co-authored-by: João Guimarães Co-authored-by: Samy RABAH-MONTAROU --- .../src/families/aptos/api/index.test.ts | 497 +++++++++++++++++- .../src/families/aptos/api/index.ts | 15 +- 2 files changed, 488 insertions(+), 24 deletions(-) diff --git a/libs/ledger-live-common/src/families/aptos/api/index.test.ts b/libs/ledger-live-common/src/families/aptos/api/index.test.ts index 47a9fe436cf2..ca140f293687 100644 --- a/libs/ledger-live-common/src/families/aptos/api/index.test.ts +++ b/libs/ledger-live-common/src/families/aptos/api/index.test.ts @@ -1,22 +1,57 @@ import { ApolloClient } from "@apollo/client"; -import { Aptos, AptosConfig } from "@aptos-labs/ts-sdk"; +import { + AccountAddress, + Aptos, + ChainId, + Ed25519PublicKey, + InputEntryFunctionData, + RawTransaction, + Serializable, + post, +} from "@aptos-labs/ts-sdk"; +import network from "@ledgerhq/live-network/network"; +import BigNumber from "bignumber.js"; import { AptosAPI } from "."; +import { Account } from "../../../e2e/enum/Account"; jest.mock("@aptos-labs/ts-sdk"); jest.mock("@apollo/client"); -const mockedAptos = jest.mocked(Aptos); -const mockedAptosConfig = jest.mocked(AptosConfig); -const mockedApolloClient = jest.mocked(ApolloClient); +let mockedAptos; +let mockedApolloClient; +let mockedPost; + +jest.mock("@ledgerhq/live-network/network"); +const mockedNetwork = jest.mocked(network); describe("Aptos API", () => { - let api: AptosAPI; - const currencyId = "aptos"; + beforeEach(() => { + mockedAptos = jest.mocked(Aptos); + mockedApolloClient = jest.mocked(ApolloClient); + mockedPost = jest.mocked(post); + }); + + afterEach(() => jest.clearAllMocks()); - beforeAll(() => { - api = new AptosAPI(currencyId); + it("builds the client properly for mainnet", () => { + const api = new AptosAPI("aptos"); + + expect(api.broadcast).toBeDefined(); + expect(typeof api.broadcast).toBe("function"); + expect(api.estimateGasPrice).toBeDefined(); + expect(typeof api.estimateGasPrice).toBe("function"); + expect(api.generateTransaction).toBeDefined(); + expect(typeof api.generateTransaction).toBe("function"); + expect(api.getAccount).toBeDefined(); + expect(typeof api.getAccount).toBe("function"); + expect(api.getAccountInfo).toBeDefined(); + expect(typeof api.getAccountInfo).toBe("function"); + expect(api.simulateTransaction).toBeDefined(); + expect(typeof api.simulateTransaction).toBe("function"); }); - it("builds the client properly", () => { + it("builds the client properly for testnet", () => { + const api = new AptosAPI("aptos_testnet"); + expect(api.broadcast).toBeDefined(); expect(typeof api.broadcast).toBe("function"); expect(api.estimateGasPrice).toBeDefined(); @@ -29,9 +64,447 @@ describe("Aptos API", () => { expect(typeof api.getAccountInfo).toBe("function"); expect(api.simulateTransaction).toBeDefined(); expect(typeof api.simulateTransaction).toBe("function"); + }); + + describe("getAccount", () => { + it("calls getAccountInfo", async () => { + const mockGetAccountInfo = jest.fn(); + mockedAptos.mockImplementation(() => { + return { + getAccountInfo: mockGetAccountInfo, + }; + }); + + const mockSimpleSpy = jest.spyOn({ getAccount: mockGetAccountInfo }, "getAccount"); + + const api = new AptosAPI("aptos"); + await api.getAccount(Account.APTOS_1.address); + + expect(mockSimpleSpy).toHaveBeenCalledWith({ + accountAddress: Account.APTOS_1.address, + }); + }); + }); + + describe("getAccountInfo", () => { + it("calls getBalance, fetchTransactions and getHeight", async () => { + mockedAptos.mockImplementation(() => ({ + view: jest.fn().mockReturnValue(["123"]), + getTransactionByVersion: jest.fn().mockReturnValue({ + type: "user_transaction", + version: "v1", + }), + getBlockByVersion: jest.fn().mockReturnValue({ + block_height: "1", + block_hash: "83ca6d", + }), + })); + + mockedNetwork.mockResolvedValue( + Promise.resolve({ + data: { + account: { + account_number: 1, + sequence: 0, + pub_key: { key: "k", "@type": "type" }, + base_account: { + account_number: 2, + sequence: 42, + pub_key: { key: "k2", "@type": "type2" }, + }, + }, + block_height: "999", + }, + status: 200, + headers: {} as any, + statusText: "", + config: { + headers: {} as any, + }, + }), + ); + + mockedApolloClient.mockImplementation(() => ({ + query: async () => ({ + data: { + address_version_from_move_resources: [{ transaction_version: "v1" }], + }, + loading: false, + networkStatus: 7, + }), + })); + + const api = new AptosAPI("aptos"); + const accountInfo = await api.getAccountInfo(Account.APTOS_1.address, "1"); + + expect(accountInfo.balance).toEqual(new BigNumber(123)); + expect(accountInfo.transactions).toEqual([ + { + type: "user_transaction", + version: "v1", + block: { + height: 1, + hash: "83ca6d", + }, + }, + ]); + expect(accountInfo.blockHeight).toEqual(999); + }); + + it("return balance = 0 if it fails to fetch balance", async () => { + mockedAptos.mockImplementation(() => ({ + view: jest.fn().mockImplementation(() => { + throw new Error("error"); + }), + getTransactionByVersion: jest.fn().mockReturnValue({ + type: "user_transaction", + version: "v1", + }), + getBlockByVersion: jest.fn().mockReturnValue({ + block_height: "1", + block_hash: "83ca6d", + }), + })); + + mockedNetwork.mockResolvedValue( + Promise.resolve({ + data: { + account: { + account_number: 1, + sequence: 0, + pub_key: { key: "k", "@type": "type" }, + base_account: { + account_number: 2, + sequence: 42, + pub_key: { key: "k2", "@type": "type2" }, + }, + }, + block_height: "999", + }, + status: 200, + headers: {} as any, + statusText: "", + config: { + headers: {} as any, + }, + }), + ); + + mockedApolloClient.mockImplementation(() => ({ + query: async () => ({ + data: { + address_version_from_move_resources: [{ transaction_version: "v1" }], + }, + loading: false, + networkStatus: 7, + }), + })); + + const api = new AptosAPI("aptos"); + const accountInfo = await api.getAccountInfo(Account.APTOS_1.address, "1"); + + expect(accountInfo.balance).toEqual(new BigNumber(0)); + expect(accountInfo.transactions).toEqual([ + { + type: "user_transaction", + version: "v1", + block: { + height: 1, + hash: "83ca6d", + }, + }, + ]); + expect(accountInfo.blockHeight).toEqual(999); + }); + + it("returns no transactions if it the address is empty", async () => { + mockedAptos.mockImplementation(() => ({ + view: jest.fn().mockReturnValue(["123"]), + getTransactionByVersion: jest.fn().mockReturnValue({ + type: "user_transaction", + version: "v1", + }), + getBlockByVersion: jest.fn().mockReturnValue({ + block_height: "1", + block_hash: "83ca6d", + }), + })); + + mockedNetwork.mockResolvedValue( + Promise.resolve({ + data: { + account: { + account_number: 1, + sequence: 0, + pub_key: { key: "k", "@type": "type" }, + base_account: { + account_number: 2, + sequence: 42, + pub_key: { key: "k2", "@type": "type2" }, + }, + }, + block_height: "999", + }, + status: 200, + headers: {} as any, + statusText: "", + config: { + headers: {} as any, + }, + }), + ); + + mockedApolloClient.mockImplementation(() => ({ + query: async () => ({ + data: { + address_version_from_move_resources: [{ transaction_version: "v1" }], + }, + loading: false, + networkStatus: 7, + }), + })); + + const api = new AptosAPI("aptos"); + const accountInfo = await api.getAccountInfo("", "1"); + + expect(accountInfo.balance).toEqual(new BigNumber(123)); + expect(accountInfo.transactions).toEqual([]); + expect(accountInfo.blockHeight).toEqual(999); + }); + + it("returns a null transaction if it fails to getTransactionByVersion", async () => { + mockedAptos.mockImplementation(() => ({ + view: jest.fn().mockReturnValue(["123"]), + getTransactionByVersion: jest.fn().mockImplementation(() => { + throw new Error("error"); + }), + getBlockByVersion: jest.fn().mockReturnValue({ + block_height: "1", + block_hash: "83ca6d", + }), + })); + + mockedNetwork.mockResolvedValue( + Promise.resolve({ + data: { + account: { + account_number: 1, + sequence: 0, + pub_key: { key: "k", "@type": "type" }, + base_account: { + account_number: 2, + sequence: 42, + pub_key: { key: "k2", "@type": "type2" }, + }, + }, + block_height: "999", + }, + status: 200, + headers: {} as any, + statusText: "", + config: { + headers: {} as any, + }, + }), + ); + + mockedApolloClient.mockImplementation(() => ({ + query: async () => ({ + data: { + address_version_from_move_resources: [{ transaction_version: "v1" }], + }, + loading: false, + networkStatus: 7, + }), + })); + + const api = new AptosAPI("aptos"); + const accountInfo = await api.getAccountInfo(Account.APTOS_1.address, "1"); + + expect(accountInfo.balance).toEqual(new BigNumber(123)); + expect(accountInfo.transactions).toEqual([null]); + expect(accountInfo.blockHeight).toEqual(999); + }); + }); + + describe("estimateGasPrice", () => { + it("estimates the gas price", async () => { + const gasEstimation = { gas_estimate: 100 }; + mockedAptos.mockImplementation(() => ({ + getGasPriceEstimation: jest.fn().mockReturnValue(gasEstimation), + })); + + const api = new AptosAPI("aptos"); + const gasPrice = await api.estimateGasPrice(); + + expect(gasPrice.gas_estimate).toEqual(100); + }); + }); + + describe("generateTransaction", () => { + const payload: InputEntryFunctionData = { + function: "0x1::coin::transfer", + functionArguments: ["0x13", 1], + }; + + it("generates a transaction with the correct options", async () => { + const options = { + maxGasAmount: "100", + gasUnitPrice: "50", + sequenceNumber: "1", + expirationTimestampSecs: "1735639799486", + }; + + const mockSimple = jest.fn().mockImplementation(async () => ({ + rawTransaction: null, + })); + mockedAptos.mockImplementation(() => ({ + transaction: { + build: { + simple: mockSimple, + }, + }, + })); + + const mockSimpleSpy = jest.spyOn({ simple: mockSimple }, "simple"); + + const api = new AptosAPI("aptos"); + await api.generateTransaction(Account.APTOS_1.address, payload, options); + + expect(mockSimpleSpy).toHaveBeenCalledWith({ + data: payload, + options: { + maxGasAmount: Number(options.maxGasAmount), + gasUnitPrice: Number(options.gasUnitPrice), + accountSequenceNumber: Number(options.sequenceNumber), + expireTimestamp: Number(options.expirationTimestampSecs), + }, + sender: Account.APTOS_1.address, + }); + }); + + it("generates a transaction with no expire timestamp option set", async () => { + const options = { + maxGasAmount: "100", + gasUnitPrice: "50", + sequenceNumber: "1", + }; + + const mockSimple = jest.fn().mockImplementation(async () => ({ + rawTransaction: null, + })); + const mockGetLedgerInfo = jest.fn().mockImplementation(async () => ({ + ledger_timestamp: "0", + })); + mockedAptos.mockImplementation(() => ({ + transaction: { + build: { + simple: mockSimple, + }, + }, + getLedgerInfo: mockGetLedgerInfo, + })); + + const mockSimpleSpy = jest.spyOn({ simple: mockSimple }, "simple"); + + const api = new AptosAPI("aptos"); + await api.generateTransaction(Account.APTOS_1.address, payload, options); + + expect(mockSimpleSpy).toHaveBeenCalledWith({ + data: payload, + options: { + maxGasAmount: Number(options.maxGasAmount), + gasUnitPrice: Number(options.gasUnitPrice), + accountSequenceNumber: Number(options.sequenceNumber), + expireTimestamp: 120, + }, + sender: Account.APTOS_1.address, + }); + }); + + it("throws an error when failing to build a transaction", async () => { + const options = { + maxGasAmount: "100", + gasUnitPrice: "50", + sequenceNumber: "1", + expirationTimestampSecs: "1735639799486", + }; + + const mockSimple = jest.fn().mockImplementation(async () => null); + mockedAptos.mockImplementation(() => ({ + transaction: { + build: { + simple: mockSimple, + }, + }, + })); + + const api = new AptosAPI("aptos"); + expect( + async () => await api.generateTransaction(Account.APTOS_1.address, payload, options), + ).rejects.toThrow(); + }); + }); + + describe("simulateTransaction", () => { + it("simulates a transaction with the correct options", async () => { + const mockSimple = jest.fn().mockImplementation(async () => ({ + rawTransaction: null, + })); + mockedAptos.mockImplementation(() => ({ + transaction: { + simulate: { + simple: mockSimple, + }, + }, + })); + + const mockSimpleSpy = jest.spyOn({ simple: mockSimple }, "simple"); + + const api = new AptosAPI("aptos"); + const address = new Ed25519PublicKey(Account.APTOS_1.address); + const tx = new RawTransaction( + new AccountAddress(Uint8Array.from(Buffer.from(Account.APTOS_2.address))), + BigInt(1), + "" as unknown as Serializable, + BigInt(100), + BigInt(50), + BigInt(1), + { chainId: 1 } as ChainId, + ); + await api.simulateTransaction(address, tx); + + expect(mockSimpleSpy).toHaveBeenCalledWith({ + signerPublicKey: address, + transaction: { rawTransaction: tx }, + options: { + estimateGasUnitPrice: true, + estimateMaxGasAmount: true, + estimatePrioritizedGasUnitPrice: false, + }, + }); + }); + }); + describe("broadcast", () => { + it("broadcasts the transaction", async () => { + mockedPost.mockImplementation(async () => ({ data: { hash: "ok" } })); + const mockedPostSpy = jest.spyOn({ post: mockedPost }, "post"); + + mockedAptos.mockImplementation(() => ({ + config: "config", + })); + + const api = new AptosAPI("aptos"); + await api.broadcast("signature"); - expect(mockedAptos).toHaveBeenCalledTimes(1); - expect(mockedAptosConfig).toHaveBeenCalledTimes(1); - expect(mockedApolloClient).toHaveBeenCalledTimes(1); + expect(mockedPostSpy).toHaveBeenCalledWith({ + contentType: "application/x.aptos.signed_transaction+bcs", + aptosConfig: "config", + body: Uint8Array.from(Buffer.from("signature", "hex")), + path: "transactions", + type: "Fullnode", + originMethod: "", + }); + }); }); }); diff --git a/libs/ledger-live-common/src/families/aptos/api/index.ts b/libs/ledger-live-common/src/families/aptos/api/index.ts index 68e45e5f9f90..3a778e740864 100644 --- a/libs/ledger-live-common/src/families/aptos/api/index.ts +++ b/libs/ledger-live-common/src/families/aptos/api/index.ts @@ -22,11 +22,7 @@ import isUndefined from "lodash/isUndefined"; import { APTOS_ASSET_ID } from "../constants"; import { isTestnet } from "../logic"; import type { AptosTransaction, TransactionOptions } from "../types"; -import { - GetAccountTransactionsData, - GetAccountTransactionsDataGt, - GetAccountTransactionsDataLt, -} from "./graphql/queries"; +import { GetAccountTransactionsData, GetAccountTransactionsDataGt } from "./graphql/queries"; import { GetAccountTransactionsDataQuery, GetAccountTransactionsDataQueryVariables, @@ -70,7 +66,7 @@ export class AptosAPI { async getAccountInfo(address: string, startAt: string) { const [balance, transactions, blockHeight] = await Promise.all([ this.getBalance(address), - this.fetchTransactions(address, undefined, startAt), + this.fetchTransactions(address, startAt), this.getHeight(), ]); @@ -171,16 +167,12 @@ export class AptosAPI { } } - private async fetchTransactions(address: string, lt?: string, gt?: string) { + private async fetchTransactions(address: string, gt?: string) { if (!address) { return []; } - // WORKAROUND: Where is no way to pass optional bigint var to query let query = GetAccountTransactionsData; - if (lt) { - query = GetAccountTransactionsDataLt; - } if (gt) { query = GetAccountTransactionsDataGt; } @@ -195,7 +187,6 @@ export class AptosAPI { limit: 1000, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - lt, gt, }, fetchPolicy: "network-only",