From 316a586717ee56fe9692495b43b3378bb4ebd3aa Mon Sep 17 00:00:00 2001 From: OleksiiM Date: Mon, 16 Dec 2024 13:44:33 +0000 Subject: [PATCH 1/8] Bridge contains all methods --- .../src/families/aptos/bridge/js.test.ts | 37 +++++++++++++++++++ .../src/families/aptos/bridge/js.ts | 4 +- 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 libs/ledger-live-common/src/families/aptos/bridge/js.test.ts diff --git a/libs/ledger-live-common/src/families/aptos/bridge/js.test.ts b/libs/ledger-live-common/src/families/aptos/bridge/js.test.ts new file mode 100644 index 000000000000..e23cc961a167 --- /dev/null +++ b/libs/ledger-live-common/src/families/aptos/bridge/js.test.ts @@ -0,0 +1,37 @@ +import bridge from "./js"; + +describe("Aptos bridge interface ", () => { + describe("currencyBridge ", () => { + it("should contain all methods", () => { + expect(bridge.currencyBridge.preload).toBeDefined(); + expect(typeof bridge.currencyBridge.preload).toBe("function"); + expect(bridge.currencyBridge.hydrate).toBeDefined(); + expect(typeof bridge.currencyBridge.hydrate).toBe("function"); + expect(bridge.currencyBridge.scanAccounts).toBeDefined(); + expect(typeof bridge.currencyBridge.scanAccounts).toBe("function"); + }); + }); + + describe("accountBridge ", () => { + it("should contain all methods", () => { + expect(bridge.accountBridge.estimateMaxSpendable).toBeDefined(); + expect(typeof bridge.accountBridge.estimateMaxSpendable).toBe("function"); + expect(bridge.accountBridge.createTransaction).toBeDefined(); + expect(typeof bridge.accountBridge.createTransaction).toBe("function"); + expect(bridge.accountBridge.updateTransaction).toBeDefined(); + expect(typeof bridge.accountBridge.updateTransaction).toBe("function"); + expect(bridge.accountBridge.getTransactionStatus).toBeDefined(); + expect(typeof bridge.accountBridge.getTransactionStatus).toBe("function"); + expect(bridge.accountBridge.prepareTransaction).toBeDefined(); + expect(typeof bridge.accountBridge.prepareTransaction).toBe("function"); + expect(bridge.accountBridge.sync).toBeDefined(); + expect(typeof bridge.accountBridge.sync).toBe("function"); + expect(bridge.accountBridge.receive).toBeDefined(); + expect(typeof bridge.accountBridge.receive).toBe("function"); + expect(bridge.accountBridge.signOperation).toBeDefined(); + expect(typeof bridge.accountBridge.signOperation).toBe("function"); + expect(bridge.accountBridge.broadcast).toBeDefined(); + expect(typeof bridge.accountBridge.broadcast).toBe("function"); + }); + }); +}); diff --git a/libs/ledger-live-common/src/families/aptos/bridge/js.ts b/libs/ledger-live-common/src/families/aptos/bridge/js.ts index c2a219eda8f9..1c5a43de034e 100644 --- a/libs/ledger-live-common/src/families/aptos/bridge/js.ts +++ b/libs/ledger-live-common/src/families/aptos/bridge/js.ts @@ -11,8 +11,6 @@ import estimateMaxSpendable from "../js-estimateMaxSpendable"; import signOperation from "../js-signOperation"; import broadcast from "../js-broadcast"; -const receive = makeAccountBridgeReceive(); - const currencyBridge: CurrencyBridge = { preload: () => Promise.resolve({}), hydrate: () => {}, @@ -24,6 +22,8 @@ const updateTransaction = (t: Transaction, patch: Partial): Transac ...patch, }); +const receive = makeAccountBridgeReceive(); + const accountBridge: AccountBridge = { estimateMaxSpendable, createTransaction, From d84750b89d5ae1c117a8d8cec4c785f4712fbc91 Mon Sep 17 00:00:00 2001 From: OleksiiM Date: Mon, 16 Dec 2024 17:00:30 +0000 Subject: [PATCH 2/8] Add logic tests --- .../families/aptos/js-getTransactionStatus.ts | 3 +- .../src/families/aptos/logic.test.ts | 99 ++++++++++++++++++- .../src/families/aptos/logic.ts | 34 +------ 3 files changed, 104 insertions(+), 32 deletions(-) diff --git a/libs/ledger-live-common/src/families/aptos/js-getTransactionStatus.ts b/libs/ledger-live-common/src/families/aptos/js-getTransactionStatus.ts index f42e2a0b5523..a15dba59f484 100644 --- a/libs/ledger-live-common/src/families/aptos/js-getTransactionStatus.ts +++ b/libs/ledger-live-common/src/families/aptos/js-getTransactionStatus.ts @@ -18,6 +18,7 @@ import { SequenseNumberTooOldError, TransactionExpiredError, } from "./errors"; +import { AccountAddress } from "@aptos-labs/ts-sdk"; const getTransactionStatus = async (a: Account, t: Transaction): Promise => { const errors: Record = {}; @@ -44,7 +45,7 @@ const getTransactionStatus = async (a: Account, t: Transaction): Promise ({ + getCryptoCurrencyById: jest.fn(), +})); + +describe("Aptos logic ", () => { + describe("isTestnet", () => { + it("should return true for testnet currencies", () => { + expect(isTestnet("aptos_testnet")).toBe(true); + }); + + it("should return false for mainnet currencies", () => { + expect(isTestnet("aptos")).toBe(false); + }); + }); + + describe("getMaxSendBalance", () => { + it("should return the correct max send balance", () => { + const amount = new BigNumber(1000000); + const result = getMaxSendBalance(amount); + expect(result.isEqualTo(amount.minus(BigNumber((2000 + 5) * 100)))).toBe(true); + }); + + // TOFIX: this test should be updated after validation of the getMaxSendBalance strategy for small amounts + // it("should return the original amount if it is less than the total gas", () => { + // const amount = new BigNumber(1000); + // const result = getMaxSendBalance(amount); + // expect(result).toEqual(amount); + // }); + }); + + describe("normalizeTransactionOptions", () => { + it("should normalize transaction options", () => { + const options: Transaction["options"] = { + maxGasAmount: "1000", + gasUnitPrice: "10", + sequenceNumber: "1", + expirationTimestampSecs: "1000000", + }; + + const result = normalizeTransactionOptions(options); + expect(result).toEqual(options); + }); + + it("should return undefined for empty values", () => { + const options: Transaction["options"] = { + maxGasAmount: "", + gasUnitPrice: "", + sequenceNumber: undefined, + expirationTimestampSecs: "1000000", + }; + + const result = normalizeTransactionOptions(options); + expect(result).toEqual({ + maxGasAmount: undefined, + gasUnitPrice: undefined, + sequenceNumber: undefined, + expirationTimestampSecs: "1000000", + }); + }); + }); + + describe("getBlankOperation", () => { + it("should return a blank operation", () => { + const tx: AptosTransaction = { + hash: "0x123", + block: { hash: "0xabc", height: 1 }, + 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: "0xabc", + blockHeight: 1, + senders: [], + recipients: [], + accountId: id, + date: new Date(1000), + extra: { version: undefined }, + transactionSequenceNumber: 1, + hasFailed: false, + }); + }); + }); +}); describe("Aptos sync logic ", () => { describe("compareAddress", () => { diff --git a/libs/ledger-live-common/src/families/aptos/logic.ts b/libs/ledger-live-common/src/families/aptos/logic.ts index 39f9ba7ceb8f..9705a0bdb9f8 100644 --- a/libs/ledger-live-common/src/families/aptos/logic.ts +++ b/libs/ledger-live-common/src/families/aptos/logic.ts @@ -16,33 +16,14 @@ import { DIRECTION, TRANSFER_TYPES, } from "./constants"; -import type { AptosTransaction, Transaction } from "./types"; +import type { AptosTransaction, TransactionOptions } from "./types"; export const DEFAULT_GAS = 200; export const DEFAULT_GAS_PRICE = 100; export const ESTIMATE_GAS_MUL = 1.2; // defines buffer for gas estimation change -const HEX_REGEXP = /^[-+]?[a-f0-9]+\.?[a-f0-9]*?$/i; const CLEAN_HEX_REGEXP = /^0x0*|^0+/; -const LENGTH_WITH_0x = 66; - -export function isValidAddress(address = ""): boolean { - let str = address; - - const validAddressWithOx = address.startsWith("0x") && address.length === LENGTH_WITH_0x; - - if (!validAddressWithOx) return false; - - str = str.substring(2); - - return isValidHex(str); -} - -function isValidHex(hex: string): boolean { - return HEX_REGEXP.test(hex); -} - export function isTestnet(currencyId: string): boolean { return getCryptoCurrencyById(currencyId).isTestnetFor ? true : false; } @@ -57,15 +38,8 @@ export const getMaxSendBalance = ( return amount.gt(totalGas) ? amount.minus(totalGas) : new BigNumber(0); }; -export function normalizeTransactionOptions( - options: Transaction["options"], -): Transaction["options"] { - const check = (v: any) => { - if (v === undefined || v === null || v === "") { - return undefined; - } - return v; - }; +export function normalizeTransactionOptions(options: TransactionOptions): TransactionOptions { + const check = (v: any) => ((v ?? "").toString().trim() ? v : undefined); return { maxGasAmount: check(options.maxGasAmount), gasUnitPrice: check(options.gasUnitPrice), @@ -74,7 +48,7 @@ export function normalizeTransactionOptions( }; } -const getBlankOperation = ( +export const getBlankOperation = ( tx: AptosTransaction, id: string, ): Operation> => ({ From 1db222d6ecc500e1e29b9af96ae05732c52b384f Mon Sep 17 00:00:00 2001 From: OleksiiM Date: Thu, 19 Dec 2024 09:56:33 +0000 Subject: [PATCH 3/8] Update logic.ts:getMaxSendBalance tests --- .../src/families/aptos/logic.test.ts | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/libs/ledger-live-common/src/families/aptos/logic.test.ts b/libs/ledger-live-common/src/families/aptos/logic.test.ts index 140d9360a27c..8999c72e27a2 100644 --- a/libs/ledger-live-common/src/families/aptos/logic.test.ts +++ b/libs/ledger-live-common/src/families/aptos/logic.test.ts @@ -32,18 +32,45 @@ describe("Aptos logic ", () => { }); describe("getMaxSendBalance", () => { - it("should return the correct max send balance", () => { + it("should return the correct max send balance when amount is greater than total gas", () => { const amount = new BigNumber(1000000); - const result = getMaxSendBalance(amount); - expect(result.isEqualTo(amount.minus(BigNumber((2000 + 5) * 100)))).toBe(true); + const gas = new BigNumber(200); + const gasPrice = new BigNumber(100); + const result = getMaxSendBalance(amount, gas, gasPrice); + expect(result.isEqualTo(amount.minus(gas.multipliedBy(gasPrice)))).toBe(true); }); - // TOFIX: this test should be updated after validation of the getMaxSendBalance strategy for small amounts - // it("should return the original amount if it is less than the total gas", () => { - // const amount = new BigNumber(1000); - // const result = getMaxSendBalance(amount); - // expect(result).toEqual(amount); - // }); + it("should return zero when amount is less than total gas", () => { + const amount = new BigNumber(1000); + const gas = new BigNumber(200); + const gasPrice = new BigNumber(100); + const result = getMaxSendBalance(amount, gas, gasPrice); + expect(result.isEqualTo(new BigNumber(0))).toBe(true); + }); + + it("should return zero when amount is equal to total gas", () => { + const amount = new BigNumber(20000); + const gas = new BigNumber(200); + const gasPrice = new BigNumber(100); + const result = getMaxSendBalance(amount, gas, gasPrice); + expect(result.isEqualTo(new BigNumber(0))).toBe(true); + }); + + it("should handle zero amount", () => { + const amount = new BigNumber(0); + const gas = new BigNumber(200); + const gasPrice = new BigNumber(100); + const result = getMaxSendBalance(amount, gas, gasPrice); + expect(result.isEqualTo(new BigNumber(0))).toBe(true); + }); + + it("should handle zero gas and gas price", () => { + const amount = new BigNumber(1000000); + const gas = new BigNumber(0); + const gasPrice = new BigNumber(0); + const result = getMaxSendBalance(amount, gas, gasPrice); + expect(result.isEqualTo(amount)).toBe(true); + }); }); describe("normalizeTransactionOptions", () => { @@ -543,4 +570,5 @@ describe("Aptos sync logic ", () => { expect(result).toEqual(new BigNumber(90).negated()); // 100 - 10 }); }); + }); From 65fb8750bf8ec46425a08abaa36e9520d5eff8bb Mon Sep 17 00:00:00 2001 From: OleksiiM Date: Thu, 19 Dec 2024 12:01:08 +0000 Subject: [PATCH 4/8] Add prepareTransaction tests --- .../families/aptos/js-getTransactionStatus.ts | 1 - .../aptos/js-prepareTransaction.test.ts | 151 ++++++++++++++++++ 2 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 libs/ledger-live-common/src/families/aptos/js-prepareTransaction.test.ts diff --git a/libs/ledger-live-common/src/families/aptos/js-getTransactionStatus.ts b/libs/ledger-live-common/src/families/aptos/js-getTransactionStatus.ts index a15dba59f484..5c9f54243a1d 100644 --- a/libs/ledger-live-common/src/families/aptos/js-getTransactionStatus.ts +++ b/libs/ledger-live-common/src/families/aptos/js-getTransactionStatus.ts @@ -12,7 +12,6 @@ import type { Account } from "@ledgerhq/types-live"; import type { TransactionStatus } from "../..//generated/types"; import type { Transaction } from "./types"; -import { isValidAddress } from "./logic"; import { SequenseNumberTooNewError, SequenseNumberTooOldError, diff --git a/libs/ledger-live-common/src/families/aptos/js-prepareTransaction.test.ts b/libs/ledger-live-common/src/families/aptos/js-prepareTransaction.test.ts new file mode 100644 index 000000000000..f43aa1968f41 --- /dev/null +++ b/libs/ledger-live-common/src/families/aptos/js-prepareTransaction.test.ts @@ -0,0 +1,151 @@ +import prepareTransaction from "./js-prepareTransaction"; +import { AptosAPI } from "./api"; +import { getEstimatedGas } from "./js-getFeesForTransaction"; +import { getMaxSendBalance } from "./logic"; +import BigNumber from "bignumber.js"; +import type { Account } from "@ledgerhq/types-live"; +import type { Transaction } from "./types"; + +jest.mock("./api"); +jest.mock("./js-getFeesForTransaction"); +jest.mock("./logic"); + +describe("Aptos prepareTransaction", () => { + describe("prepareTransaction", () => { + let account: Account; + let transaction: Transaction; + + beforeEach(() => { + account = { + id: "test-account-id", + name: "Test Account", + currency: { + id: "aptos", + name: "Aptos", + ticker: "APT", + units: [{ name: "Aptos", code: "APT", magnitude: 6 }], + }, + spendableBalance: new BigNumber(1000), + balance: new BigNumber(1000), + blockHeight: 0, + operations: [], + pendingOperations: [], + unit: { code: "APT", name: "Aptos", magnitude: 6 }, + lastSyncDate: new Date(), + subAccounts: [], + } as unknown as Account; + + transaction = { + amount: new BigNumber(0), + recipient: "", + useAllAmount: false, + fees: new BigNumber(0), + firstEmulation: true, + options: {}, + } as Transaction; + }); + + it("should return the transaction if recipient is not set", async () => { + const result = await prepareTransaction(account, transaction); + expect(result).toEqual(transaction); + }); + + it("should return the transaction with zero fees if amount is zero and useAllAmount is false", async () => { + transaction.recipient = "test-recipient"; + const result = await prepareTransaction(account, transaction); + expect(result.fees?.isZero()).toBe(true); + }); + + it("should set the amount to max sendable balance if useAllAmount is true", async () => { + transaction.recipient = "test-recipient"; + transaction.useAllAmount = true; + (getMaxSendBalance as jest.Mock).mockReturnValue(new BigNumber(900)); + (getEstimatedGas as jest.Mock).mockResolvedValue({ + fees: new BigNumber(2000), + estimate: { maxGasAmount: new BigNumber(200), gasUnitPrice: new BigNumber(10) }, + errors: {}, + }); + + const result = await prepareTransaction(account, transaction); + expect(result.amount.isEqualTo(new BigNumber(900))).toBe(true); + expect(result.fees?.isEqualTo(new BigNumber(2000))).toBe(true); + expect(new BigNumber(result.estimate.maxGasAmount).isEqualTo(new BigNumber(200))).toBe(true); + expect(result.errors).toEqual({}); + }); + + it("should call getEstimatedGas and set the transaction fees, estimate, and errors", async () => { + transaction.recipient = "test-recipient"; + transaction.amount = new BigNumber(100); + (getEstimatedGas as jest.Mock).mockResolvedValue({ + fees: new BigNumber(10), + estimate: { maxGasAmount: new BigNumber(200) }, + errors: {}, + }); + + const result = await prepareTransaction(account, transaction); + expect(getEstimatedGas).toHaveBeenCalledWith(account, transaction, expect.any(AptosAPI)); + expect(result.fees?.isEqualTo(new BigNumber(10))).toBe(true); + expect(new BigNumber(result.estimate.maxGasAmount).isEqualTo(new BigNumber(200))).toBe(true); + expect(result.errors).toEqual({}); + }); + + it("should set firstEmulation to false after the first call", async () => { + transaction.recipient = "test-recipient"; + transaction.amount = new BigNumber(100); + (getEstimatedGas as jest.Mock).mockResolvedValue({ + fees: new BigNumber(10), + estimate: { maxGasAmount: new BigNumber(200) }, + errors: {}, + }); + + const result = await prepareTransaction(account, transaction); + expect(result.firstEmulation).toBe(false); + }); + + //-------------------------------------------------------------------------------- + it("should return the transaction with updated fees and estimate if recipient is set and amount is not zero", async () => { + transaction.recipient = "test-recipient"; + transaction.amount = new BigNumber(100); + (getEstimatedGas as jest.Mock).mockResolvedValue({ + fees: new BigNumber(2000), + estimate: { maxGasAmount: new BigNumber(200), gasUnitPrice: new BigNumber(10) }, + errors: {}, + }); + + const result = await prepareTransaction(account, transaction); + expect(result.fees?.isEqualTo(new BigNumber(2000))).toBe(true); + expect(new BigNumber(result.estimate.maxGasAmount).isEqualTo(new BigNumber(200))).toBe(true); + expect(result.errors).toEqual({}); + }); + + it("should set maxGasAmount in options if firstEmulation is true", async () => { + transaction.recipient = "test-recipient"; + transaction.amount = new BigNumber(100); + transaction.firstEmulation = true; + (getEstimatedGas as jest.Mock).mockResolvedValue({ + fees: new BigNumber(2000), + estimate: { maxGasAmount: new BigNumber(200), gasUnitPrice: new BigNumber(10) }, + errors: {}, + }); + + const result = await prepareTransaction(account, transaction); + expect(new BigNumber(result.estimate.maxGasAmount).isEqualTo(new BigNumber(200))).toBe(true); + expect(result.firstEmulation).toBe(false); + }); + + it("should not change transaction.options.maxGasAmount if firstEmulation is false", async () => { + transaction.recipient = "test-recipient"; + transaction.amount = new BigNumber(100); + transaction.firstEmulation = false; + (getEstimatedGas as jest.Mock).mockResolvedValue({ + fees: new BigNumber(2000), + estimate: { maxGasAmount: new BigNumber(200), gasUnitPrice: new BigNumber(10) }, + errors: {}, + }); + + const result = await prepareTransaction(account, transaction); + expect(result.options.maxGasAmount).toBeUndefined(); + expect(result.firstEmulation).toBe(false); + }); + }); +}); From 17aae7796db56d25c5c9b7c68c79da9f12667ac5 Mon Sep 17 00:00:00 2001 From: OleksiiM Date: Thu, 2 Jan 2025 21:10:25 +0000 Subject: [PATCH 5/8] Fix conflicts --- ...-prepareTransaction.test.ts => prepareTransaction.test.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename libs/ledger-live-common/src/families/aptos/{js-prepareTransaction.test.ts => prepareTransaction.test.ts} (98%) diff --git a/libs/ledger-live-common/src/families/aptos/js-prepareTransaction.test.ts b/libs/ledger-live-common/src/families/aptos/prepareTransaction.test.ts similarity index 98% rename from libs/ledger-live-common/src/families/aptos/js-prepareTransaction.test.ts rename to libs/ledger-live-common/src/families/aptos/prepareTransaction.test.ts index f43aa1968f41..fbc7f36aebed 100644 --- a/libs/ledger-live-common/src/families/aptos/js-prepareTransaction.test.ts +++ b/libs/ledger-live-common/src/families/aptos/prepareTransaction.test.ts @@ -1,6 +1,6 @@ -import prepareTransaction from "./js-prepareTransaction"; +import prepareTransaction from "./prepareTransaction"; import { AptosAPI } from "./api"; -import { getEstimatedGas } from "./js-getFeesForTransaction"; +import { getEstimatedGas } from "./getFeesForTransaction"; import { getMaxSendBalance } from "./logic"; import BigNumber from "bignumber.js"; import type { Account } from "@ledgerhq/types-live"; From 3539a519e87de067048c4e3a6155178b9764c34c Mon Sep 17 00:00:00 2001 From: OleksiiM Date: Fri, 3 Jan 2025 13:18:22 +0000 Subject: [PATCH 6/8] Update test to full coverage --- .../src/families/aptos/bridge/js.test.ts | 77 +++++- .../src/families/aptos/broadcast.test.ts | 101 ++++++++ .../src/families/aptos/logic.test.ts | 237 +++++++++++++++++- .../src/families/aptos/logic.ts | 2 +- .../families/aptos/prepareTransaction.test.ts | 19 +- 5 files changed, 412 insertions(+), 24 deletions(-) create mode 100644 libs/ledger-live-common/src/families/aptos/broadcast.test.ts diff --git a/libs/ledger-live-common/src/families/aptos/bridge/js.test.ts b/libs/ledger-live-common/src/families/aptos/bridge/js.test.ts index e23cc961a167..ce24ff1fe361 100644 --- a/libs/ledger-live-common/src/families/aptos/bridge/js.test.ts +++ b/libs/ledger-live-common/src/families/aptos/bridge/js.test.ts @@ -1,14 +1,35 @@ +import BigNumber from "bignumber.js"; import bridge from "./js"; +import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies"; describe("Aptos bridge interface ", () => { - describe("currencyBridge ", () => { - it("should contain all methods", () => { - expect(bridge.currencyBridge.preload).toBeDefined(); - expect(typeof bridge.currencyBridge.preload).toBe("function"); + describe("currencyBridge", () => { + it("should have a preload method that returns a promise", async () => { + const cryptoCurrency = getCryptoCurrencyById("aptos"); + const result = bridge.currencyBridge.preload(cryptoCurrency); + expect(result).toBeInstanceOf(Promise); + await expect(result).resolves.toEqual({}); + }); + + it("should have a hydrate method that is a function", () => { expect(bridge.currencyBridge.hydrate).toBeDefined(); expect(typeof bridge.currencyBridge.hydrate).toBe("function"); + const cryptoCurrency = getCryptoCurrencyById("aptos"); + const result = bridge.currencyBridge.hydrate({}, cryptoCurrency); + expect(result).toBeUndefined(); + }); + + it("should have a scanAccounts method that is a function", () => { expect(bridge.currencyBridge.scanAccounts).toBeDefined(); expect(typeof bridge.currencyBridge.scanAccounts).toBe("function"); + const cryptoCurrency = getCryptoCurrencyById("aptos"); + const deviceId = "test-device"; + const result = bridge.currencyBridge.scanAccounts({ + currency: cryptoCurrency, + deviceId, + syncConfig: { paginationConfig: {} }, + }); + expect(result).toBeDefined(); }); }); @@ -34,4 +55,52 @@ describe("Aptos bridge interface ", () => { expect(typeof bridge.accountBridge.broadcast).toBe("function"); }); }); + describe("updateTransaction", () => { + it("should update the transaction with the given patch", () => { + const initialTransaction = { + amount: new BigNumber(100), + recipient: "address1", + mode: "send", + family: "aptos" as const, + options: { maxGasAmount: "", gasUnitPrice: "" }, + estimate: { maxGasAmount: "", gasUnitPrice: "" }, + firstEmulation: true, + }; + const patch = { amount: new BigNumber(200) }; + const updatedTransaction = bridge.accountBridge.updateTransaction(initialTransaction, patch); + expect(updatedTransaction).toEqual({ + amount: new BigNumber(200), + recipient: "address1", + mode: "send", + family: "aptos" as const, + options: { maxGasAmount: "", gasUnitPrice: "" }, + estimate: { maxGasAmount: "", gasUnitPrice: "" }, + firstEmulation: true, + }); + }); + + it("should not modify the original transaction object", () => { + const initialTransaction = { + amount: new BigNumber(100), + recipient: "address1", + mode: "send", + family: "aptos" as const, + options: { maxGasAmount: "", gasUnitPrice: "" }, + estimate: { maxGasAmount: "", gasUnitPrice: "" }, + firstEmulation: true, + }; + const patch = { amount: new BigNumber(200) }; + const updatedTransaction = bridge.accountBridge.updateTransaction(initialTransaction, patch); + expect(initialTransaction).toEqual({ + amount: new BigNumber(100), + recipient: "address1", + mode: "send", + family: "aptos" as const, + options: { maxGasAmount: "", gasUnitPrice: "" }, + estimate: { maxGasAmount: "", gasUnitPrice: "" }, + firstEmulation: true, + }); + expect(updatedTransaction).not.toBe(initialTransaction); + }); + }); }); diff --git a/libs/ledger-live-common/src/families/aptos/broadcast.test.ts b/libs/ledger-live-common/src/families/aptos/broadcast.test.ts new file mode 100644 index 000000000000..54cf9907f8d5 --- /dev/null +++ b/libs/ledger-live-common/src/families/aptos/broadcast.test.ts @@ -0,0 +1,101 @@ +import broadcast from "./broadcast"; +import { AptosAPI } from "./api"; +import { patchOperationWithHash } from "./../../operation"; +import { log } from "@ledgerhq/logs"; +import type { Account, Operation, SignedOperation } from "@ledgerhq/types-live"; +import BigNumber from "bignumber.js"; + +jest.mock("./api"); +jest.mock("./../../operation"); +jest.mock("@ledgerhq/logs"); + +describe("broadcast", () => { + const mockAccount: Account = { + type: "Account", + seedIdentifier: "mockSeedIdentifier", + operationsCount: 0, + id: "mockAccountId", + currency: { + type: "CryptoCurrency", + id: "aptos", + name: "Aptos", + ticker: "APT", + units: [{ name: "APT", code: "APT", magnitude: 6 }], + managerAppName: "Aptos", + coinType: 637, + scheme: "aptos", + color: "#000000", + family: "aptos", + blockAvgTime: 5, + explorerViews: [], + }, + balance: BigNumber(1000), + spendableBalance: BigNumber(1000), + operations: [], + pendingOperations: [], + lastSyncDate: new Date(), + blockHeight: 0, + index: 0, + derivationMode: "", + freshAddress: "", + freshAddressPath: "", + used: false, + swapHistory: [], + creationDate: new Date(), + balanceHistoryCache: { + HOUR: { latestDate: 0, balances: [] }, + DAY: { latestDate: 0, balances: [] }, + WEEK: { latestDate: 0, balances: [] }, + }, + }; + + const mockOperation: Operation = { + id: "mockOperationId", + hash: "", + type: "OUT", + value: BigNumber(100), + fee: BigNumber(1), + senders: ["sender"], + recipients: ["recipient"], + blockHeight: null, + blockHash: null, + accountId: "mockAccountId", + date: new Date(), + extra: {}, + }; + + const mockSignedOperation: SignedOperation = { + operation: mockOperation, + signature: "mockSignature", + }; + + it("should broadcast the signed operation and return the patched operation", async () => { + const mockHash = "mockHash"; + (AptosAPI.prototype.broadcast as jest.Mock).mockResolvedValue(mockHash); + (patchOperationWithHash as jest.Mock).mockReturnValue({ + ...mockOperation, + hash: mockHash, + }); + + const result = await broadcast({ + signedOperation: mockSignedOperation, + account: mockAccount, + }); + + expect(AptosAPI.prototype.broadcast).toHaveBeenCalledWith("mockSignature"); + expect(patchOperationWithHash).toHaveBeenCalledWith(mockOperation, mockHash); + expect(log).toHaveBeenCalledWith("INFO", "APTOS_OP", mockOperation); + expect(result).toEqual({ ...mockOperation, hash: mockHash }); + }); + + it("should throw an error if broadcast fails", async () => { + (AptosAPI.prototype.broadcast as jest.Mock).mockRejectedValue(new Error("Broadcast failed")); + + await expect( + broadcast({ + signedOperation: mockSignedOperation, + account: mockAccount, + }), + ).rejects.toThrow("Broadcast failed"); + }); +}); diff --git a/libs/ledger-live-common/src/families/aptos/logic.test.ts b/libs/ledger-live-common/src/families/aptos/logic.test.ts index 8999c72e27a2..9c9d29404c23 100644 --- a/libs/ledger-live-common/src/families/aptos/logic.test.ts +++ b/libs/ledger-live-common/src/families/aptos/logic.test.ts @@ -1,7 +1,12 @@ -import { Event, InputEntryFunctionData, WriteSetChange } from "@aptos-labs/ts-sdk"; +import { + EntryFunctionPayloadResponse, + Event, + InputEntryFunctionData, + WriteSetChange, +} from "@aptos-labs/ts-sdk"; import type { Operation, OperationType } from "@ledgerhq/types-live"; import BigNumber from "bignumber.js"; -import { APTOS_ASSET_ID, APTOS_COIN_CHANGE } from "./constants"; +import { APTOS_ASSET_ID, APTOS_COIN_CHANGE, DIRECTION } from "./constants"; import { calculateAmount, compareAddress, @@ -13,6 +18,7 @@ import { getMaxSendBalance, normalizeTransactionOptions, getBlankOperation, + txsToOps, } from "./logic"; import type { AptosTransaction, Transaction } from "./types"; @@ -571,4 +577,231 @@ describe("Aptos sync logic ", () => { }); }); + describe("txsToOps", () => { + it("should convert transactions to operations correctly", () => { + 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 = txsToOps({ address }, id, txs); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + id: expect.any(String), + hash: "0x123", + type: DIRECTION.OUT, + value: new BigNumber(20100), + fee: new BigNumber(20000), + blockHash: "0xabc", + blockHeight: 1, + senders: ["0x11"], + recipients: ["0x12"], + accountId: id, + date: new Date(1000), + extra: { version: undefined }, + transactionSequenceNumber: 1, + hasFailed: false, + }); + }); + + it("should skip transactions without functions in payload", () => { + 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, + // payload: { + // type: "entry_function_payload", + // function: "0x1::coin::transfer", + // type_arguments: [], + // arguments: ["0x12", 100], + // } as EntryFunctionPayloadResponse, + events: [], + changes: [], + block: { hash: "0xabc", height: 1 }, + timestamp: "1000000", + sequence_number: "1", + } as unknown as AptosTransaction, + ]; + + const result = txsToOps({ address }, id, txs); + + expect(result).toHaveLength(0); + }); + + it("should skip transactions that result in no Aptos change", () => { + 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 = txsToOps({ address }, id, txs); + + expect(result).toHaveLength(0); + }); + + it("should handle failed transactions", () => { + const address = "0x11"; + const id = "test-id"; + const txs: AptosTransaction[] = [ + { + hash: "0x123", + sender: "0x11", + gas_used: "200", + gas_unit_price: "100", + success: false, + 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 = txsToOps({ address }, id, txs); + + expect(result).toHaveLength(1); + expect(result[0].hasFailed).toBe(true); + }); + }); }); diff --git a/libs/ledger-live-common/src/families/aptos/logic.ts b/libs/ledger-live-common/src/families/aptos/logic.ts index 6193e17d12da..33a7a4db00a5 100644 --- a/libs/ledger-live-common/src/families/aptos/logic.ts +++ b/libs/ledger-live-common/src/families/aptos/logic.ts @@ -128,7 +128,7 @@ export function compareAddress(addressA: string, addressB: string) { } export function getFunctionAddress(payload: InputEntryFunctionData): string | undefined { - if ("function" in payload) { + if (payload.function) { const parts = payload.function.split("::"); return parts.length === 3 && parts[0].length ? parts[0] : undefined; } diff --git a/libs/ledger-live-common/src/families/aptos/prepareTransaction.test.ts b/libs/ledger-live-common/src/families/aptos/prepareTransaction.test.ts index fbc7f36aebed..689b967f7280 100644 --- a/libs/ledger-live-common/src/families/aptos/prepareTransaction.test.ts +++ b/libs/ledger-live-common/src/families/aptos/prepareTransaction.test.ts @@ -7,7 +7,7 @@ import type { Account } from "@ledgerhq/types-live"; import type { Transaction } from "./types"; jest.mock("./api"); -jest.mock("./js-getFeesForTransaction"); +jest.mock("./getFeesForTransaction"); jest.mock("./logic"); describe("Aptos prepareTransaction", () => { @@ -118,7 +118,7 @@ describe("Aptos prepareTransaction", () => { expect(result.errors).toEqual({}); }); - it("should set maxGasAmount in options if firstEmulation is true", async () => { + it("should set maxGasAmount in options", async () => { transaction.recipient = "test-recipient"; transaction.amount = new BigNumber(100); transaction.firstEmulation = true; @@ -132,20 +132,5 @@ describe("Aptos prepareTransaction", () => { expect(new BigNumber(result.estimate.maxGasAmount).isEqualTo(new BigNumber(200))).toBe(true); expect(result.firstEmulation).toBe(false); }); - - it("should not change transaction.options.maxGasAmount if firstEmulation is false", async () => { - transaction.recipient = "test-recipient"; - transaction.amount = new BigNumber(100); - transaction.firstEmulation = false; - (getEstimatedGas as jest.Mock).mockResolvedValue({ - fees: new BigNumber(2000), - estimate: { maxGasAmount: new BigNumber(200), gasUnitPrice: new BigNumber(10) }, - errors: {}, - }); - - const result = await prepareTransaction(account, transaction); - expect(result.options.maxGasAmount).toBeUndefined(); - expect(result.firstEmulation).toBe(false); - }); }); }); From bf8f7eb17ed4ed2a68a1236b30015c668f3f82e3 Mon Sep 17 00:00:00 2001 From: OleksiiM Date: Fri, 3 Jan 2025 14:12:48 +0000 Subject: [PATCH 7/8] Fix getTransactionStatus tests --- .../src/families/aptos/getTransactionStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/ledger-live-common/src/families/aptos/getTransactionStatus.ts b/libs/ledger-live-common/src/families/aptos/getTransactionStatus.ts index 3c243b2f63f5..469954737047 100644 --- a/libs/ledger-live-common/src/families/aptos/getTransactionStatus.ts +++ b/libs/ledger-live-common/src/families/aptos/getTransactionStatus.ts @@ -44,7 +44,7 @@ const getTransactionStatus = async (a: Account, t: Transaction): Promise Date: Fri, 3 Jan 2025 15:10:24 +0000 Subject: [PATCH 8/8] review update --- .../src/families/aptos/prepareTransaction.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/ledger-live-common/src/families/aptos/prepareTransaction.test.ts b/libs/ledger-live-common/src/families/aptos/prepareTransaction.test.ts index 689b967f7280..3d17e1c115a5 100644 --- a/libs/ledger-live-common/src/families/aptos/prepareTransaction.test.ts +++ b/libs/ledger-live-common/src/families/aptos/prepareTransaction.test.ts @@ -102,7 +102,6 @@ describe("Aptos prepareTransaction", () => { expect(result.firstEmulation).toBe(false); }); - //-------------------------------------------------------------------------------- it("should return the transaction with updated fees and estimate if recipient is set and amount is not zero", async () => { transaction.recipient = "test-recipient"; transaction.amount = new BigNumber(100);