@ledgerhq/coin-stacks
Version:
Ledger Stacks Coin integration
303 lines • 12.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.findNextNonce = exports.sip010OpToParentOp = exports.sip010TxnToOperation = exports.mapTxToOps = exports.mapPendingTxToOps = exports.getAddress = exports.getUnit = exports.getTxToBroadcast = void 0;
exports.reconciliatePublicKey = reconciliatePublicKey;
const bignumber_js_1 = require("bignumber.js");
const transactions_1 = require("@stacks/transactions");
const index_1 = require("@ledgerhq/coin-framework/account/index");
const index_2 = require("../../network/index");
const currencies_1 = require("@ledgerhq/cryptoassets/currencies");
const operation_1 = require("@ledgerhq/coin-framework/operation");
const logs_1 = require("@ledgerhq/logs");
const memoUtils_1 = require("./memoUtils");
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 = [
(0, transactions_1.uintCV)(value.toFixed()), // Amount
(0, transactions_1.standardPrincipalCV)(senders[0]), // Sender
(0, transactions_1.standardPrincipalCV)(recipients[0]), // Recipient
memo ? (0, transactions_1.someCV)((0, transactions_1.stringAsciiCV)(memo)) : (0, transactions_1.noneCV)(), // Memo (optional)
];
const tx = await (0, transactions_1.makeUnsignedContractCall)({
contractAddress,
contractName,
functionName: "transfer",
functionArgs,
anchorMode,
network: index_2.StacksNetwork[network],
publicKey: xpub,
fee: fee.toFixed(),
nonce: operation.transactionSequenceNumber?.toString() ?? "0",
postConditions: [
{
type: transactions_1.StacksMessageType.PostCondition,
conditionType: transactions_1.PostConditionType.Fungible,
principal: (0, transactions_1.createStandardPrincipal)(senders[0]),
conditionCode: transactions_1.FungibleConditionCode.Equal,
amount: BigInt(value.toFixed()),
assetInfo: (0, transactions_1.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 = (0, transactions_1.createMessageSignature)(signature);
return Buffer.from(tx.serialize());
}
else {
const options = {
amount: (0, bignumber_js_1.BigNumber)(value).minus(fee).toFixed(),
recipient: recipients[0],
anchorMode,
memo,
network: index_2.StacksNetwork[network],
publicKey: xpub,
fee: (0, bignumber_js_1.BigNumber)(fee).toFixed(),
nonce: operation.transactionSequenceNumber?.toString() ?? "0",
};
const tx = await (0, transactions_1.makeUnsignedSTXTokenTransfer)(options);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore need to ignore the TS error here
tx.auth.spendingCondition.signature = (0, transactions_1.createMessageSignature)(signature);
return Buffer.from(tx.serialize());
}
};
exports.getTxToBroadcast = getTxToBroadcast;
const getUnit = () => (0, currencies_1.getCryptoCurrencyById)("stacks").units[0];
exports.getUnit = getUnit;
const getAddress = (account) => ({ address: account.freshAddress, derivationPath: account.freshAddressPath });
exports.getAddress = getAddress;
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 = (0, memoUtils_1.hexMemoToString)(token_transfer.memo);
const feeToUse = new bignumber_js_1.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_js_1.BigNumber(nonce),
value: new bignumber_js_1.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: (0, operation_1.encodeOperationId)(accountID, tx_id, type),
type,
});
}
else if (isReceiving) {
const type = "IN";
ops.push({
...operationCommons,
id: (0, operation_1.encodeOperationId)(accountID, tx_id, type),
type,
});
}
return ops;
};
exports.mapPendingTxToOps = mapPendingTxToOps;
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 = (0, memoUtils_1.hexMemoToString)(memoHex ?? "");
const ops = [];
const date = new Date(burn_block_time * 1000);
const feeToUse = new bignumber_js_1.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_js_1.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 = (0, transactions_1.deserializeCV)(tx.tx.contract_call.function_args[0].hex);
const decodedArgs = (0, transactions_1.cvToJSON)(deserialized);
for (const [idx, t] of decodedArgs.value.entries()) {
internalOperations.push({
...operationCommons,
id: (0, operation_1.encodeSubOperationId)(accountID, tx_id, type, idx),
contract: "send-many",
type,
value: new bignumber_js_1.BigNumber(t.value.ustx.value),
senders: [sender_address],
recipients: [t.value.to.value],
extra: {
memo: (0, memoUtils_1.hexMemoToString)(t.value.memo?.value ?? ""),
},
});
}
}
ops.push({
...operationCommons,
id: (0, operation_1.encodeOperationId)(accountID, tx_id, type),
value: new bignumber_js_1.BigNumber(sentValue),
recipients,
type,
internalOperations,
});
}
if (isReceiving) {
const type = "IN";
ops.push({
...operationCommons,
id: (0, operation_1.encodeOperationId)(accountID, tx_id, type),
value: new bignumber_js_1.BigNumber(receivedValue),
recipients: recipients.length ? recipients : [address],
type,
});
}
return ops;
}
catch (err) {
(0, logs_1.log)("warn", "mapTxToOps failed for stacks", err);
return [];
}
};
exports.mapTxToOps = mapTxToOps;
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) {
(0, logs_1.log)("error", "contract_call is not defined", tx);
return [];
}
const contractCallData = tx.tx.contract_call;
const contractCallArgs = contractCallData.function_args;
if (contractCallArgs.length !== 4) {
(0, logs_1.log)("error", "contractCallArgs len is not sip010 token transfer", tx);
return [];
}
const [valueArg, senderArg, receiverArg, memoArg] = contractCallArgs;
const sender = (0, transactions_1.cvToJSON)((0, transactions_1.deserializeCV)(senderArg.hex)).value;
const receiver = (0, transactions_1.cvToJSON)((0, transactions_1.deserializeCV)(receiverArg.hex)).value;
const valueStr = (0, transactions_1.cvToJSON)((0, transactions_1.deserializeCV)(valueArg.hex)).value;
const memoJson = (0, transactions_1.cvToJSON)((0, transactions_1.deserializeCV)(memoArg.hex)).value;
const memo = (0, memoUtils_1.bufferMemoToString)(memoJson);
const value = new bignumber_js_1.BigNumber(valueStr);
const recipients = [receiver];
const senders = [sender];
const ops = [];
const date = new Date(burn_block_time * 1000);
const fee = new bignumber_js_1.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 === "") {
(0, logs_1.log)("error", "op type is not known", tx);
return [];
}
ops.push({
hash: tx_id,
blockHeight: block_height,
blockHash,
fee,
accountId,
senders,
recipients,
transactionSequenceNumber: (0, bignumber_js_1.BigNumber)(nonce),
date,
value,
hasFailed,
extra: {
memo,
},
type,
id: (0, operation_1.encodeOperationId)(accountId, tx_id, type),
});
return ops;
}
catch (e) {
(0, logs_1.log)("error", "stacks error converting sip010 transaction to operation", e);
return [];
}
};
exports.sip010TxnToOperation = sip010TxnToOperation;
function reconciliatePublicKey(publicKey, initialAccount) {
if (publicKey)
return publicKey;
if (initialAccount) {
const { xpubOrAddress } = (0, index_1.decodeAccountId)(initialAccount.id);
return xpubOrAddress;
}
throw new Error("publicKey wasn't properly restored");
}
const sip010OpToParentOp = (tokenOps, parentAccountId) => {
return tokenOps.map(op => ({
...op,
accountId: parentAccountId,
value: (0, bignumber_js_1.BigNumber)(0),
subOperations: [op],
id: (0, operation_1.encodeOperationId)(parentAccountId, op.hash, op.type),
}));
};
exports.sip010OpToParentOp = sip010OpToParentOp;
const findNextNonce = async (senderAddress, pendingOps) => {
let nextNonce = (0, bignumber_js_1.BigNumber)(0);
for (const op of pendingOps) {
const nonce = op.transactionSequenceNumber ? op.transactionSequenceNumber : new bignumber_js_1.BigNumber(0);
if (nonce.gt(nextNonce)) {
nextNonce = nonce;
}
}
const allMempoolTxns = await (0, index_2.fetchFullMempoolTxs)(senderAddress);
for (const tx of allMempoolTxns) {
const nonce = (0, bignumber_js_1.BigNumber)(tx.nonce);
if (nonce.gt(nextNonce)) {
nextNonce = nonce;
}
}
if (!nextNonce.eq(0)) {
nextNonce = nextNonce.plus(1);
}
const nonceResp = await (0, index_2.fetchNonce)(senderAddress);
const possibleNextNonce = new bignumber_js_1.BigNumber(nonceResp.possible_next_nonce);
if (possibleNextNonce.gt(nextNonce)) {
nextNonce = possibleNextNonce;
}
return nextNonce;
};
exports.findNextNonce = findNextNonce;
//# sourceMappingURL=misc.js.map