@ledgerhq/coin-stacks
Version:
Ledger Stacks Coin integration
291 lines • 11.6 kB
JavaScript
import { BigNumber } from "bignumber.js";
import { makeUnsignedSTXTokenTransfer, createMessageSignature, deserializeCV, cvToJSON, uintCV, standardPrincipalCV, stringAsciiCV, someCV, noneCV, makeUnsignedContractCall, StacksMessageType, PostConditionType, createStandardPrincipal, FungibleConditionCode, createAssetInfo, } 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";
import { bufferMemoToString, hexMemoToString } from "./memoUtils";
export const getTxToBroadcast = async (operation, signature, rawData) => {
const { value, recipients, senders, fee, extra: { memo }, } = operation;
const { anchorMode, network, xpub, contractAddress, contractName, assetName } = rawData;
if (contractAddress && contractName && assetName) {
// Create the function arguments for the SIP-010 transfer function
const functionArgs = [
uintCV(value.toFixed()), // Amount
standardPrincipalCV(senders[0]), // Sender
standardPrincipalCV(recipients[0]), // Recipient
memo ? someCV(stringAsciiCV(memo)) : noneCV(), // Memo (optional)
];
const tx = await makeUnsignedContractCall({
contractAddress,
contractName,
functionName: "transfer",
functionArgs,
anchorMode,
network: StacksNetwork[network],
publicKey: xpub,
fee: fee.toFixed(),
nonce: operation.transactionSequenceNumber?.toString() ?? "0",
postConditions: [
{
type: StacksMessageType.PostCondition,
conditionType: PostConditionType.Fungible,
principal: createStandardPrincipal(senders[0]),
conditionCode: FungibleConditionCode.Equal,
amount: BigInt(value.toFixed()),
assetInfo: createAssetInfo(contractAddress, contractName, assetName),
},
],
});
// 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());
}
else {
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?.toString() ?? "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 });
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 = hexMemoToString(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: new BigNumber(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 = hexMemoToString(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: new BigNumber(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: hexMemoToString(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 const sip010TxnToOperation = (tx, address, accountId) => {
try {
const { tx_id, fee_rate, nonce, block_height, burn_block_time, block_hash: blockHash } = tx.tx;
if (!tx.tx.contract_call) {
log("error", "contract_call is not defined", tx);
return [];
}
const contractCallData = tx.tx.contract_call;
const contractCallArgs = contractCallData.function_args;
if (contractCallArgs.length !== 4) {
log("error", "contractCallArgs len is not sip010 token transfer", tx);
return [];
}
const [valueArg, senderArg, receiverArg, memoArg] = contractCallArgs;
const sender = cvToJSON(deserializeCV(senderArg.hex)).value;
const receiver = cvToJSON(deserializeCV(receiverArg.hex)).value;
const valueStr = cvToJSON(deserializeCV(valueArg.hex)).value;
const memoJson = cvToJSON(deserializeCV(memoArg.hex)).value;
const memo = bufferMemoToString(memoJson);
const value = new BigNumber(valueStr);
const recipients = [receiver];
const senders = [sender];
const ops = [];
const date = new Date(burn_block_time * 1000);
const fee = new BigNumber(fee_rate || "0");
const hasFailed = tx.tx.tx_status !== "success";
let type = "";
if (address === sender) {
type = "OUT";
}
else if (address === receiver) {
type = "IN";
}
if (type === "") {
log("error", "op type is not known", tx);
return [];
}
ops.push({
hash: tx_id,
blockHeight: block_height,
blockHash,
fee,
accountId,
senders,
recipients,
transactionSequenceNumber: BigNumber(nonce),
date,
value,
hasFailed,
extra: {
memo,
},
type,
id: encodeOperationId(accountId, tx_id, type),
});
return ops;
}
catch (e) {
log("error", "stacks error converting sip010 transaction to operation", e);
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 sip010OpToParentOp = (tokenOps, parentAccountId) => {
return tokenOps.map(op => ({
...op,
accountId: parentAccountId,
value: BigNumber(0),
subOperations: [op],
id: encodeOperationId(parentAccountId, op.hash, op.type),
}));
};
export const findNextNonce = async (senderAddress, pendingOps) => {
let nextNonce = BigNumber(0);
for (const op of pendingOps) {
const nonce = op.transactionSequenceNumber ? 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