@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
168 lines (149 loc) • 6.1 kB
text/typescript
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),
);
});