@ledgerhq/coin-stacks
Version:
Ledger Stacks Coin integration
412 lines (357 loc) • 11.8 kB
text/typescript
import { getCryptoCurrencyById } from "@ledgerhq/ledger-wallet-framework/currencies";
import { decodeAccountId } from "@ledgerhq/ledger-wallet-framework/account/index";
import {
encodeOperationId,
encodeSubOperationId,
} from "@ledgerhq/ledger-wallet-framework/operation";
import { log } from "@ledgerhq/logs";
import { Account, Operation, OperationType } from "@ledgerhq/types-live";
import {
makeUnsignedSTXTokenTransfer,
UnsignedTokenTransferOptions,
createMessageSignature,
deserializeCV,
cvToJSON,
uintCV,
standardPrincipalCV,
stringAsciiCV,
someCV,
noneCV,
makeUnsignedContractCall,
StacksMessageType,
PostConditionType,
createStandardPrincipal,
FungibleConditionCode,
createAssetInfo,
} from "@stacks/transactions";
import { BigNumber } from "bignumber.js";
import {
fetchFullMempoolTxs,
fetchNonce,
DecodedSendManyFunctionArgsCV,
MempoolTransaction,
StacksNetwork,
TransactionResponse,
} from "../../network/index";
import { StacksOperation } from "../../types";
import { bufferMemoToString, hexMemoToString } from "./memoUtils";
type ContractCallArg = {
hex: string;
repr: string;
name: string;
type: string;
};
type Sip010ContractCallArgs = [ContractCallArg, ContractCallArg, ContractCallArg, ContractCallArg];
export const getTxToBroadcast = async (
operation: StacksOperation,
signature: string,
rawData: Record<string, any>,
): Promise<Buffer> => {
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: UnsignedTokenTransferOptions = {
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: Account,
): {
address: string;
derivationPath: string;
} => ({ address: account.freshAddress, derivationPath: account.freshAddressPath });
export const mapPendingTxToOps =
(accountID: string, address: string) =>
(tx: MempoolTransaction): StacksOperation[] => {
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: StacksOperation[] = [];
if (isSending) {
const type: OperationType = "OUT";
ops.push({
...operationCommons,
id: encodeOperationId(accountID, tx_id, type),
type,
});
} else if (isReceiving) {
const type: OperationType = "IN";
ops.push({
...operationCommons,
id: encodeOperationId(accountID, tx_id, type),
type,
});
}
return ops;
};
export const mapTxToOps =
(accountID: string, address: string) =>
(tx: TransactionResponse): StacksOperation[] => {
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: string[] = [];
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: string = hexMemoToString(memoHex ?? "");
const ops: StacksOperation[] = [];
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: OperationType = "OUT";
let internalOperations: StacksOperation[] | undefined = 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: DecodedSendManyFunctionArgsCV = 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: OperationType = "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: TransactionResponse,
address: string,
accountId: string,
): StacksOperation[] => {
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 as Sip010ContractCallArgs;
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: string[] = [receiver];
const senders: string[] = [sender];
const ops: StacksOperation[] = [];
const date = new Date(burn_block_time * 1000);
const fee = new BigNumber(fee_rate || "0");
const hasFailed = tx.tx.tx_status !== "success";
let type: OperationType | "" = "";
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: string | undefined,
initialAccount: Account | undefined,
): string {
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: Operation[], parentAccountId: string): Operation[] => {
return tokenOps.map(op => ({
...op,
accountId: parentAccountId,
value: BigNumber(0),
subOperations: [op],
id: encodeOperationId(parentAccountId, op.hash, op.type),
}));
};
export const findNextNonce = async (
senderAddress: string,
pendingOps: Operation[],
): Promise<BigNumber> => {
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;
};