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 00000000000..e23cc961a16 --- /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 c2a219eda8f..1c5a43de034 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, 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 f42e2a0b552..5c9f54243a1 100644 --- a/libs/ledger-live-common/src/families/aptos/js-getTransactionStatus.ts +++ b/libs/ledger-live-common/src/families/aptos/js-getTransactionStatus.ts @@ -12,12 +12,12 @@ 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, TransactionExpiredError, } from "./errors"; +import { AccountAddress } from "@aptos-labs/ts-sdk"; const getTransactionStatus = async (a: Account, t: Transaction): Promise => { const errors: Record = {}; @@ -44,7 +44,7 @@ const getTransactionStatus = async (a: Account, t: Transaction): Promise { + 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); + }); + }); +}); 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 67eb93f3302..8999c72e27a 100644 --- a/libs/ledger-live-common/src/families/aptos/logic.test.ts +++ b/libs/ledger-live-common/src/families/aptos/logic.test.ts @@ -8,9 +8,133 @@ import { getAptosAmounts, getFunctionAddress, isChangeOfAptos, + isTestnet, processRecipients, + getMaxSendBalance, + normalizeTransactionOptions, + getBlankOperation, } from "./logic"; -import type { AptosTransaction } from "./types"; +import type { AptosTransaction, Transaction } from "./types"; + +jest.mock("@ledgerhq/cryptoassets", () => ({ + 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 when amount is greater than total gas", () => { + const amount = new BigNumber(1000000); + 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); + }); + + 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", () => { + 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", () => { @@ -446,4 +570,5 @@ describe("Aptos sync logic ", () => { expect(result).toEqual(new BigNumber(90).negated()); // 100 - 10 }); }); + }); diff --git a/libs/ledger-live-common/src/families/aptos/logic.ts b/libs/ledger-live-common/src/families/aptos/logic.ts index 39f9ba7ceb8..9705a0bdb9f 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> => ({