UNPKG

@ledgerhq/live-common

Version:
168 lines (149 loc) • 6.1 kB
import { Observable } from "rxjs"; import { SignerContext } from "@ledgerhq/coin-framework/signer"; import type { Account, DeviceId, SignOperationEvent, AccountBridge } from "@ledgerhq/types-live"; import { getAlpacaApi } from "./alpaca"; import { buildOptimisticOperation, transactionToIntent } from "./utils"; import { FeeNotLoaded } from "@ledgerhq/errors"; import { Result } from "@ledgerhq/coin-framework/derivation"; import { MapMemo, TransactionIntent } from "@ledgerhq/coin-framework/api/types"; import { StellarMemo } from "@ledgerhq/coin-stellar/types/bridge"; import { log } from "@ledgerhq/logs"; import BigNumber from "bignumber.js"; import { GenericTransaction } from "./types"; /** * Applies memo information to transaction intent * Handles both destination tags (XRP-like) and Stellar-style memos */ function applyMemoToIntent( transactionIntent: TransactionIntent<any>, transaction: GenericTransaction, ): TransactionIntent<any> { // Handle destination tag memo (for XRP-like chains) if (transaction.tag) { const txWithMemoTag = transactionIntent as TransactionIntent<MapMemo<string, string>>; const txMemo = String(transaction.tag); txWithMemoTag.memo = { type: "map", memos: new Map(), }; txWithMemoTag.memo.memos.set("destinationTag", txMemo); return txWithMemoTag; } // Handle Stellar-style memo if (transaction.memoType && transaction.memoValue) { const txWithMemo = transactionIntent as TransactionIntent<StellarMemo>; const txMemoType = String(transaction.memoType); const txMemoValue = String(transaction.memoValue); txWithMemo.memo = { type: txMemoType as "NO_MEMO" | "MEMO_TEXT" | "MEMO_ID" | "MEMO_HASH" | "MEMO_RETURN", value: txMemoValue, }; return txWithMemo; } return transactionIntent; } /** * Enriches transaction intent with memo and asset information */ function enrichTransactionIntent( transactionIntent: TransactionIntent<any>, transaction: GenericTransaction, publicKey: string, ): TransactionIntent<any> { // Set sender public key transactionIntent.senderPublicKey = publicKey; // Apply memo information transactionIntent = applyMemoToIntent(transactionIntent, transaction); return transactionIntent; } /** * Sign Transaction with Ledger hardware */ export const genericSignOperation = (network, kind) => (signerContext: SignerContext<any>): AccountBridge<GenericTransaction>["signOperation"] => ({ account, transaction, deviceId, }: { account: Account; transaction: GenericTransaction; deviceId: DeviceId; }): Observable<SignOperationEvent> => new Observable(o => { async function main() { const alpacaApi = getAlpacaApi(account.currency.id, kind); if (!transaction.fees) throw new FeeNotLoaded(); const fees = BigInt(transaction.fees?.toString() || "0"); if (transaction.useAllAmount) { const draftTransaction = { mode: transaction.mode, recipient: transaction.recipient, amount: transaction.amount ?? 0, useAllAmount: !!transaction.useAllAmount, assetReference: transaction?.assetReference || "", assetOwner: transaction?.assetOwner || "", subAccountId: transaction.subAccountId || "", family: transaction.family, feesStrategy: transaction.feesStrategy, data: transaction.data, }; const { amount } = await alpacaApi.validateIntent( transactionToIntent(account, draftTransaction, alpacaApi.computeIntentType), { value: fees }, ); transaction.amount = new BigNumber(amount.toString()); } const signedInfo = await signerContext(deviceId, async signer => { const derivationPath = account.freshAddressPath; const { publicKey } = (await signer.getAddress(derivationPath)) as Result; let transactionIntent = transactionToIntent( account, { ...transaction }, alpacaApi.computeIntentType, ); transactionIntent.senderPublicKey = publicKey; // Enrich with memo and asset information transactionIntent = enrichTransactionIntent(transactionIntent, transaction, publicKey); // TODO: should compute it and pass it down to craftTransaction (duplicate call right now) const sequenceNumber = await alpacaApi.getSequence(transactionIntent.sender); transactionIntent.sequence = sequenceNumber; /* Craft unsigned blob via Alpaca */ const { transaction: unsigned } = await alpacaApi.craftTransaction(transactionIntent, { value: fees, }); /* Notify UI that the device is now showing the tx */ o.next({ type: "device-signature-requested" }); /* Sign on Ledger device */ const txnSig = await signer.signTransaction(derivationPath, unsigned); return { unsigned, txnSig, publicKey, sequence: transactionIntent.sequence }; }); /* If the user cancelled inside signerContext */ if (!signedInfo) return; o.next({ type: "device-signature-granted" }); /* Combine payload + signature for broadcast */ const combined = await alpacaApi.combine( signedInfo.unsigned, signedInfo.txnSig, signedInfo.publicKey, ); const operation = buildOptimisticOperation(account, transaction, signedInfo.sequence); if (!operation.id) { log("Generic alpaca", "buildOptimisticOperation", operation); } // NOTE: we set the transactionSequenceNumber before on the operation // now that we create it in craftTransaction, we might need to return it back from craftTransaction also o.next({ type: "signed", signedOperation: { operation, signature: combined, }, }); } main().then( () => o.complete(), e => o.error(e), ); });