@ledgerhq/coin-stacks
Version:
Ledger Stacks Coin integration
193 lines • 7.46 kB
JavaScript
import { BigNumber } from "bignumber.js";
import { makeUnsignedSTXTokenTransfer, createMessageSignature, deserializeCV, cvToJSON, } from "@stacks/transactions";
import { decodeAccountId } from "@ledgerhq/coin-framework/account/index";
import { fetchFullMempoolTxs, fetchNonce, StacksNetwork, } from "../../network/index";
import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies";
import { encodeOperationId, encodeSubOperationId } from "@ledgerhq/coin-framework/operation";
import { log } from "@ledgerhq/logs";
export const getTxToBroadcast = async (operation, signature, rawData) => {
const { value, recipients, fee, extra: { memo }, } = operation;
const { anchorMode, network, xpub } = rawData;
const options = {
amount: BigNumber(value).minus(fee).toFixed(),
recipient: recipients[0],
anchorMode,
memo,
network: StacksNetwork[network],
publicKey: xpub,
fee: BigNumber(fee).toFixed(),
nonce: operation.transactionSequenceNumber ?? 0,
};
const tx = await makeUnsignedSTXTokenTransfer(options);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore need to ignore the TS error here
tx.auth.spendingCondition.signature = createMessageSignature(signature);
return Buffer.from(tx.serialize());
};
export const getUnit = () => getCryptoCurrencyById("stacks").units[0];
export const getAddress = (account) => ({ address: account.freshAddress, derivationPath: account.freshAddressPath });
const getMemo = (memoHex) => {
if (memoHex?.substring(0, 2) === "0x") {
// eslint-disable-next-line no-control-regex
return Buffer.from(memoHex.substring(2), "hex").toString().replace(/\x00/g, "");
// NOTE: couldn't use replaceAll because it's not supported in node 14
}
return "";
};
export const mapPendingTxToOps = (accountID, address) => (tx) => {
const { sender_address, receipt_time, fee_rate, tx_id, token_transfer, tx_status, nonce } = tx;
if (tx.tx_type !== "token_transfer" || tx_status !== "pending") {
return [];
}
const memo = getMemo(token_transfer.memo);
const feeToUse = new BigNumber(fee_rate || "0");
const date = new Date(receipt_time * 1000);
const operationCommons = {
hash: tx_id,
fee: feeToUse,
accountId: accountID,
senders: [sender_address],
recipients: [token_transfer.recipient_address],
transactionSequenceNumber: nonce,
value: new BigNumber(token_transfer.amount).plus(feeToUse),
date,
extra: {
memo,
},
blockHeight: null,
blockHash: null,
};
const isSending = address === sender_address;
const isReceiving = token_transfer.recipient_address === address;
const ops = [];
if (isSending) {
const type = "OUT";
ops.push({
...operationCommons,
id: encodeOperationId(accountID, tx_id, type),
type,
});
}
else if (isReceiving) {
const type = "IN";
ops.push({
...operationCommons,
id: encodeOperationId(accountID, tx_id, type),
type,
});
}
return ops;
};
export const mapTxToOps = (accountID, address) => (tx) => {
try {
const { tx_id, fee_rate, nonce, block_height, burn_block_time, sender_address, block_hash: blockHash, } = tx.tx;
const { stx_received: receivedValue, stx_sent: sentValue } = tx;
let recipients = [];
if (tx.tx.tx_type === "token_transfer" && tx.tx.token_transfer) {
recipients = [tx.tx.token_transfer.recipient_address];
}
const memoHex = tx.tx.token_transfer?.memo;
const memo = getMemo(memoHex ?? "");
const ops = [];
const date = new Date(burn_block_time * 1000);
const feeToUse = new BigNumber(fee_rate || "0");
const isSending = sentValue !== "0" && receivedValue === "0";
const isReceiving = receivedValue !== "0";
const operationCommons = {
hash: tx_id,
blockHeight: block_height,
blockHash,
fee: feeToUse,
accountId: accountID,
senders: [sender_address],
transactionSequenceNumber: nonce,
date,
extra: {
memo,
},
};
if (isSending) {
const type = "OUT";
let internalOperations = undefined;
if (tx.tx.tx_type === "contract_call" && tx.tx.contract_call) {
internalOperations = [];
const deserialized = deserializeCV(tx.tx.contract_call.function_args[0].hex);
const decodedArgs = cvToJSON(deserialized);
for (const [idx, t] of decodedArgs.value.entries()) {
internalOperations.push({
...operationCommons,
id: encodeSubOperationId(accountID, tx_id, type, idx),
contract: "send-many",
type,
value: new BigNumber(t.value.ustx.value),
senders: [sender_address],
recipients: [t.value.to.value],
extra: {
memo: getMemo(t.value.memo?.value ?? ""),
},
});
}
}
ops.push({
...operationCommons,
id: encodeOperationId(accountID, tx_id, type),
value: new BigNumber(sentValue),
recipients,
type,
internalOperations,
});
}
if (isReceiving) {
const type = "IN";
ops.push({
...operationCommons,
id: encodeOperationId(accountID, tx_id, type),
value: new BigNumber(receivedValue),
recipients: recipients.length ? recipients : [address],
type,
});
}
return ops;
}
catch (err) {
log("warn", "mapTxToOps failed for stacks", err);
return [];
}
};
export function reconciliatePublicKey(publicKey, initialAccount) {
if (publicKey)
return publicKey;
if (initialAccount) {
const { xpubOrAddress } = decodeAccountId(initialAccount.id);
return xpubOrAddress;
}
throw new Error("publicKey wasn't properly restored");
}
export const findNextNonce = async (senderAddress, pendingOps) => {
let nextNonce = BigNumber(0);
for (const op of pendingOps) {
const nonce = op.transactionSequenceNumber
? new BigNumber(op.transactionSequenceNumber)
: new BigNumber(0);
if (nonce.gt(nextNonce)) {
nextNonce = nonce;
}
}
const allMempoolTxns = await fetchFullMempoolTxs(senderAddress);
for (const tx of allMempoolTxns) {
const nonce = BigNumber(tx.nonce);
if (nonce.gt(nextNonce)) {
nextNonce = nonce;
}
}
if (!nextNonce.eq(0)) {
nextNonce = nextNonce.plus(1);
}
const nonceResp = await fetchNonce(senderAddress);
const possibleNextNonce = new BigNumber(nonceResp.possible_next_nonce);
if (possibleNextNonce.gt(nextNonce)) {
nextNonce = possibleNextNonce;
}
return nextNonce;
};
//# sourceMappingURL=misc.js.map