@ledgerhq/coin-stellar
Version:
Ledger Stellar Coin integration
129 lines • 5.11 kB
JavaScript
import coinConfig from "../config";
import { broadcast, combine, craftTransaction, estimateFees, getBalance, lastBlock, listOperations, } from "../logic";
import { LedgerAPI4xx } from "@ledgerhq/errors";
import { log } from "@ledgerhq/logs";
import { xdr } from "@stellar/stellar-sdk";
export function createApi(config) {
coinConfig.setCoinConfig(() => ({ ...config, status: { type: "active" } }));
return {
broadcast,
combine: compose,
craftTransaction: craft,
estimateFees: estimate,
getBalance,
lastBlock,
listOperations: operations,
getBlock(_height) {
throw new Error("getBlock is not supported");
},
getBlockInfo(_height) {
throw new Error("getBlockInfo is not supported");
},
};
}
async function craft(transactionIntent, customFees) {
const fees = customFees !== undefined ? customFees : await estimateFees();
// NOTE: check how many memos, throw if more than one?
// if (transactionIntent.memos && transactionIntent.memos.length > 1) {
// throw new Error("Stellar only supports one memo per transaction.");
// }
const memo = "memo" in transactionIntent ? transactionIntent.memo : undefined;
const hasMemoValue = memo && memo.type !== "NO_MEMO";
const tx = await craftTransaction({ address: transactionIntent.sender }, {
type: transactionIntent.type,
recipient: transactionIntent.recipient,
amount: transactionIntent.amount,
fee: fees,
...(transactionIntent.asset.type === "token"
? {
assetCode: transactionIntent.asset.assetCode,
assetIssuer: transactionIntent.asset.assetIssuer,
}
: {}),
memoType: memo?.type,
...(hasMemoValue ? { memoValue: memo.value } : {}),
});
// Note: the API returns the signature base, not the full XDR, see BACK-8727 for more context
return tx.signatureBase;
}
function compose(tx, signature, pubkey) {
if (!pubkey) {
throw new Error("Missing pubkey");
}
// note: accept here `TransactionEnvelope` or `TransactionSignaturePayload`, see BACK-8727 for more context
return combine(envelopeFromAnyXDR(tx, "base64"), signature, pubkey);
}
async function estimate() {
const value = await estimateFees();
return { value };
}
async function operations(address, { minHeight }) {
return operationsFromHeight(address, minHeight);
}
async function operationsFromHeight(address, minHeight) {
const state = {
pageSize: 200,
heightLimit: minHeight,
continueIterations: true,
accumulator: [],
};
// unfortunately, the stellar API does not support an option to filter by min height
// so the only strategy to get ALL operations is to iterate over all of them in descending order
// until we reach the desired minHeight
while (state.continueIterations) {
const options = { limit: state.pageSize, order: "desc", minHeight };
if (state.apiNextCursor) {
options.cursor = state.apiNextCursor;
}
try {
const [operations, nextCursor] = await listOperations(address, options);
state.accumulator.push(...operations);
state.apiNextCursor = nextCursor;
state.continueIterations = nextCursor !== "";
}
catch (e) {
if (e instanceof LedgerAPI4xx && e.status === 429) {
log("coin:stellar", "(api/operations): TooManyRequests, retrying in 4s");
await new Promise(resolve => setTimeout(resolve, 4000));
}
else {
throw e;
}
}
}
return [state.accumulator, state.apiNextCursor ? state.apiNextCursor : ""];
}
/**
* Deserialize a transaction envelope, also accepting transaction signature payload form.
*
* @param input serialized `TransactionEnvelope` or `TransactionSignaturePayload`
* @param format serialization encoding
*/
export function envelopeFromAnyXDR(input, format) {
try {
return xdr.TransactionEnvelope.fromXDR(input, format);
}
catch (envelopeError) {
try {
return signatureBaseToEnvelope(xdr.TransactionSignaturePayload.fromXDR(input, format));
}
catch (signatureBaseError) {
throw new Error(`Failed decoding transaction as an envelope (${envelopeError}) or as a signature base: (${signatureBaseError})`);
}
}
}
/**
* Convert a `TransactionSignaturePayload` into a `TransactionEnvelope`.
*
* @param signatureBase deserialized `TransactionSignaturePayload`
*/
function signatureBaseToEnvelope(signatureBase) {
const tx = signatureBase.taggedTransaction().value();
if (tx instanceof xdr.Transaction) {
return xdr.TransactionEnvelope.envelopeTypeTx(new xdr.TransactionV1Envelope({ tx, signatures: [] }));
}
else {
return xdr.TransactionEnvelope.envelopeTypeTxFeeBump(new xdr.FeeBumpTransactionEnvelope({ tx, signatures: [] }));
}
}
//# sourceMappingURL=index.js.map