diff --git a/.changeset/thick-boats-fix.md b/.changeset/thick-boats-fix.md new file mode 100644 index 00000000000..fc202495a06 --- /dev/null +++ b/.changeset/thick-boats-fix.md @@ -0,0 +1,10 @@ +--- +"@ledgerhq/coin-stellar": minor +"@ledgerhq/live-common": minor +"@ledgerhq/coin-framework": minor +"@ledgerhq/coin-polkadot": patch +"@ledgerhq/coin-tezos": patch +"@ledgerhq/coin-xrp": patch +--- + +Prepare CoinModule Stellar for Alpaca diff --git a/libs/coin-framework/src/api/types.ts b/libs/coin-framework/src/api/types.ts index fd9a5159095..58be941507b 100644 --- a/libs/coin-framework/src/api/types.ts +++ b/libs/coin-framework/src/api/types.ts @@ -17,17 +17,18 @@ export type Operation = { transactionSequenceNumber: number; }; +export type Transaction = { + mode: string; + recipient: string; + amount: bigint; + fee: bigint; + supplement?: unknown; +}; + export type Api = { broadcast: (tx: string) => Promise; combine: (tx: string, signature: string, pubkey?: string) => string; - craftTransaction: ( - address: string, - transaction: { - recipient: string; - amount: bigint; - fee: bigint; - }, - ) => Promise; + craftTransaction: (address: string, transaction: Transaction, pubkey?: string) => Promise; estimateFees: (addr: string, amount: bigint) => Promise; getBalance: (address: string) => Promise; lastBlock: () => Promise; diff --git a/libs/coin-modules/coin-polkadot/src/api/index.integ.test.ts b/libs/coin-modules/coin-polkadot/src/api/index.integ.test.ts index 0cb00ca9967..3f138de7707 100644 --- a/libs/coin-modules/coin-polkadot/src/api/index.integ.test.ts +++ b/libs/coin-modules/coin-polkadot/src/api/index.integ.test.ts @@ -81,6 +81,7 @@ describe("Polkadot Api", () => { it("returns a raw transaction", async () => { // When const result = await module.craftTransaction(address, { + mode: "send", recipient: "16YreVmGhM8mNMqnsvK7rn7b1e4SKYsTfFUn4UfCZ65BgDjh", amount: BigInt(10), fee: BigInt(1), diff --git a/libs/coin-modules/coin-polkadot/src/api/index.ts b/libs/coin-modules/coin-polkadot/src/api/index.ts index 22fed372c43..5a9ea2a8707 100644 --- a/libs/coin-modules/coin-polkadot/src/api/index.ts +++ b/libs/coin-modules/coin-polkadot/src/api/index.ts @@ -30,8 +30,11 @@ export function createApi(config: PolkadotConfig): Api { async function craft( address: string, transaction: { + mode: string; recipient: string; amount: bigint; + fee: bigint; + supplement?: unknown; }, ): Promise { const extrinsicArg = defaultExtrinsicArg(transaction.amount, transaction.recipient); diff --git a/libs/coin-modules/coin-stellar/.unimportedrc.json b/libs/coin-modules/coin-stellar/.unimportedrc.json index f9162d00a3a..dad5b54f16c 100644 --- a/libs/coin-modules/coin-stellar/.unimportedrc.json +++ b/libs/coin-modules/coin-stellar/.unimportedrc.json @@ -1,11 +1,11 @@ { "entry": [ - "src/deviceTransactionConfig.ts", - "src/errors.ts", - "src/hw-getAddress.ts", - "src/serialization.ts", - "src/specs.ts", - "src/transaction.ts" + "src/api/index.ts", + "src/bridge/index.ts", + "src/bridge/deviceTransactionConfig.ts", + "src/signer/index.ts", + "src/test/index.ts", + "src/index.ts" ], "ignorePatterns": [ "**/node_modules/**", @@ -13,22 +13,6 @@ "**/*.mock.ts", "**/*.test.{js,jsx,ts,tsx}" ], - "ignoreUnresolved": [], - "ignoreUnimported": [ - "src/bridge/index.ts", - "src/broadcast.ts", - "src/buildOptimisticOperation.ts", - "src/buildTransaction.ts", - "src/cli.ts", - "src/config.ts", - "src/createTransaction.ts", - "src/estimateMaxSpendable.ts", - "src/getTransactionStatus.ts", - "src/prepareTransaction.ts", - "src/signOperation.ts", - "src/synchronization.ts", - "src/tokens.ts" - ], "ignoreUnused": [ "rxjs" ] diff --git a/libs/coin-modules/coin-stellar/package.json b/libs/coin-modules/coin-stellar/package.json index 44d8dfb1085..e6f6fbc09d3 100644 --- a/libs/coin-modules/coin-stellar/package.json +++ b/libs/coin-modules/coin-stellar/package.json @@ -31,29 +31,6 @@ "typecheck": "tsc --noEmit", "unimported": "unimported" }, - "dependencies": { - "@ledgerhq/coin-framework": "workspace:^", - "@ledgerhq/cryptoassets": "workspace:^", - "@ledgerhq/devices": "workspace:^", - "@ledgerhq/errors": "workspace:^", - "@ledgerhq/live-env": "workspace:^", - "@ledgerhq/live-network": "workspace:^", - "@ledgerhq/logs": "workspace:^", - "@ledgerhq/types-cryptoassets": "workspace:^", - "@ledgerhq/types-live": "workspace:^", - "@stellar/stellar-sdk": "^11.3.0", - "bignumber.js": "^9.1.2", - "expect": "^27.4.6", - "invariant": "^2.2.2", - "rxjs": "^7.8.1" - }, - "devDependencies": { - "@faker-js/faker": "^8.4.1", - "@types/invariant": "^2.2.2", - "@types/jest": "^29.5.10", - "jest": "^29.7.0", - "ts-jest": "^29.1.1" - }, "publishConfig": { "access": "public" }, @@ -65,18 +42,100 @@ "lib-es/*": [ "lib-es/*" ], + "api": [ + "lib/api/index" + ], + "deviceTransactionConfig": [ + "lib/bridge/deviceTransactionConfig" + ], + "logic": [ + "lib/logic/index" + ], + "specs": [ + "lib/test/bot-specs" + ], + "transaction": [ + "lib/bridge/transaction" + ], + "types": [ + "lib/types/index" + ], "*": [ - "lib/*" + "lib/*", + "lib/bridge/*", + "lib/logic/*", + "lib/signer/*", + "lib/test/*", + "lib/types/*" ] } }, "exports": { "./lib/*": "./lib/*.js", "./lib-es/*": "./lib-es/*.js", + "./api": { + "require": "./lib/api/index.js", + "default": "./lib-es/api/index.js" + }, + "./deviceTransactionConfig": { + "require": "./lib/bridge/deviceTransactionConfig.js", + "default": "./lib-es/bridge/deviceTransactionConfig.js" + }, + "./errors": { + "require": "./lib/types/errors.js", + "default": "./lib-es/types/errors.js" + }, + "./logic": { + "require": "./lib/logic/index.js", + "default": "./lib-es/logic/index.js" + }, + "./signer": { + "require": "./lib/signer/index.js", + "default": "./lib-es/signer/index.js" + }, + "./specs": { + "require": "./lib/test/bot-specs.js", + "default": "./lib-es/test/bot-specs.js" + }, + "./transaction": { + "require": "./lib/bridge/transaction.js", + "default": "./lib-es/bridge/transaction.js" + }, + "./types": { + "require": "./lib/types/index.js", + "default": "./lib-es/types/index.js" + }, "./*": { "require": "./lib/*.js", "default": "./lib-es/*.js" }, + ".": { + "require": "./lib/index.js", + "default": "./lib-es/index.js" + }, "./package.json": "./package.json" + }, + "dependencies": { + "@ledgerhq/coin-framework": "workspace:^", + "@ledgerhq/cryptoassets": "workspace:^", + "@ledgerhq/devices": "workspace:^", + "@ledgerhq/errors": "workspace:^", + "@ledgerhq/live-network": "workspace:^", + "@ledgerhq/logs": "workspace:^", + "@ledgerhq/types-cryptoassets": "workspace:^", + "@ledgerhq/types-live": "workspace:^", + "@stellar/stellar-sdk": "^11.3.0", + "bignumber.js": "^9.1.2", + "expect": "^27.4.6", + "invariant": "^2.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@faker-js/faker": "^8.4.1", + "@types/invariant": "^2.2.2", + "@types/jest": "^29.5.10", + "@types/node": "^20.8.10", + "jest": "^29.7.0", + "ts-jest": "^29.1.1" } } diff --git a/libs/coin-modules/coin-stellar/src/api/index.integ.test.ts b/libs/coin-modules/coin-stellar/src/api/index.integ.test.ts new file mode 100644 index 00000000000..865efffba88 --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/api/index.integ.test.ts @@ -0,0 +1,86 @@ +import type { Api } from "@ledgerhq/coin-framework/api/index"; +import { createApi } from "."; + +describe("Stellar Api", () => { + let module: Api; + const address = "GD6QELUZPSKPRWVXOQ3F6GBF4OBRMCHO5PHREXH4ZRTPJAG7V5MD7JGX"; + + beforeAll(() => { + module = createApi({ + explorer: { + url: "https://horizon-testnet.stellar.org/", + }, + }); + }); + + describe("estimateFees", () => { + it("returns a default value", async () => { + // Given + const amount = BigInt(100_000); + + // When + const result = await module.estimateFees(address, amount); + + // Then + expect(result).toEqual(BigInt(100)); + }); + }); + + describe("listOperations", () => { + it("returns a list regarding address parameter", async () => { + // When + const result = await module.listOperations(address, 0); + + // Then + expect(result.length).toBeGreaterThanOrEqual(1); + result.forEach(operation => { + expect(operation.address).toEqual(address); + const isSenderOrReceipt = + operation.senders.includes(address) || operation.recipients.includes(address); + expect(isSenderOrReceipt).toBeTruthy(); + }); + }); + }); + + describe("lastBlock", () => { + it("returns last block info", async () => { + // When + const result = await module.lastBlock(); + + // Then + expect(result.hash).toBeDefined(); + expect(result.height).toBeDefined(); + expect(result.time).toBeInstanceOf(Date); + }); + }); + + describe("getBalance", () => { + it("returns a list regarding address parameter", async () => { + // When + const result = await module.getBalance(address); + + // Then + expect(result).toBeGreaterThan(0); + }); + }); + + describe("craftTransaction", () => { + it("returns a raw transaction", async () => { + // When + const result = await module.craftTransaction(address, { + mode: "send", + recipient: "GD6QELUZPSKPRWVXOQ3F6GBF4OBRMCHO5PHREXH4ZRTPJAG7V5MD7JGX", + amount: BigInt(1_000_000), + fee: BigInt(100), + }); + + // Then + expect(result.slice(0, 67)).toEqual( + "AAAAAgAAAAD9Ai6ZfJT42rd0Nl8YJeODFgju688SXPzMZvSA369YPwAAAGQAAHloAAA", + ); + expect(result.slice(70)).toEqual( + "AAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAD9Ai6ZfJT42rd0Nl8YJeODFgju688SXPzMZvSA369YPwAAAAAAAAAAAA9CQAAAAAAAAAAA", + ); + }); + }); +}); diff --git a/libs/coin-modules/coin-stellar/src/api/index.ts b/libs/coin-modules/coin-stellar/src/api/index.ts new file mode 100644 index 00000000000..df3d896a845 --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/api/index.ts @@ -0,0 +1,72 @@ +import type { Api } from "@ledgerhq/coin-framework/api/index"; +import coinConfig, { type StellarConfig } from "../config"; +import { + broadcast, + combine, + craftTransaction, + estimateFees, + getBalance, + listOperations, + lastBlock, +} from "../logic"; + +export function createApi(config: StellarConfig): Api { + coinConfig.setCoinConfig(() => ({ ...config, status: { type: "active" } })); + + return { + broadcast, + combine: compose, + craftTransaction: craft, + estimateFees, + getBalance, + lastBlock, + listOperations, + }; +} + +type Supplement = { + assetCode?: string | undefined; + assetIssuer?: string | undefined; + memoType?: string | null | undefined; + memoValue?: string | null | undefined; +}; +function isSupplement(supplement: unknown): supplement is Supplement { + return typeof supplement === "object"; +} +async function craft( + address: string, + transaction: { + mode: string; + recipient: string; + amount: bigint; + fee: bigint; + supplement?: unknown; + }, +): Promise { + const supplement = isSupplement(transaction.supplement) + ? { + assetCode: transaction.supplement?.assetCode, + assetIssuer: transaction.supplement?.assetIssuer, + memoType: transaction.supplement?.memoType, + memoValue: transaction.supplement?.memoValue, + } + : {}; + const tx = await craftTransaction( + { address }, + { + ...transaction, + assetCode: supplement?.assetCode, + assetIssuer: supplement?.assetIssuer, + memoType: supplement?.memoType, + memoValue: supplement?.memoValue, + }, + ); + return tx.xdr; +} + +function compose(tx: string, signature: string, pubkey?: string): string { + if (!pubkey) { + throw new Error("Missing pubkey"); + } + return combine(tx, signature, pubkey); +} diff --git a/libs/coin-modules/coin-stellar/src/broadcast.test.ts b/libs/coin-modules/coin-stellar/src/bridge/broadcast.test.ts similarity index 91% rename from libs/coin-modules/coin-stellar/src/broadcast.test.ts rename to libs/coin-modules/coin-stellar/src/bridge/broadcast.test.ts index c50fc435ebe..c5958e7d08e 100644 --- a/libs/coin-modules/coin-stellar/src/broadcast.test.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/broadcast.test.ts @@ -1,8 +1,8 @@ import { broadcast } from "./broadcast"; -import { createFixtureAccount, createFixtureOperation } from "./types/bridge.fixture"; +import { createFixtureAccount, createFixtureOperation } from "../types/bridge.fixture"; const mockBroadcast = jest.fn(); -jest.mock("./network", () => ({ +jest.mock("../network", () => ({ broadcastTransaction: (sig: unknown) => mockBroadcast(sig), })); diff --git a/libs/coin-modules/coin-stellar/src/broadcast.ts b/libs/coin-modules/coin-stellar/src/bridge/broadcast.ts similarity index 85% rename from libs/coin-modules/coin-stellar/src/broadcast.ts rename to libs/coin-modules/coin-stellar/src/bridge/broadcast.ts index 0dd1c58b23b..fc546df13cd 100644 --- a/libs/coin-modules/coin-stellar/src/broadcast.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/broadcast.ts @@ -1,7 +1,7 @@ import { patchOperationWithHash } from "@ledgerhq/coin-framework/operation"; import type { AccountBridge, Operation, SignedOperation } from "@ledgerhq/types-live"; -import { broadcastTransaction as apiBroadcast } from "./network"; -import { Transaction } from "./types"; +import { broadcast as apiBroadcast } from "../logic"; +import { Transaction } from "../types"; /** * Broadcast a signed transaction diff --git a/libs/coin-modules/coin-stellar/src/buildOptimisticOperation.ts b/libs/coin-modules/coin-stellar/src/bridge/buildOptimisticOperation.ts similarity index 94% rename from libs/coin-modules/coin-stellar/src/buildOptimisticOperation.ts rename to libs/coin-modules/coin-stellar/src/bridge/buildOptimisticOperation.ts index bc298941a64..18bb126b443 100644 --- a/libs/coin-modules/coin-stellar/src/buildOptimisticOperation.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/buildOptimisticOperation.ts @@ -1,9 +1,9 @@ import BigNumber from "bignumber.js"; import { Account } from "@ledgerhq/types-live"; import { encodeOperationId } from "@ledgerhq/coin-framework/operation"; -import { StellarOperation, Transaction } from "./types"; +import { StellarOperation, Transaction } from "../types"; import { getAmountValue } from "./logic"; -import { fetchSequence } from "./network"; +import { fetchSequence } from "../network"; export async function buildOptimisticOperation( account: Account, diff --git a/libs/coin-modules/coin-stellar/src/buildTransaction.integ.test.ts b/libs/coin-modules/coin-stellar/src/bridge/buildTransaction.integ.test.ts similarity index 96% rename from libs/coin-modules/coin-stellar/src/buildTransaction.integ.test.ts rename to libs/coin-modules/coin-stellar/src/bridge/buildTransaction.integ.test.ts index d3e6d247fc3..ab0205d7297 100644 --- a/libs/coin-modules/coin-stellar/src/buildTransaction.integ.test.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/buildTransaction.integ.test.ts @@ -1,14 +1,14 @@ import BigNumber from "bignumber.js"; import { buildTransaction } from "./buildTransaction"; -import { createFixtureAccount, createFixtureTransaction } from "./types/bridge.fixture"; -import { setCoinConfig, type StellarCoinConfig } from "./config"; -import { NetworkInfo } from "./types"; +import { createFixtureAccount, createFixtureTransaction } from "../types/bridge.fixture"; +import coinConfig, { type StellarCoinConfig } from "../config"; +import { NetworkInfo } from "../types"; describe("buildTransaction", () => { const sender = "GAT4LBXYJGJJJRSNK74NPFLO55CDDXSYVMQODSEAAH3M6EY4S7LPH5GV"; beforeAll(() => { - setCoinConfig( + coinConfig.setCoinConfig( (): StellarCoinConfig => ({ status: { type: "active" }, explorer: { @@ -19,22 +19,24 @@ describe("buildTransaction", () => { ); }); - it("throws an error when no fees are setted in the transaction", async () => { + it("throws an error if transaction has no NetworkInfo", async () => { // Given - const account = createFixtureAccount(); - const transaction = createFixtureTransaction(); + const account = createFixtureAccount({ freshAddress: sender }); + const transaction = createFixtureTransaction({ fees: BigNumber(1) }); // When - await expect(buildTransaction(account, transaction)).rejects.toThrow("FeeNotLoaded"); + await expect(buildTransaction(account, transaction)).rejects.toThrow("stellar family"); }); - it("throws an error if transaction has no NetworkInfo", async () => { + it("throws an error when no fees are setted in the transaction", async () => { // Given - const account = createFixtureAccount({ freshAddress: sender }); - const transaction = createFixtureTransaction({ fees: BigNumber(1) }); + const account = createFixtureAccount(); + const transaction = createFixtureTransaction({ + networkInfo: { family: "stellar" } as NetworkInfo, + }); // When - await expect(buildTransaction(account, transaction)).rejects.toThrow("stellar family"); + await expect(buildTransaction(account, transaction)).rejects.toThrow("FeeNotLoaded"); }); it.skip("crash if transaction amount is 0", async () => { diff --git a/libs/coin-modules/coin-stellar/src/bridge/buildTransaction.ts b/libs/coin-modules/coin-stellar/src/bridge/buildTransaction.ts new file mode 100644 index 00000000000..b825430e97c --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/bridge/buildTransaction.ts @@ -0,0 +1,45 @@ +import { AmountRequired, FeeNotLoaded } from "@ledgerhq/errors"; +import type { Account } from "@ledgerhq/types-live"; +import invariant from "invariant"; +import { craftTransaction } from "../logic"; +import type { Transaction } from "../types"; +import { getAmountValue } from "./logic"; + +/** + * @param {Account} account + * @param {Transaction} transaction + */ +export async function buildTransaction(account: Account, transaction: Transaction) { + const { recipient, networkInfo, fees, memoType, memoValue, mode, assetCode, assetIssuer } = + transaction; + + invariant(networkInfo && networkInfo.family === "stellar", "stellar family"); + + if (!fees) { + throw new FeeNotLoaded(); + } + + const amount = getAmountValue(account, transaction, fees); + + if (!amount) { + throw new AmountRequired(); + } + + const { transaction: built } = await craftTransaction( + { address: account.freshAddress }, + { + mode, + recipient, + amount: BigInt(amount.toString()), + fee: BigInt(fees.toString()), + assetCode, + assetIssuer, + memoType, + memoValue, + }, + ); + + return built; +} + +export default buildTransaction; diff --git a/libs/coin-modules/coin-stellar/src/createTransaction.ts b/libs/coin-modules/coin-stellar/src/bridge/createTransaction.ts similarity index 92% rename from libs/coin-modules/coin-stellar/src/createTransaction.ts rename to libs/coin-modules/coin-stellar/src/bridge/createTransaction.ts index c07bc1b9eff..e1d84801636 100644 --- a/libs/coin-modules/coin-stellar/src/createTransaction.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/createTransaction.ts @@ -1,6 +1,6 @@ import { BigNumber } from "bignumber.js"; import { AccountBridge } from "@ledgerhq/types-live"; -import type { Transaction } from "./types"; +import type { Transaction } from "../types"; /** * Create an empty transaction diff --git a/libs/coin-modules/coin-stellar/src/deviceTransactionConfig.ts b/libs/coin-modules/coin-stellar/src/bridge/deviceTransactionConfig.ts similarity index 96% rename from libs/coin-modules/coin-stellar/src/deviceTransactionConfig.ts rename to libs/coin-modules/coin-stellar/src/bridge/deviceTransactionConfig.ts index c361c291ea4..3a74053a4f6 100644 --- a/libs/coin-modules/coin-stellar/src/deviceTransactionConfig.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/deviceTransactionConfig.ts @@ -1,6 +1,6 @@ import type { AccountLike, Account } from "@ledgerhq/types-live"; import type { CommonDeviceTransactionField as DeviceTransactionField } from "@ledgerhq/coin-framework/transaction/common"; -import type { Transaction, TransactionStatus } from "./types"; +import type { Transaction, TransactionStatus } from "../types"; export type ExtraDeviceTransactionField = | { diff --git a/libs/coin-modules/coin-stellar/src/estimateMaxSpendable.ts b/libs/coin-modules/coin-stellar/src/bridge/estimateMaxSpendable.ts similarity index 96% rename from libs/coin-modules/coin-stellar/src/estimateMaxSpendable.ts rename to libs/coin-modules/coin-stellar/src/bridge/estimateMaxSpendable.ts index 8fc3b31913a..0ede5e5dd26 100644 --- a/libs/coin-modules/coin-stellar/src/estimateMaxSpendable.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/estimateMaxSpendable.ts @@ -4,7 +4,7 @@ import { getMainAccount } from "@ledgerhq/coin-framework/account"; import { getTransactionStatus } from "./getTransactionStatus"; import { prepareTransaction } from "./prepareTransaction"; import { createTransaction } from "./createTransaction"; -import type { Transaction } from "./types"; +import type { Transaction } from "../types"; const notCreatedStellarMockAddress = "GAW46JE3SHIAYLNNNQCAZFQ437WB5ZH7LDRDWR5LVDWHCTHCKYB6RCCH"; diff --git a/libs/coin-modules/coin-stellar/src/getTransactionStatus.ts b/libs/coin-modules/coin-stellar/src/bridge/getTransactionStatus.ts similarity index 96% rename from libs/coin-modules/coin-stellar/src/getTransactionStatus.ts rename to libs/coin-modules/coin-stellar/src/bridge/getTransactionStatus.ts index 9736b57d203..0fbbbba3b8a 100644 --- a/libs/coin-modules/coin-stellar/src/getTransactionStatus.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/getTransactionStatus.ts @@ -13,9 +13,8 @@ import { BigNumber } from "bignumber.js"; import type { AccountBridge } from "@ledgerhq/types-live"; import { findSubAccountById } from "@ledgerhq/coin-framework/account/index"; import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index"; -import { isAddressValid, isAccountMultiSign, isMemoValid, getRecipientAccount } from "./logic"; -import { BASE_RESERVE, MIN_BALANCE } from "./network"; -import type { Transaction } from "./types"; +import { isAddressValid, isAccountMultiSign, isMemoValid } from "./logic"; +import { BASE_RESERVE, MIN_BALANCE, getRecipientAccount } from "../network"; import { StellarWrongMemoFormat, StellarAssetRequired, @@ -27,7 +26,8 @@ import { StellarNotEnoughNativeBalanceToAddTrustline, StellarMuxedAccountNotExist, StellarSourceHasMultiSign, -} from "./errors"; + type Transaction, +} from "../types"; export const getTransactionStatus: AccountBridge["getTransactionStatus"] = async ( account, @@ -99,7 +99,6 @@ export const getTransactionStatus: AccountBridge["getTransactionSta } const recipientAccount = await getRecipientAccount({ - account: account, recipient: transaction.recipient, }); diff --git a/libs/coin-modules/coin-stellar/src/bridge/index.ts b/libs/coin-modules/coin-stellar/src/bridge/index.ts index 2e170f3d4b4..ea9714cfeb9 100644 --- a/libs/coin-modules/coin-stellar/src/bridge/index.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/index.ts @@ -5,24 +5,24 @@ import { makeScanAccounts, makeSync, } from "@ledgerhq/coin-framework/bridge/jsHelpers"; -import { estimateMaxSpendable } from "../estimateMaxSpendable"; -import { getTransactionStatus } from "../getTransactionStatus"; +import { estimateMaxSpendable } from "./estimateMaxSpendable"; +import { getTransactionStatus } from "./getTransactionStatus"; import type { StellarSigner, Transaction, TransactionStatus } from "../types"; -import { prepareTransaction } from "../prepareTransaction"; -import { createTransaction } from "../createTransaction"; -import { buildSignOperation } from "../signOperation"; -import { broadcast } from "../broadcast"; +import { prepareTransaction } from "./prepareTransaction"; +import { createTransaction } from "./createTransaction"; +import { buildSignOperation } from "./signOperation"; +import { broadcast } from "./broadcast"; import getAddressWrapper from "@ledgerhq/coin-framework/bridge/getAddressWrapper"; -import resolver from "../hw-getAddress"; +import signerGetAddress from "../signer"; import { SignerContext } from "@ledgerhq/coin-framework/signer"; -import { getAccountShape } from "../synchronization"; +import { getAccountShape } from "./synchronization"; import { CoinConfig } from "@ledgerhq/coin-framework/config"; -import { StellarCoinConfig, setCoinConfig } from "../config"; +import stellarCoinConfig, { type StellarCoinConfig } from "../config"; const PRELOAD_MAX_AGE = 30 * 60 * 1000; // 30 minutes function buildCurrencyBridge(signerContext: SignerContext): CurrencyBridge { - const getAddress = resolver(signerContext); + const getAddress = signerGetAddress(signerContext); const scanAccounts = makeScanAccounts({ getAccountShape, @@ -44,7 +44,7 @@ function buildCurrencyBridge(signerContext: SignerContext): Curre } function buildAccountBridge(signerContext: SignerContext) { - const getAddress = resolver(signerContext); + const getAddress = signerGetAddress(signerContext); const receive = makeAccountBridgeReceive(getAddressWrapper(getAddress)); const signOperation = buildSignOperation(signerContext); const sync = makeSync({ getAccountShape }); @@ -68,7 +68,7 @@ export function createBridges( signerContext: SignerContext, coinConfig: CoinConfig, ) { - setCoinConfig(coinConfig); + stellarCoinConfig.setCoinConfig(coinConfig); return { currencyBridge: buildCurrencyBridge(signerContext), diff --git a/libs/coin-modules/coin-stellar/src/logic.ts b/libs/coin-modules/coin-stellar/src/bridge/logic.ts similarity index 81% rename from libs/coin-modules/coin-stellar/src/logic.ts rename to libs/coin-modules/coin-stellar/src/bridge/logic.ts index 84470c8c06b..77ddccc0aef 100644 --- a/libs/coin-modules/coin-stellar/src/logic.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/logic.ts @@ -1,32 +1,18 @@ import { BigNumber } from "bignumber.js"; -import type { CacheRes } from "@ledgerhq/live-network/cache"; -import { makeLRUCache } from "@ledgerhq/live-network/cache"; import type { Account, OperationType, TokenAccount } from "@ledgerhq/types-live"; -import { - Horizon, - StrKey, - MuxedAccount, - // @ts-expect-error stellar-sdk ts definition missing? - AccountRecord, -} from "@stellar/stellar-sdk"; +import { Horizon, StrKey } from "@stellar/stellar-sdk"; import { findSubAccountById } from "@ledgerhq/coin-framework/account/helpers"; import { encodeOperationId } from "@ledgerhq/coin-framework/operation"; import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies"; import { parseCurrencyUnit } from "@ledgerhq/coin-framework/currencies/parseCurrencyUnit"; -import { - BASE_RESERVE, - BASE_RESERVE_MIN_COUNT, - fetchBaseFee, - fetchSigners, - loadAccount, -} from "./network"; +import { BASE_RESERVE, BASE_RESERVE_MIN_COUNT, fetchBaseFee, fetchSigners } from "../network"; import type { BalanceAsset, RawOperation, StellarOperation, Transaction, TransactionRaw, -} from "./types"; +} from "../types"; export const STELLAR_BURN_ADDRESS = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"; @@ -62,20 +48,6 @@ export function getAmountValue( : transaction.amount; } -export function getBalanceId(balance: BalanceAsset): string | null { - switch (balance.asset_type) { - case "native": - return "native"; - case "liquidity_pool_shares": - return balance.liquidity_pool_id || null; - case "credit_alphanum4": - case "credit_alphanum12": - return `${balance.asset_code}:${balance.asset_issuer}`; - default: - return null; - } -} - export function getReservedBalance(account: Horizon.ServerApi.AccountRecord): BigNumber { const numOfSponsoringEntries = Number(account.num_sponsoring); const numOfSponsoredEntries = Number(account.num_sponsored); @@ -288,58 +260,6 @@ export function isAddressValid(address: string): boolean { } } -export const getRecipientAccount: CacheRes< - Array<{ - account: Account; - recipient: string; - }>, - { - id: string | null; - isMuxedAccount: boolean; - assetIds: string[]; - } | null -> = makeLRUCache( - async ({ recipient }) => await recipientAccount(recipient), - extract => extract.recipient, - { - max: 300, - ttl: 5 * 60, - }, // 5 minutes -); - -export async function recipientAccount(address?: string): Promise<{ - id: string | null; - isMuxedAccount: boolean; - assetIds: string[]; -} | null> { - if (!address) { - return null; - } - - let accountAddress = address; - - const isMuxedAccount = StrKey.isValidMed25519PublicKey(address); - - if (isMuxedAccount) { - const muxedAccount = MuxedAccount.fromAddress(address, "0"); - accountAddress = muxedAccount.baseAccount().accountId(); - } - - const account: AccountRecord = await loadAccount(accountAddress); - - if (!account) { - return null; - } - - return { - id: account.id, - isMuxedAccount, - assetIds: account.balances.reduce((allAssets: any[], balance: any) => { - return [...allAssets, getBalanceId(balance)]; - }, []), - }; -} - export function rawOperationsToOperations( operations: RawOperation[], addr: string, diff --git a/libs/coin-modules/coin-stellar/src/prepareTransaction.ts b/libs/coin-modules/coin-stellar/src/bridge/prepareTransaction.ts similarity index 91% rename from libs/coin-modules/coin-stellar/src/prepareTransaction.ts rename to libs/coin-modules/coin-stellar/src/bridge/prepareTransaction.ts index 2f6d31de810..9103c62f03a 100644 --- a/libs/coin-modules/coin-stellar/src/prepareTransaction.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/prepareTransaction.ts @@ -1,9 +1,9 @@ import invariant from "invariant"; import type { AccountBridge } from "@ledgerhq/types-live"; import { defaultUpdateTransaction } from "@ledgerhq/coin-framework/bridge/jsHelpers"; -import { fetchAccountNetworkInfo } from "./network"; +import { fetchAccountNetworkInfo } from "../network"; import { getAssetCodeIssuer } from "./logic"; -import type { Transaction } from "./types"; +import type { Transaction } from "../types"; export const prepareTransaction: AccountBridge["prepareTransaction"] = async ( account, diff --git a/libs/coin-modules/coin-stellar/src/signOperation.test.ts b/libs/coin-modules/coin-stellar/src/bridge/signOperation.test.ts similarity index 91% rename from libs/coin-modules/coin-stellar/src/signOperation.test.ts rename to libs/coin-modules/coin-stellar/src/bridge/signOperation.test.ts index 659b06eda54..f0058a138cf 100644 --- a/libs/coin-modules/coin-stellar/src/signOperation.test.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/signOperation.test.ts @@ -1,13 +1,13 @@ -import BigNumber from "bignumber.js"; import { SignOperationEvent } from "@ledgerhq/types-live"; -import { buildSignOperation } from "./signOperation"; -import { setCoinConfig, type StellarCoinConfig } from "./config"; -import { StellarSigner } from "./types/signer"; -import { createFixtureAccount, createFixtureTransaction } from "./types/bridge.fixture"; -import { NetworkInfo } from "./types"; import { Keypair } from "@stellar/stellar-sdk"; -import buildTransaction from "./buildTransaction"; +import BigNumber from "bignumber.js"; import { subtle } from "crypto"; +import coinConfig, { type StellarCoinConfig } from "../config"; +import { NetworkInfo } from "../types"; +import { createFixtureAccount, createFixtureTransaction } from "../types/bridge.fixture"; +import { StellarSigner } from "../types/signer"; +import buildTransaction from "./buildTransaction"; +import { buildSignOperation } from "./signOperation"; const stellarKp = Keypair.random(); const mockLoadAccount = jest.fn().mockResolvedValue( @@ -28,8 +28,8 @@ const mockLoadAccount = jest.fn().mockResolvedValue( incrementSequenceNumber: () => "1", }, ); -jest.mock("./network", () => ({ - ...jest.requireActual("./network"), +jest.mock("../network", () => ({ + ...jest.requireActual("../network"), loadAccount: () => mockLoadAccount(), fetchSequence: jest.fn(), })); @@ -46,9 +46,12 @@ describe.skip("signOperation", () => { const deviceId = "dummyDeviceId"; beforeAll(() => { - setCoinConfig( + coinConfig.setCoinConfig( (): StellarCoinConfig => ({ status: { type: "active" }, + explorer: { + url: "https://localhost", + }, }), ); }); diff --git a/libs/coin-modules/coin-stellar/src/signOperation.ts b/libs/coin-modules/coin-stellar/src/bridge/signOperation.ts similarity index 94% rename from libs/coin-modules/coin-stellar/src/signOperation.ts rename to libs/coin-modules/coin-stellar/src/bridge/signOperation.ts index 519b5ffe6b1..fece6ee2b96 100644 --- a/libs/coin-modules/coin-stellar/src/signOperation.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/signOperation.ts @@ -2,10 +2,10 @@ import { Observable } from "rxjs"; import { FeeNotLoaded } from "@ledgerhq/errors"; import type { Account, AccountBridge } from "@ledgerhq/types-live"; import { SignerContext } from "@ledgerhq/coin-framework/signer"; -import type { Transaction } from "./types"; +import type { Transaction } from "../types"; import { buildTransaction } from "./buildTransaction"; import { buildOptimisticOperation } from "./buildOptimisticOperation"; -import { StellarSigner } from "./types/signer"; +import { StellarSigner } from "../types/signer"; export function buildSignOperation( signerContext: SignerContext, diff --git a/libs/coin-modules/coin-stellar/src/synchronization.integ.test.ts b/libs/coin-modules/coin-stellar/src/bridge/synchronization.integ.test.ts similarity index 87% rename from libs/coin-modules/coin-stellar/src/synchronization.integ.test.ts rename to libs/coin-modules/coin-stellar/src/bridge/synchronization.integ.test.ts index 6af36b0f5fb..6658fdf4114 100644 --- a/libs/coin-modules/coin-stellar/src/synchronization.integ.test.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/synchronization.integ.test.ts @@ -1,9 +1,9 @@ import { firstValueFrom, reduce } from "rxjs"; import { Account, AccountBridge, SyncConfig, TransactionCommon } from "@ledgerhq/types-live"; -import type { StellarCoinConfig } from "./config"; -import { Transaction, StellarAccount } from "./types"; -import { createBridges } from "./bridge/index"; -import { createFixtureAccount } from "./types/bridge.fixture"; +import type { StellarCoinConfig } from "../config"; +import { Transaction, StellarAccount } from "../types"; +import { createBridges } from "../bridge/index"; +import { createFixtureAccount } from "../types/bridge.fixture"; const defaultSyncConfig = { paginationConfig: {}, diff --git a/libs/coin-modules/coin-stellar/src/bridge/synchronization.test.ts b/libs/coin-modules/coin-stellar/src/bridge/synchronization.test.ts new file mode 100644 index 00000000000..b983c24de35 --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/bridge/synchronization.test.ts @@ -0,0 +1,3 @@ +describe("getAccountShape", () => { + it.todo("returns an AccountShapeInfo"); +}); diff --git a/libs/coin-modules/coin-stellar/src/synchronization.ts b/libs/coin-modules/coin-stellar/src/bridge/synchronization.ts similarity index 93% rename from libs/coin-modules/coin-stellar/src/synchronization.ts rename to libs/coin-modules/coin-stellar/src/bridge/synchronization.ts index 2d1554b44d1..1b931e141f9 100644 --- a/libs/coin-modules/coin-stellar/src/synchronization.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/synchronization.ts @@ -2,11 +2,10 @@ import { encodeAccountId } from "@ledgerhq/coin-framework/account/index"; import { inferSubOperations } from "@ledgerhq/coin-framework/serialization/index"; import { Account } from "@ledgerhq/types-live"; import { GetAccountShape, mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers"; -import { fetchAccount, fetchOperations } from "./network"; +import { fetchAccount, fetchOperations } from "../network"; import { buildSubAccounts } from "./tokens"; -import { StellarOperation } from "./types"; +import { StellarBurnAddressError, StellarOperation } from "../types"; import { STELLAR_BURN_ADDRESS } from "./logic"; -import { StellarBurnAddressError } from "./errors"; export const getAccountShape: GetAccountShape = async (info, syncConfig) => { const { address, currency, initialAccount, derivationMode } = info; diff --git a/libs/coin-modules/coin-stellar/src/tokens.ts b/libs/coin-modules/coin-stellar/src/bridge/tokens.ts similarity index 97% rename from libs/coin-modules/coin-stellar/src/tokens.ts rename to libs/coin-modules/coin-stellar/src/bridge/tokens.ts index 1daa366f655..3469100b505 100644 --- a/libs/coin-modules/coin-stellar/src/tokens.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/tokens.ts @@ -5,7 +5,7 @@ import type { SyncConfig, TokenAccount } from "@ledgerhq/types-live"; import { parseCurrencyUnit } from "@ledgerhq/coin-framework/currencies/parseCurrencyUnit"; import { encodeOperationId } from "@ledgerhq/coin-framework/operation"; import { findTokenById, listTokensForCryptoCurrency } from "@ledgerhq/cryptoassets"; -import type { BalanceAsset, StellarOperation } from "./types"; +import type { BalanceAsset, StellarOperation } from "../types"; export const getAssetIdFromTokenId = (tokenId: string): string => tokenId.split("/")[2]; diff --git a/libs/coin-modules/coin-stellar/src/transaction.ts b/libs/coin-modules/coin-stellar/src/bridge/transaction.ts similarity index 98% rename from libs/coin-modules/coin-stellar/src/transaction.ts rename to libs/coin-modules/coin-stellar/src/bridge/transaction.ts index bd8d41c1065..21a075cbdd1 100644 --- a/libs/coin-modules/coin-stellar/src/transaction.ts +++ b/libs/coin-modules/coin-stellar/src/bridge/transaction.ts @@ -10,7 +10,7 @@ import type { Account } from "@ledgerhq/types-live"; import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/formatCurrencyUnit"; import { getAccountCurrency } from "@ledgerhq/coin-framework/account/helpers"; import { getAssetCodeIssuer } from "./logic"; -import type { Transaction, TransactionRaw } from "./types"; +import type { Transaction, TransactionRaw } from "../types"; export function formatTransaction( { amount, recipient, fees, memoValue, useAllAmount, subAccountId }: Transaction, diff --git a/libs/coin-modules/coin-stellar/src/buildTransaction.ts b/libs/coin-modules/coin-stellar/src/buildTransaction.ts deleted file mode 100644 index 78c8261eca7..00000000000 --- a/libs/coin-modules/coin-stellar/src/buildTransaction.ts +++ /dev/null @@ -1,106 +0,0 @@ -import invariant from "invariant"; -import { Memo, Operation as StellarSdkOperation, xdr } from "@stellar/stellar-sdk"; -import { AmountRequired, FeeNotLoaded, NetworkDown } from "@ledgerhq/errors"; -import type { Account } from "@ledgerhq/types-live"; -import type { Transaction } from "./types"; -import { - buildPaymentOperation, - buildCreateAccountOperation, - buildTransactionBuilder, - buildChangeTrustOperation, - loadAccount, -} from "./network"; -import { getRecipientAccount, getAmountValue } from "./logic"; -import { StellarAssetRequired, StellarMuxedAccountNotExist } from "./errors"; - -/** - * @param {Account} account - * @param {Transaction} transaction - */ -export async function buildTransaction(account: Account, transaction: Transaction) { - const { recipient, networkInfo, fees, memoType, memoValue, mode, assetCode, assetIssuer } = - transaction; - - if (!fees) { - throw new FeeNotLoaded(); - } - - const source = await loadAccount(account.freshAddress); - - if (!source) { - throw new NetworkDown(); - } - - invariant(networkInfo && networkInfo.family === "stellar", "stellar family"); - - const transactionBuilder = buildTransactionBuilder(source, fees); - let operation: xdr.Operation | null = null; - - if (mode === "changeTrust") { - if (!assetCode || !assetIssuer) { - throw new StellarAssetRequired(""); - } - - operation = buildChangeTrustOperation(assetCode, assetIssuer); - } else { - // Payment - const amount = getAmountValue(account, transaction, fees); - - if (!amount) { - throw new AmountRequired(); - } - - const recipientAccount = await getRecipientAccount({ - account, - recipient: transaction.recipient, - }); - - if (recipientAccount?.id) { - operation = buildPaymentOperation({ - destination: recipient, - amount, - assetCode, - assetIssuer, - }); - } else { - if (recipientAccount?.isMuxedAccount) { - throw new StellarMuxedAccountNotExist(""); - } - - operation = buildCreateAccountOperation(recipient, amount); - } - } - - transactionBuilder.addOperation(operation); - - let memo: Memo | null = null; - - if (memoType && memoValue) { - switch (memoType) { - case "MEMO_TEXT": - memo = Memo.text(memoValue); - break; - - case "MEMO_ID": - memo = Memo.id(memoValue); - break; - - case "MEMO_HASH": - memo = Memo.hash(memoValue); - break; - - case "MEMO_RETURN": - memo = Memo.return(memoValue); - break; - } - } - - if (memo) { - transactionBuilder.addMemo(memo); - } - - const built = transactionBuilder.setTimeout(0).build(); - return built; -} - -export default buildTransaction; diff --git a/libs/coin-modules/coin-stellar/src/config.ts b/libs/coin-modules/coin-stellar/src/config.ts index 577996a85f6..a0acd4cbd7d 100644 --- a/libs/coin-modules/coin-stellar/src/config.ts +++ b/libs/coin-modules/coin-stellar/src/config.ts @@ -1,18 +1,16 @@ -import { CurrencyConfig, CoinConfig } from "@ledgerhq/coin-framework/config"; -import { MissingCoinConfig } from "@ledgerhq/coin-framework/errors"; +import buildCoinConfig, { type CurrencyConfig } from "@ledgerhq/coin-framework/config"; -export type StellarCoinConfig = CurrencyConfig; +export type StellarConfig = { + explorer: { + url: string; + fetchLimit?: number; + }; + useStaticFees?: boolean; + enableNetworkLogs?: boolean; +}; -let coinConfig: CoinConfig | undefined; +export type StellarCoinConfig = CurrencyConfig & StellarConfig; -export function setCoinConfig(config: CoinConfig): void { - coinConfig = config; -} +const coinConfig = buildCoinConfig(); -export function getCoinConfig(): StellarCoinConfig { - if (!coinConfig) { - throw new MissingCoinConfig(); - } - - return coinConfig(); -} +export default coinConfig; diff --git a/libs/coin-modules/coin-stellar/src/index.ts b/libs/coin-modules/coin-stellar/src/index.ts new file mode 100644 index 00000000000..b1bb866826d --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/index.ts @@ -0,0 +1 @@ +export { createBridges } from "./bridge"; diff --git a/libs/coin-modules/coin-stellar/src/logic/broadcast.ts b/libs/coin-modules/coin-stellar/src/logic/broadcast.ts new file mode 100644 index 00000000000..8d41982c71b --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/logic/broadcast.ts @@ -0,0 +1,5 @@ +import { broadcastTransaction } from "../network"; + +export async function broadcast(signature: string): Promise { + return broadcastTransaction(signature); +} diff --git a/libs/coin-modules/coin-stellar/src/logic/combine.ts b/libs/coin-modules/coin-stellar/src/logic/combine.ts new file mode 100644 index 00000000000..f909d7d16fb --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/logic/combine.ts @@ -0,0 +1,7 @@ +import { Networks, Transaction as StellarSdkTransaction } from "@stellar/stellar-sdk"; + +export function combine(transaction: string, signature: string, publicKey: string): string { + const unsignedTx = new StellarSdkTransaction(transaction, Networks.PUBLIC); + unsignedTx.addSignature(publicKey, signature); + return unsignedTx.toXDR(); +} diff --git a/libs/coin-modules/coin-stellar/src/logic/craftTransaction.ts b/libs/coin-modules/coin-stellar/src/logic/craftTransaction.ts new file mode 100644 index 00000000000..c37785967f7 --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/logic/craftTransaction.ts @@ -0,0 +1,102 @@ +import { + Memo, + Operation as StellarSdkOperation, + Transaction as StellarSdkTransaction, + xdr, +} from "@stellar/stellar-sdk"; +import { NetworkDown } from "@ledgerhq/errors"; +import { getRecipientAccount, loadAccount } from "../network"; +import { StellarAssetRequired, StellarMuxedAccountNotExist } from "../types"; +import { + buildChangeTrustOperation, + buildCreateAccountOperation, + buildPaymentOperation, + buildTransactionBuilder, +} from "./sdkWrapper"; + +export async function craftTransaction( + account: { + address: string; + }, + transaction: { + mode: string; + recipient: string; + amount: bigint; + fee: bigint; + assetCode?: string | undefined; + assetIssuer?: string | undefined; + memoType?: string | null | undefined; + memoValue?: string | null | undefined; + }, +): Promise<{ transaction: StellarSdkTransaction; xdr: string }> { + const { amount, recipient, fee, memoType, memoValue, mode, assetCode, assetIssuer } = transaction; + + const source = await loadAccount(account.address); + + if (!source) { + throw new NetworkDown(); + } + + const transactionBuilder = buildTransactionBuilder(source, fee); + let operation: xdr.Operation | null = null; + + if (mode === "changeTrust") { + if (!assetCode || !assetIssuer) { + throw new StellarAssetRequired(""); + } + + operation = buildChangeTrustOperation(assetCode, assetIssuer); + } else { + // Payment + const recipientAccount = await getRecipientAccount({ + recipient, + }); + + if (recipientAccount?.id) { + operation = buildPaymentOperation({ + destination: recipient, + amount, + assetCode, + assetIssuer, + }); + } else { + if (recipientAccount?.isMuxedAccount) { + throw new StellarMuxedAccountNotExist(""); + } + + operation = buildCreateAccountOperation(recipient, amount); + } + } + + transactionBuilder.addOperation(operation); + + const memo = buildMemo(memoType, memoValue); + if (memo) { + transactionBuilder.addMemo(memo); + } + + const craftedTransaction = transactionBuilder.setTimeout(0).build(); + return { + transaction: craftedTransaction, + xdr: craftedTransaction.toXDR(), + }; +} + +function buildMemo( + memoType?: string | null | undefined, + memoValue?: string | null | undefined, +): Memo | null { + if (memoType && memoValue) { + switch (memoType) { + case "MEMO_TEXT": + return Memo.text(memoValue); + case "MEMO_ID": + return Memo.id(memoValue); + case "MEMO_HASH": + return Memo.hash(memoValue); + case "MEMO_RETURN": + return Memo.return(memoValue); + } + } + return null; +} diff --git a/libs/coin-modules/coin-stellar/src/logic/estimateFees.ts b/libs/coin-modules/coin-stellar/src/logic/estimateFees.ts new file mode 100644 index 00000000000..4cf386aa8e4 --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/logic/estimateFees.ts @@ -0,0 +1,10 @@ +import { fetchBaseFee } from "../network"; + +/** + * Estimate the fees for one transaction + * @see {@link https://developers.stellar.org/docs/learn/fundamentals/fees-resource-limits-metering#inclusion-fee} + */ +export async function estimateFees(): Promise { + const fees = await fetchBaseFee(); + return BigInt(fees.recommendedFee); +} diff --git a/libs/coin-modules/coin-stellar/src/logic/getBalance.ts b/libs/coin-modules/coin-stellar/src/logic/getBalance.ts new file mode 100644 index 00000000000..b6f8cfc91fc --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/logic/getBalance.ts @@ -0,0 +1,6 @@ +import { fetchAccount } from "../network"; + +export async function getBalance(addr: string): Promise { + const { balance } = await fetchAccount(addr); + return BigInt(balance.toString()); +} diff --git a/libs/coin-modules/coin-stellar/src/logic/index.ts b/libs/coin-modules/coin-stellar/src/logic/index.ts new file mode 100644 index 00000000000..5bbaeb2211c --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/logic/index.ts @@ -0,0 +1,7 @@ +export { broadcast } from "./broadcast"; +export { combine } from "./combine"; +export { craftTransaction } from "./craftTransaction"; +export { estimateFees } from "./estimateFees"; +export { getBalance } from "./getBalance"; +export { lastBlock } from "./lastBlock"; +export { listOperations } from "./listOperations"; diff --git a/libs/coin-modules/coin-stellar/src/logic/lastBlock.ts b/libs/coin-modules/coin-stellar/src/logic/lastBlock.ts new file mode 100644 index 00000000000..9247f870dad --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/logic/lastBlock.ts @@ -0,0 +1,6 @@ +import type { BlockInfo } from "@ledgerhq/coin-framework/api/index"; +import { getLastBlock } from "../network"; + +export async function lastBlock(): Promise { + return await getLastBlock(); +} diff --git a/libs/coin-modules/coin-stellar/src/logic/listOperations.ts b/libs/coin-modules/coin-stellar/src/logic/listOperations.ts new file mode 100644 index 00000000000..1799f453f14 --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/logic/listOperations.ts @@ -0,0 +1,42 @@ +import type { Operation as LiveOperation } from "@ledgerhq/types-live"; +import { fetchOperations } from "../network"; + +export type Operation = { + hash: string; + address: string; + type: string; + value: bigint; + fee: bigint; + blockHeight: number; + senders: string[]; + recipients: string[]; + date: Date; + transactionSequenceNumber: number; +}; + +export async function listOperations(address: string, _blockHeight: number): Promise { + // Fake accountId + const accountId = ""; + const operations = await fetchOperations({ + accountId, + addr: address, + order: "asc", + cursor: "0", + }); + return operations.map(convertToCoreOperation(address)); +} + +const convertToCoreOperation = (address: string) => (operation: LiveOperation) => { + return { + hash: operation.hash, + address, + type: operation.type, + value: BigInt(operation.value.toString()), + fee: BigInt(operation.fee.toString()), + blockHeight: operation.blockHeight!, + senders: operation.senders, + recipients: operation.recipients, + date: operation.date, + transactionSequenceNumber: operation.transactionSequenceNumber ?? 0, + }; +}; diff --git a/libs/coin-modules/coin-stellar/src/logic/sdkWrapper.ts b/libs/coin-modules/coin-stellar/src/logic/sdkWrapper.ts new file mode 100644 index 00000000000..992e89a2b81 --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/logic/sdkWrapper.ts @@ -0,0 +1,60 @@ +import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies"; +import { + Account as StellarSdkAccount, + Operation as StellarSdkOperation, + TransactionBuilder, + Networks, + Asset, +} from "@stellar/stellar-sdk"; + +const currency = getCryptoCurrencyById("stellar"); + +export function buildTransactionBuilder(source: StellarSdkAccount, fee: bigint) { + const formattedFee = fee.toString(); + return new TransactionBuilder(source, { + fee: formattedFee, + networkPassphrase: Networks.PUBLIC, + }); +} + +export function buildChangeTrustOperation(assetCode: string, assetIssuer: string) { + return StellarSdkOperation.changeTrust({ + asset: new Asset(assetCode, assetIssuer), + }); +} + +export function buildCreateAccountOperation(destination: string, amount: bigint) { + const formattedAmount = getFormattedAmount(amount); + return StellarSdkOperation.createAccount({ + destination: destination, + startingBalance: formattedAmount, + }); +} + +export function buildPaymentOperation({ + destination, + amount, + assetCode, + assetIssuer, +}: { + destination: string; + amount: bigint; + assetCode: string | undefined; + assetIssuer: string | undefined; +}) { + const formattedAmount = getFormattedAmount(amount); + // Non-native assets should always have asset code and asset issuer. If an + // asset doesn't have both, we assume it is native asset. + const asset = assetCode && assetIssuer ? new Asset(assetCode, assetIssuer) : Asset.native(); + return StellarSdkOperation.payment({ + destination: destination, + amount: formattedAmount, + asset, + }); +} + +function getFormattedAmount(amount: bigint) { + const div = 10 ** currency.units[0].magnitude; + // BigInt division is always an integer, never a float. We need to convert first to a Number. + return (Number(amount) / div).toString(); +} diff --git a/libs/coin-modules/coin-stellar/src/network/horizon.ts b/libs/coin-modules/coin-stellar/src/network/horizon.ts index 7b95bd30121..c80cfd0e741 100644 --- a/libs/coin-modules/coin-stellar/src/network/horizon.ts +++ b/libs/coin-modules/coin-stellar/src/network/horizon.ts @@ -1,22 +1,29 @@ +import { parseCurrencyUnit } from "@ledgerhq/coin-framework/currencies"; +import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies"; import { LedgerAPI4xx, LedgerAPI5xx, NetworkDown } from "@ledgerhq/errors"; +import type { CacheRes } from "@ledgerhq/live-network/cache"; +import { makeLRUCache } from "@ledgerhq/live-network/cache"; +import { log } from "@ledgerhq/logs"; import type { Account, Operation } from "@ledgerhq/types-live"; -import { BigNumber } from "bignumber.js"; import { // @ts-expect-error stellar-sdk ts definition missing? AccountRecord, + BASE_FEE, + Horizon, NetworkError, + Networks, NotFoundError, - Horizon, - BASE_FEE, - Asset, - Operation as StellarSdkOperation, - Account as StellarSdkAccount, Transaction as StellarSdkTransaction, - TransactionBuilder, - Networks, + StrKey, + MuxedAccount, } from "@stellar/stellar-sdk"; -import { log } from "@ledgerhq/logs"; -import { getEnv } from "@ledgerhq/live-env"; +import { BigNumber } from "bignumber.js"; +import { + getAccountSpendableBalance, + getReservedBalance, + rawOperationsToOperations, +} from "../bridge/logic"; +import coinConfig from "../config"; import { type BalanceAsset, type NetworkInfo, @@ -24,20 +31,19 @@ import { type Signer, NetworkCongestionLevel, } from "../types"; -import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies"; -import { - getAccountSpendableBalance, - getReservedBalance, - rawOperationsToOperations, -} from "../logic"; -import { parseCurrencyUnit } from "@ledgerhq/coin-framework/currencies"; -const LIMIT = getEnv("API_STELLAR_HORIZON_FETCH_LIMIT"); const FALLBACK_BASE_FEE = 100; const TRESHOLD_LOW = 0.5; const TRESHOLD_MEDIUM = 0.75; +const FETCH_LIMIT = 100; const currency = getCryptoCurrencyById("stellar"); -const server = new Horizon.Server(getEnv("API_STELLAR_HORIZON")); +let server: Horizon.Server | undefined; +const getServer = () => { + if (!server) { + server = new Horizon.Server(coinConfig.getCoinConfig().explorer.url); + } + return server; +}; // Constants export const BASE_RESERVE = 0.5; @@ -48,7 +54,7 @@ export const MIN_BALANCE = 1; // and the version (0.26.1) used by `@ledgerhq/live-network/network`, it is not possible to use the interceptors // provided by `@ledgerhq/live-network/network`. Horizon.AxiosClient.interceptors.request.use(config => { - if (!getEnv("ENABLE_NETWORK_LOGS")) { + if (!coinConfig.getCoinConfig().enableNetworkLogs) { return config; } @@ -58,7 +64,7 @@ Horizon.AxiosClient.interceptors.request.use(config => { }); Horizon.AxiosClient.interceptors.response.use(response => { - if (getEnv("ENABLE_NETWORK_LOGS")) { + if (coinConfig.getCoinConfig().enableNetworkLogs) { const { url, method } = response.config; log("network-success", `${response.status} ${method} ${url}`, { data: response.data }); } @@ -70,24 +76,20 @@ Horizon.AxiosClient.interceptors.response.use(response => { if (next_href) { const next = new URL(next_href); - next.host = new URL(getEnv("API_STELLAR_HORIZON")).host; + next.host = new URL(coinConfig.getCoinConfig().explorer.url).host; response.data._links.next.href = next.toString(); } return response; }); -function getFormattedAmount(amount: BigNumber) { - return amount.div(new BigNumber(10).pow(currency.units[0].magnitude)).toString(10); -} - export async function fetchBaseFee(): Promise<{ baseFee: number; recommendedFee: number; networkCongestionLevel: NetworkCongestionLevel; }> { // For tests - if (getEnv("API_STELLAR_HORIZON_STATIC_FEE")) { + if (coinConfig.getCoinConfig().useStaticFees) { return { baseFee: 100, recommendedFee: 100, @@ -100,7 +102,7 @@ export async function fetchBaseFee(): Promise<{ let networkCongestionLevel = NetworkCongestionLevel.MEDIUM; try { - const feeStats = await server.feeStats(); + const feeStats = await getServer().feeStats(); const ledgerCapacityUsage = feeStats.ledger_capacity_usage; recommendedFee = new BigNumber(feeStats.fee_charged.mode).toNumber(); @@ -142,7 +144,7 @@ export async function fetchAccount(addr: string): Promise<{ let balance = "0"; try { - account = await server.accounts().accountId(addr).call(); + account = await getServer().accounts().accountId(addr).call(); balance = account.balances?.find(balance => { return balance.asset_type === "native"; @@ -195,10 +197,10 @@ export async function fetchOperations({ let operations: Operation[] = []; try { - let rawOperations = await server + let rawOperations = await getServer() .operations() .forAccount(addr) - .limit(LIMIT) + .limit(coinConfig.getCoinConfig().explorer.fetchLimit ?? FETCH_LIMIT) .order(order) .cursor(cursor) .includeFailed(true) @@ -252,7 +254,7 @@ export async function fetchOperations({ export async function fetchAccountNetworkInfo(account: Account): Promise { try { - const extendedAccount = await server.accounts().accountId(account.freshAddress).call(); + const extendedAccount = await getServer().accounts().accountId(account.freshAddress).call(); const baseReserve = getReservedBalance(extendedAccount); const { recommendedFee, networkCongestionLevel, baseFee } = await fetchBaseFee(); @@ -280,7 +282,7 @@ export async function fetchSequence(account: Account): Promise { export async function fetchSigners(account: Account): Promise { try { - const extendedAccount = await server.accounts().accountId(account.freshAddress).call(); + const extendedAccount = await getServer().accounts().accountId(account.freshAddress).call(); return extendedAccount.signers; } catch (error) { return []; @@ -289,64 +291,98 @@ export async function fetchSigners(account: Account): Promise { export async function broadcastTransaction(signedTransaction: string): Promise { const transaction = new StellarSdkTransaction(signedTransaction, Networks.PUBLIC); - const res = await server.submitTransaction(transaction, { + const res = await getServer().submitTransaction(transaction, { skipMemoRequiredCheck: true, }); return res.hash; } -export function buildPaymentOperation({ - destination, - amount, - assetCode, - assetIssuer, -}: { - destination: string; - amount: BigNumber; - assetCode: string | undefined; - assetIssuer: string | undefined; -}) { - const formattedAmount = getFormattedAmount(amount); - // Non-native assets should always have asset code and asset issuer. If an - // asset doesn't have both, we assume it is native asset. - const asset = assetCode && assetIssuer ? new Asset(assetCode, assetIssuer) : Asset.native(); - return StellarSdkOperation.payment({ - destination: destination, - amount: formattedAmount, - asset, - }); -} - -export function buildCreateAccountOperation(destination: string, amount: BigNumber) { - const formattedAmount = getFormattedAmount(amount); - return StellarSdkOperation.createAccount({ - destination: destination, - startingBalance: formattedAmount, - }); -} +export async function loadAccount(addr: string): Promise { + if (!addr || !addr.length) { + return null; + } -export function buildChangeTrustOperation(assetCode: string, assetIssuer: string) { - return StellarSdkOperation.changeTrust({ - asset: new Asset(assetCode, assetIssuer), - }); + try { + return await getServer().loadAccount(addr); + } catch (e) { + return null; + } } -export function buildTransactionBuilder(source: StellarSdkAccount, fee: BigNumber) { - const formattedFee = fee.toString(); - return new TransactionBuilder(source, { - fee: formattedFee, - networkPassphrase: Networks.PUBLIC, - }); +export async function getLastBlock(): Promise<{ + height: number; + hash: string; + time: Date; +}> { + const ledger = await getServer().ledgers().order("desc").limit(1).call(); + return { + height: ledger.records[0].sequence, + hash: ledger.records[0].hash, + time: new Date(ledger.records[0].closed_at), + }; } -export async function loadAccount(addr: string): Promise { - if (!addr || !addr.length) { +export const getRecipientAccount: CacheRes< + Array<{ + recipient: string; + }>, + { + id: string | null; + isMuxedAccount: boolean; + assetIds: string[]; + } | null +> = makeLRUCache( + async ({ recipient }) => await recipientAccount(recipient), + extract => extract.recipient, + { + max: 300, + ttl: 5 * 60, + }, // 5 minutes +); + +async function recipientAccount(address?: string): Promise<{ + id: string | null; + isMuxedAccount: boolean; + assetIds: string[]; +} | null> { + if (!address) { return null; } - try { - return await server.loadAccount(addr); - } catch (e) { + let accountAddress = address; + + const isMuxedAccount = StrKey.isValidMed25519PublicKey(address); + + if (isMuxedAccount) { + const muxedAccount = MuxedAccount.fromAddress(address, "0"); + accountAddress = muxedAccount.baseAccount().accountId(); + } + + const account: AccountRecord = await loadAccount(accountAddress); + + if (!account) { return null; } + + return { + id: account.id, + isMuxedAccount, + assetIds: account.balances.reduce((allAssets: any[], balance: any) => { + return [...allAssets, getBalanceId(balance)]; + }, []), + }; +} + +function getBalanceId(balance: BalanceAsset): string | null { + switch (balance.asset_type) { + case "native": + return "native"; + case "liquidity_pool_shares": + return balance.liquidity_pool_id || null; + case "credit_alphanum4": + case "credit_alphanum12": + return `${balance.asset_code}:${balance.asset_issuer}`; + default: + return null; + } } diff --git a/libs/coin-modules/coin-stellar/src/network/index.ts b/libs/coin-modules/coin-stellar/src/network/index.ts index 9b4530f7eff..3ca613ddc28 100644 --- a/libs/coin-modules/coin-stellar/src/network/index.ts +++ b/libs/coin-modules/coin-stellar/src/network/index.ts @@ -1,15 +1,13 @@ export { + broadcastTransaction, fetchAccount, fetchOperations, fetchBaseFee, fetchSequence, fetchSigners, fetchAccountNetworkInfo, - broadcastTransaction, - buildPaymentOperation, - buildCreateAccountOperation, - buildChangeTrustOperation, - buildTransactionBuilder, + getLastBlock, + getRecipientAccount, loadAccount, BASE_RESERVE, BASE_RESERVE_MIN_COUNT, diff --git a/libs/coin-modules/coin-stellar/src/hw-getAddress.ts b/libs/coin-modules/coin-stellar/src/signer/getAddress.ts similarity index 81% rename from libs/coin-modules/coin-stellar/src/hw-getAddress.ts rename to libs/coin-modules/coin-stellar/src/signer/getAddress.ts index 3d497a97b33..48047f90e03 100644 --- a/libs/coin-modules/coin-stellar/src/hw-getAddress.ts +++ b/libs/coin-modules/coin-stellar/src/signer/getAddress.ts @@ -2,9 +2,9 @@ import { GetAddressFn } from "@ledgerhq/coin-framework/bridge/getAddressWrapper" import { SignerContext } from "@ledgerhq/coin-framework/signer"; import { GetAddressOptions } from "@ledgerhq/coin-framework/derivation"; import { StrKey } from "@stellar/stellar-sdk"; -import { StellarSigner } from "./types/signer"; +import { StellarSigner } from "../types/signer"; -function resolver(signerContext: SignerContext): GetAddressFn { +function getAddress(signerContext: SignerContext): GetAddressFn { return async (deviceId: string, { path, verify }: GetAddressOptions) => { const rawPublicKey = await signerContext(deviceId, async signer => { const { rawPublicKey } = await signer.getPublicKey(path, verify); @@ -21,4 +21,4 @@ function resolver(signerContext: SignerContext): GetAddressFn { }; } -export default resolver; +export default getAddress; diff --git a/libs/coin-modules/coin-stellar/src/signer/index.ts b/libs/coin-modules/coin-stellar/src/signer/index.ts new file mode 100644 index 00000000000..1f3f13571b7 --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/signer/index.ts @@ -0,0 +1,7 @@ +/** + * This directory is the home for all types and logic based on Ledgers signer. + */ + +import getAddress from "./getAddress"; + +export default getAddress; diff --git a/libs/coin-modules/coin-stellar/src/speculos-deviceActions.ts b/libs/coin-modules/coin-stellar/src/test/bot-deviceActions.ts similarity index 98% rename from libs/coin-modules/coin-stellar/src/speculos-deviceActions.ts rename to libs/coin-modules/coin-stellar/src/test/bot-deviceActions.ts index 68a4a6f8df4..c628df2d0b8 100644 --- a/libs/coin-modules/coin-stellar/src/speculos-deviceActions.ts +++ b/libs/coin-modules/coin-stellar/src/test/bot-deviceActions.ts @@ -7,7 +7,7 @@ import { SpeculosButton, } from "@ledgerhq/coin-framework/bot/specs"; import { Account } from "@ledgerhq/types-live"; -import type { Transaction, TransactionStatus } from "./types"; +import type { Transaction, TransactionStatus } from "../types"; function expectedAmount({ account, diff --git a/libs/coin-modules/coin-stellar/src/specs.ts b/libs/coin-modules/coin-stellar/src/test/bot-specs.ts similarity index 98% rename from libs/coin-modules/coin-stellar/src/specs.ts rename to libs/coin-modules/coin-stellar/src/test/bot-specs.ts index f33f5a1f111..433cc37eaee 100644 --- a/libs/coin-modules/coin-stellar/src/specs.ts +++ b/libs/coin-modules/coin-stellar/src/test/bot-specs.ts @@ -9,8 +9,8 @@ import { parseCurrencyUnit } from "@ledgerhq/coin-framework/currencies"; import { AppSpec } from "@ledgerhq/coin-framework/bot/types"; import { botTest, pickSiblings } from "@ledgerhq/coin-framework/bot/specs"; import { listTokensForCryptoCurrency } from "@ledgerhq/cryptoassets/tokens"; -import { acceptTransaction } from "./speculos-deviceActions"; -import type { Transaction } from "./types"; +import { acceptTransaction } from "./bot-deviceActions"; +import type { Transaction } from "../types"; const currency = getCryptoCurrencyById("stellar"); const minAmountCutoff = parseCurrencyUnit(currency.units[0], "0.1"); @@ -29,7 +29,7 @@ const stellar: AppSpec = { name: "Stellar", currency, appQuery: { - model: DeviceModelId.nanoS, + model: DeviceModelId.nanoSP, appName: "Stellar", }, genericDeviceAction: acceptTransaction, diff --git a/libs/coin-modules/coin-stellar/src/bridge/bridge.integration.test.ts b/libs/coin-modules/coin-stellar/src/test/bridgeDatasetTest.ts similarity index 98% rename from libs/coin-modules/coin-stellar/src/bridge/bridge.integration.test.ts rename to libs/coin-modules/coin-stellar/src/test/bridgeDatasetTest.ts index 768334cccde..f36e5c0f70b 100644 --- a/libs/coin-modules/coin-stellar/src/bridge/bridge.integration.test.ts +++ b/libs/coin-modules/coin-stellar/src/test/bridgeDatasetTest.ts @@ -6,8 +6,8 @@ import { NotEnoughBalanceBecauseDestinationNotCreated, } from "@ledgerhq/errors"; import type { Transaction } from "../types/index"; -import transactionTransformer from "../transaction"; -import { StellarWrongMemoFormat } from "../errors"; +import transactionTransformer from "../bridge/transaction"; +import { StellarWrongMemoFormat } from "../types"; export const dataset: DatasetTest = { implementations: ["js"], @@ -365,9 +365,3 @@ export const dataset: DatasetTest = { }, }, }; - -describe("Stellar bridge", () => { - test.todo( - "This is an empty test to make jest command pass. Remove it once there is a real test.", - ); -}); diff --git a/libs/coin-modules/coin-stellar/src/cli.ts b/libs/coin-modules/coin-stellar/src/test/cli.ts similarity index 95% rename from libs/coin-modules/coin-stellar/src/cli.ts rename to libs/coin-modules/coin-stellar/src/test/cli.ts index 974703b53d1..fa1ea4d1d28 100644 --- a/libs/coin-modules/coin-stellar/src/cli.ts +++ b/libs/coin-modules/coin-stellar/src/test/cli.ts @@ -1,8 +1,8 @@ import invariant from "invariant"; import type { AccountLike, Account, AccountLikeArray } from "@ledgerhq/types-live"; import { getAccountCurrency } from "@ledgerhq/coin-framework/account/helpers"; -import type { Transaction } from "./types"; -import { getAssetIdFromTokenId } from "./tokens"; +import type { Transaction } from "../types"; +import { getAssetIdFromTokenId } from "../bridge/tokens"; const options = [ { diff --git a/libs/coin-modules/coin-stellar/src/test/index.ts b/libs/coin-modules/coin-stellar/src/test/index.ts new file mode 100644 index 00000000000..b4fce0ef353 --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/test/index.ts @@ -0,0 +1,6 @@ +import makeCliTools from "./cli"; + +export * from "./bridgeDatasetTest"; +export { makeCliTools }; +export * from "./bot-specs"; +export * from "./bot-deviceActions"; diff --git a/libs/coin-modules/coin-stellar/src/types/bridge.ts b/libs/coin-modules/coin-stellar/src/types/bridge.ts new file mode 100644 index 00000000000..ed0a5f21c03 --- /dev/null +++ b/libs/coin-modules/coin-stellar/src/types/bridge.ts @@ -0,0 +1,110 @@ +import { Horizon } from "@stellar/stellar-sdk"; +import type { BigNumber } from "bignumber.js"; +import type { + Account, + Operation, + OperationType, + TransactionCommon, + TransactionCommonRaw, + TransactionStatusCommon, + TransactionStatusCommonRaw, +} from "@ledgerhq/types-live"; + +export type NetworkInfo = { + family: "stellar"; + fees: BigNumber; + baseFee: BigNumber; + baseReserve: BigNumber; + networkCongestionLevel?: NetworkCongestionLevel | undefined; +}; + +export type NetworkInfoRaw = { + family: "stellar"; + fees: string; + baseFee: string; + baseReserve: string; + networkCongestionLevel?: NetworkCongestionLevel | undefined; +}; + +export enum NetworkCongestionLevel { + LOW = "LOW", + MEDIUM = "MEDIUM", + HIGH = "HIGH", +} + +export const StellarMemoType = ["NO_MEMO", "MEMO_TEXT", "MEMO_ID", "MEMO_HASH", "MEMO_RETURN"]; + +export type StellarTransactionMode = "send" | "changeTrust"; + +export type Transaction = TransactionCommon & { + family: "stellar"; + networkInfo?: NetworkInfo | null | undefined; + fees?: BigNumber | null; + baseReserve?: BigNumber | null; + memoType?: string | null; + memoValue?: string | null; + mode: StellarTransactionMode; + assetCode?: string; + assetIssuer?: string; +}; + +export type TransactionRaw = TransactionCommonRaw & { + family: "stellar"; + networkInfo?: NetworkInfoRaw | null | undefined; + fees?: string | null; + baseReserve?: string | null; + memoType?: string | null; + memoValue?: string | null; + mode: StellarTransactionMode; + assetCode?: string; + assetIssuer?: string; +}; + +export type BalanceAsset = { + balance: string; + limit: string; + buying_liabilities: string; + selling_liabilities: string; + last_modified_ledger: number; + is_authorized: boolean; + is_authorized_to_maintain_liabilities: boolean; + asset_type: string; + asset_code: string; + asset_issuer: string; + liquidity_pool_id?: string; +}; + +export type RawOperation = Horizon.ServerApi.OperationRecord & { + asset_code?: string; + asset_issuer?: string; + from?: string; + to?: string; + to_muxed?: string; + funder?: string; + trustor?: string; + account?: string; + transaction_successful: boolean; +}; + +export type Signer = { + weight: number; + key: string; + type: string; +}; + +export type TransactionStatus = TransactionStatusCommon; + +export type TransactionStatusRaw = TransactionStatusCommonRaw; + +export type StellarOperation = Operation; + +export type StellarOperationExtra = { + pagingToken?: string; + assetCode?: string; + assetIssuer?: string; + assetAmount?: string | undefined; + ledgerOpType: OperationType; + memo?: string; +}; + +export type StellarAccount = Account; diff --git a/libs/coin-modules/coin-stellar/src/errors.ts b/libs/coin-modules/coin-stellar/src/types/errors.ts similarity index 100% rename from libs/coin-modules/coin-stellar/src/errors.ts rename to libs/coin-modules/coin-stellar/src/types/errors.ts diff --git a/libs/coin-modules/coin-stellar/src/types/index.ts b/libs/coin-modules/coin-stellar/src/types/index.ts index 3680ecb549f..b2baff0e52a 100644 --- a/libs/coin-modules/coin-stellar/src/types/index.ts +++ b/libs/coin-modules/coin-stellar/src/types/index.ts @@ -1,112 +1,3 @@ -import { Horizon } from "@stellar/stellar-sdk"; -import type { BigNumber } from "bignumber.js"; -import type { - Account, - Operation, - OperationType, - TransactionCommon, - TransactionCommonRaw, - TransactionStatusCommon, - TransactionStatusCommonRaw, -} from "@ledgerhq/types-live"; - -export type NetworkInfo = { - family: "stellar"; - fees: BigNumber; - baseFee: BigNumber; - baseReserve: BigNumber; - networkCongestionLevel?: NetworkCongestionLevel | undefined; -}; - -export type NetworkInfoRaw = { - family: "stellar"; - fees: string; - baseFee: string; - baseReserve: string; - networkCongestionLevel?: NetworkCongestionLevel | undefined; -}; - -export enum NetworkCongestionLevel { - LOW = "LOW", - MEDIUM = "MEDIUM", - HIGH = "HIGH", -} - -export const StellarMemoType = ["NO_MEMO", "MEMO_TEXT", "MEMO_ID", "MEMO_HASH", "MEMO_RETURN"]; - -export type StellarTransactionMode = "send" | "changeTrust"; - -export type Transaction = TransactionCommon & { - family: "stellar"; - networkInfo?: NetworkInfo | null | undefined; - fees?: BigNumber | null; - baseReserve?: BigNumber | null; - memoType?: string | null; - memoValue?: string | null; - mode: StellarTransactionMode; - assetCode?: string; - assetIssuer?: string; -}; - -export type TransactionRaw = TransactionCommonRaw & { - family: "stellar"; - networkInfo?: NetworkInfoRaw | null | undefined; - fees?: string | null; - baseReserve?: string | null; - memoType?: string | null; - memoValue?: string | null; - mode: StellarTransactionMode; - assetCode?: string; - assetIssuer?: string; -}; - -export type BalanceAsset = { - balance: string; - limit: string; - buying_liabilities: string; - selling_liabilities: string; - last_modified_ledger: number; - is_authorized: boolean; - is_authorized_to_maintain_liabilities: boolean; - asset_type: string; - asset_code: string; - asset_issuer: string; - liquidity_pool_id?: string; -}; - -export type RawOperation = Horizon.ServerApi.OperationRecord & { - asset_code?: string; - asset_issuer?: string; - from?: string; - to?: string; - to_muxed?: string; - funder?: string; - trustor?: string; - account?: string; - transaction_successful: boolean; -}; - -export type Signer = { - weight: number; - key: string; - type: string; -}; - -export type TransactionStatus = TransactionStatusCommon; - -export type TransactionStatusRaw = TransactionStatusCommonRaw; - -export type StellarOperation = Operation; - -export type StellarOperationExtra = { - pagingToken?: string; - assetCode?: string; - assetIssuer?: string; - assetAmount?: string | undefined; - ledgerOpType: OperationType; - memo?: string; -}; - -export type StellarAccount = Account; - +export * from "./bridge"; +export * from "./errors"; export * from "./signer"; diff --git a/libs/coin-modules/coin-tezos/src/api/index.integ.test.ts b/libs/coin-modules/coin-tezos/src/api/index.integ.test.ts index 8e408281f60..e42bd57df02 100644 --- a/libs/coin-modules/coin-tezos/src/api/index.integ.test.ts +++ b/libs/coin-modules/coin-tezos/src/api/index.integ.test.ts @@ -85,6 +85,7 @@ describe("Tezos Api", () => { it("returns a raw transaction", async () => { // When const result = await module.craftTransaction(address, { + mode: "send", recipient: "tz1aWXP237BLwNHJcCD4b3DutCevhqq2T1Z9", amount: BigInt(10), fee: BigInt(1), diff --git a/libs/coin-modules/coin-xrp/src/api/index.integ.test.ts b/libs/coin-modules/coin-xrp/src/api/index.integ.test.ts index c67ce5d4b72..b9ad9945521 100644 --- a/libs/coin-modules/coin-xrp/src/api/index.integ.test.ts +++ b/libs/coin-modules/coin-xrp/src/api/index.integ.test.ts @@ -69,6 +69,7 @@ describe("Xrp Api", () => { it("returns a raw transaction", async () => { // When const result = await module.craftTransaction(address, { + mode: "send", recipient: "rKRtUG15iBsCQRgrkeUEg5oX4Ae2zWZ89z", amount: BigInt(10), fee: BigInt(1), diff --git a/libs/coin-modules/coin-xrp/src/api/index.ts b/libs/coin-modules/coin-xrp/src/api/index.ts index 02771b24a1e..6b70daf5e46 100644 --- a/libs/coin-modules/coin-xrp/src/api/index.ts +++ b/libs/coin-modules/coin-xrp/src/api/index.ts @@ -28,6 +28,7 @@ export function createApi(config: XrpConfig): Api { async function craft( address: string, transaction: { + mode: string; recipient: string; amount: bigint; fee: bigint; diff --git a/libs/ledger-live-common/src/families/stellar/bridge.integration.test.ts b/libs/ledger-live-common/src/families/stellar/bridge.integration.test.ts index 4326a059092..03eb5951de4 100644 --- a/libs/ledger-live-common/src/families/stellar/bridge.integration.test.ts +++ b/libs/ledger-live-common/src/families/stellar/bridge.integration.test.ts @@ -1,5 +1,5 @@ import "../../__tests__/test-helpers/setup"; import { testBridge } from "../../__tests__/test-helpers/bridge"; -import { dataset } from "@ledgerhq/coin-stellar/bridge/bridge.integration.test"; +import { dataset } from "@ledgerhq/coin-stellar/test/index"; testBridge(dataset); diff --git a/libs/ledger-live-common/src/families/stellar/config.ts b/libs/ledger-live-common/src/families/stellar/config.ts index 0186be0b9ae..da9920c317a 100644 --- a/libs/ledger-live-common/src/families/stellar/config.ts +++ b/libs/ledger-live-common/src/families/stellar/config.ts @@ -1,4 +1,5 @@ import { ConfigInfo } from "@ledgerhq/live-config/LiveConfig"; +import { getEnv } from "@ledgerhq/live-env"; export const stellarConfig: Record = { config_currency_stellar: { @@ -7,6 +8,12 @@ export const stellarConfig: Record = { status: { type: "active", }, + explorer: { + url: getEnv("API_STELLAR_HORIZON"), + fetchLimit: getEnv("API_STELLAR_HORIZON_FETCH_LIMIT"), + }, + useStaticFees: getEnv("API_STELLAR_HORIZON_STATIC_FEE"), + enableNetworkLogs: getEnv("ENABLE_NETWORK_LOGS"), }, }, }; diff --git a/libs/ledger-live-common/src/families/stellar/setup.ts b/libs/ledger-live-common/src/families/stellar/setup.ts index 1bbb728d355..c98ad966dc7 100644 --- a/libs/ledger-live-common/src/families/stellar/setup.ts +++ b/libs/ledger-live-common/src/families/stellar/setup.ts @@ -1,16 +1,20 @@ // Goal of this file is to inject all necessary device/signer dependency to coin-modules -import { Transaction, StellarAccount, TransactionStatus } from "@ledgerhq/coin-stellar/types/index"; -import Transport from "@ledgerhq/hw-transport"; -import Stellar from "@ledgerhq/hw-app-str"; -import type { Bridge } from "@ledgerhq/types-live"; +import { createBridges } from "@ledgerhq/coin-stellar/bridge/index"; +import makeCliTools from "@ledgerhq/coin-stellar/test/cli"; import { StellarCoinConfig } from "@ledgerhq/coin-stellar/config"; -import makeCliTools from "@ledgerhq/coin-stellar/cli"; +import stellarResolver from "@ledgerhq/coin-stellar/signer/index"; +import type { + StellarAccount, + Transaction, + TransactionStatus, +} from "@ledgerhq/coin-stellar/types/index"; import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies"; -import { createBridges } from "@ledgerhq/coin-stellar/bridge/index"; -import stellarResolver from "@ledgerhq/coin-stellar/hw-getAddress"; +import Stellar from "@ledgerhq/hw-app-str"; +import Transport from "@ledgerhq/hw-transport"; +import type { Bridge } from "@ledgerhq/types-live"; import { CreateSigner, createResolver, executeWithSigner } from "../../bridge/setup"; -import { Resolver } from "../../hw/getAddress/types"; import { getCurrencyConfiguration } from "../../config"; +import { Resolver } from "../../hw/getAddress/types"; const createSigner: CreateSigner = (transport: Transport) => { return new Stellar(transport); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94ae7163751..be6682cd204 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2491,9 +2491,6 @@ importers: '@ledgerhq/errors': specifier: workspace:^ version: link:../../ledgerjs/packages/errors - '@ledgerhq/live-env': - specifier: workspace:^ - version: link:../../env '@ledgerhq/live-network': specifier: workspace:^ version: link:../../live-network @@ -2531,12 +2528,15 @@ importers: '@types/jest': specifier: ^29.5.10 version: 29.5.12 + '@types/node': + specifier: ^20.8.10 + version: 20.12.12 jest: specifier: ^29.7.0 - version: 29.7.0 + version: 29.7.0(@types/node@20.12.12) ts-jest: specifier: ^29.1.1 - version: 29.1.4(jest@29.7.0)(typescript@5.4.3) + version: 29.1.5(jest@29.7.0(@types/node@20.12.12))(typescript@5.4.3) libs/coin-modules/coin-tezos: dependencies: @@ -25069,6 +25069,9 @@ packages: q@1.5.1: resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} engines: {node: '>=0.6.0', teleport: '>=0.2.0'} + deprecated: |- + You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qrcode-terminal@0.11.0: resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==} @@ -27833,6 +27836,30 @@ packages: esbuild: optional: true + ts-jest@29.1.5: + resolution: {integrity: sha512-UuClSYxM7byvvYfyWdFI+/2UxMmwNyJb0NPkZPQE2hew3RurV7l7zURgOHAd/1I1ZdPpe3GUsXNXAcN8TFKSIg==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + ts-loader@9.5.1: resolution: {integrity: sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==} engines: {node: '>=12.0.0'} @@ -57286,7 +57313,7 @@ snapshots: postcss-flexbugs-fixes: 5.0.2(postcss@8.4.38) postcss-normalize: 10.0.1(browserslist@4.23.0)(postcss@8.4.38) postcss-preset-env: 7.8.3(postcss@8.4.38) - semver: 7.5.4 + semver: 7.6.2 webpack: 5.89.0 transitivePeerDependencies: - browserslist @@ -61986,6 +62013,19 @@ snapshots: typescript: 5.4.3 yargs-parser: 21.1.1 + ts-jest@29.1.5(jest@29.7.0(@types/node@20.12.12))(typescript@5.4.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@20.12.12) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.6.2 + typescript: 5.4.3 + yargs-parser: 21.1.1 + ts-loader@9.5.1(typescript@5.1.3)(webpack@5.89.0): dependencies: chalk: 4.1.2