@okxweb3/coin-bitcoin
Version:
@ok/coin-bitcoin is a Bitcoin SDK for building Web3 wallets and applications. It supports BTC, BSV, DOGE, LTC, and TBTC, enabling private key management, transaction signing, address generation, and inscriptions like BRC-20, Runes, CAT, and Atomicals.
213 lines • 9.99 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.calcVsizeTransfer = exports.transfer = exports.estimateFee = void 0;
const common_1 = require("../common");
const scrypt_ts_1 = require("scrypt-ts");
const utils_1 = require("../utils");
const cat_smartcontracts_1 = require("@cat-protocol/cat-smartcontracts");
const functions_1 = require("./functions");
const merge_1 = require("./merge");
async function estimateFee(param) {
param.data.estimateFee = true;
return await transfer(param);
}
exports.estimateFee = estimateFee;
async function transfer(param) {
const txParams = param.data;
const ecKey = !txParams.estimateFee
? new utils_1.EcKeyService({ privateKey: param.privateKey })
: (0, utils_1.getDummyEcKey)();
let metadata = (0, utils_1.tokenInfoParse)(txParams.tokenMetadata, common_1.SupportedNetwork.fractalMainnet);
const minterP2TR = (0, utils_1.toP2tr)(metadata.minterAddr);
const { p2tr: tokenP2TR, tapScript: tokenTapScript } = (0, utils_1.getTokenContractP2TR)(minterP2TR);
let verifyScript = txParams.verifyScript || false;
let receiver;
let amount;
try {
receiver = common_1.btc.Address.fromString(txParams.toAddress);
}
catch (error) {
throw new Error(`Invalid receiver address: ${txParams.toAddress}`);
}
if (receiver.type !== 'taproot') {
throw new Error(`Invalid address type: ${receiver.type}`);
}
try {
amount = BigInt(txParams.tokenAmount);
}
catch (error) {
throw new Error(`Invalid token amount: ${txParams.tokenAmount}`);
}
if (amount <= 0n) {
throw new Error('Invalid token amount');
}
const feeUtxos = (0, utils_1.feeUtxoParse)(ecKey, param.data.feeInputs);
let feeUtxo = feeUtxos[0];
let mergeTx;
if (feeUtxos.length > 1) {
({ feeUtxo, mergeTx } = (0, merge_1.mergeFee)(ecKey, feeUtxos, txParams.feeRate));
}
let tokens = (0, utils_1.tokenUtxoParse)(txParams.tokens);
const commitResult = (0, functions_1.createGuardContract)(ecKey, feeUtxo, txParams.feeRate, tokens, tokenP2TR, txParams.changeAddress);
if (commitResult === null) {
return null;
}
const { commitTx, contact: guardContract, guardTapScript } = commitResult;
const newState = cat_smartcontracts_1.ProtocolState.getEmptyState();
const receiverTokenState = cat_smartcontracts_1.CAT20Proto.create(amount, (0, utils_1.toTokenAddress)(receiver));
newState.updateDataList(0, cat_smartcontracts_1.CAT20Proto.toByteString(receiverTokenState));
const tokenInputAmount = tokens.reduce((acc, t) => acc + t.state.data.amount, 0n);
const changeTokenInputAmount = tokenInputAmount - amount;
let changeTokenState = null;
if (changeTokenInputAmount > 0n) {
const tokenChangeAddress = ecKey.getTokenAddress();
changeTokenState = cat_smartcontracts_1.CAT20Proto.create(changeTokenInputAmount, tokenChangeAddress);
newState.updateDataList(1, cat_smartcontracts_1.CAT20Proto.toByteString(changeTokenState));
}
const newFeeUtxo = {
txId: commitTx.id,
outputIndex: 2,
script: commitTx.outputs[2].script.toHex(),
satoshis: commitTx.outputs[2].satoshis,
};
const inputUtxos = [
...tokens.map((t) => t.utxo),
guardContract.utxo,
newFeeUtxo,
];
if (inputUtxos.length > cat_smartcontracts_1.MAX_INPUT) {
throw new Error('Too many inputs, max 4 token inputs');
}
const revealTx = new common_1.btc.Transaction()
.from(inputUtxos)
.addOutput(new common_1.btc.Transaction.Output({
satoshis: 0,
script: (0, utils_1.toStateScript)(newState),
}))
.addOutput(new common_1.btc.Transaction.Output({
satoshis: common_1.Postage.TOKEN_POSTAGE,
script: tokenP2TR,
}))
.feePerByte(txParams.feeRate);
if (changeTokenState) {
revealTx.addOutput(new common_1.btc.Transaction.Output({
satoshis: common_1.Postage.TOKEN_POSTAGE,
script: tokenP2TR,
}));
}
const satoshiChangeScript = common_1.btc.Script.fromAddress(txParams.changeAddress);
revealTx.addOutput(new common_1.btc.Transaction.Output({
satoshis: 0,
script: satoshiChangeScript,
}));
let tokenTxs = [];
if (txParams.tokenPrevTxs.length !== tokens.length) {
throw new Error('Invalid tokenPrevTxs length');
}
for (let i = 0; i < tokens.length; i++) {
const prevTx = txParams.tokenPrevTxs[i].prevTx;
const prevPrevTx = txParams.tokenPrevTxs[i].prevPrevTx;
const res = (0, utils_1.validatePrevTx)(metadata, prevTx, prevPrevTx, common_1.SupportedNetwork.fractalMainnet);
if (res === null) {
throw new Error('prevTx does not match prevPrevTx');
}
tokenTxs.push(res);
}
const guardCommitTxHeader = (0, cat_smartcontracts_1.getTxHeaderCheck)(commitTx, guardContract.utxo.outputIndex);
const guardInputIndex = tokens.length;
const guardInfo = {
outputIndex: (0, utils_1.toTxOutpoint)(guardContract.utxo.txId, guardContract.utxo.outputIndex).outputIndex,
inputIndexVal: BigInt(guardInputIndex),
tx: guardCommitTxHeader.tx,
guardState: guardContract.state.data,
};
let { vsize } = await (0, exports.calcVsizeTransfer)(tokens, guardContract, revealTx, guardInfo, tokenTxs, tokenTapScript, guardTapScript, newState, receiverTokenState, changeTokenState, satoshiChangeScript, minterP2TR);
const satoshiChangeOutputIndex = changeTokenState === null ? 2 : 3;
let satoshiChangeAmount = revealTx.inputAmount - vsize * txParams.feeRate - common_1.Postage.TOKEN_POSTAGE - (changeTokenState === null ? 0 : common_1.Postage.TOKEN_POSTAGE);
let fee = vsize * txParams.feeRate;
if (satoshiChangeAmount <= common_1.CHANGE_MIN_POSTAGE) {
satoshiChangeAmount = 0;
revealTx.removeOutput(satoshiChangeOutputIndex);
const { vsize: newVsize, fee: newFee } = await (0, exports.calcVsizeTransfer)(tokens, guardContract, revealTx, guardInfo, tokenTxs, tokenTapScript, guardTapScript, newState, receiverTokenState, changeTokenState, satoshiChangeScript, minterP2TR);
vsize = newVsize;
fee = newFee;
}
else {
revealTx.outputs[satoshiChangeOutputIndex].satoshis = satoshiChangeAmount;
}
if (txParams.estimateFee) {
if (mergeTx) {
return {
mergeTx: (0, utils_1.getFee)(mergeTx),
commitTx: (0, utils_1.getFee)(commitTx),
revealTx: fee
};
}
return {
commitTx: (0, utils_1.getFee)(commitTx),
revealTx: fee
};
}
let changeInfo = null;
if (satoshiChangeAmount > BigInt(0)) {
changeInfo = {
script: (0, scrypt_ts_1.toByteString)(satoshiChangeScript.toHex()),
satoshis: (0, scrypt_ts_1.int2ByteString)(BigInt(satoshiChangeAmount), BigInt(8)),
};
}
const txCtxs = (0, cat_smartcontracts_1.getTxCtxMulti)(revealTx, tokens.map((_, i) => i).concat([tokens.length]), [
...new Array(tokens.length).fill(Buffer.from(tokenTapScript, 'hex')),
Buffer.from(guardTapScript, 'hex'),
]);
for (let i = 0; i < tokens.length; i++) {
const res = await (0, functions_1.unlockToken)(ecKey, tokens[i], i, tokenTxs[i].prevTx, tokenTxs[i].prevTokenInputIndex, tokenTxs[i].prevPrevTx, guardInfo, revealTx, minterP2TR, txCtxs[i], verifyScript);
if (!res) {
return null;
}
}
const res = await (0, functions_1.unlockGuard)(guardContract, guardInfo, guardInputIndex, newState, revealTx, receiverTokenState, changeTokenState, changeInfo, txCtxs[guardInputIndex], verifyScript);
if (!res) {
return null;
}
ecKey.signTx(revealTx);
if (mergeTx) {
return {
mergeTx: mergeTx.uncheckedSerialize(),
commitTx: commitTx.uncheckedSerialize(),
revealTx: revealTx.uncheckedSerialize(),
};
}
return {
commitTx: commitTx.uncheckedSerialize(),
revealTx: revealTx.uncheckedSerialize(),
};
}
exports.transfer = transfer;
const calcVsizeTransfer = async (tokens, guardContract, revealTx, guardInfo, tokenTxs, tokenTapScript, guardTapScript, newState, receiverTokenState, changeTokenState, satoshisChangeScript, minterP2TR) => {
const fakeEcKey = (0, utils_1.getDummyEcKey)();
const fakeFeeScript = common_1.btc.Script.fromAddress(fakeEcKey.getAddress());
const feeUtxoIndex = revealTx.inputs.length - 1;
const originalFeeScript = revealTx.inputs[feeUtxoIndex].output.script;
revealTx.inputs[feeUtxoIndex].output.setScript(fakeFeeScript);
const txCtxs = (0, cat_smartcontracts_1.getTxCtxMulti)(revealTx, tokens.map((_, i) => i).concat([tokens.length]), [
...new Array(tokens.length).fill(Buffer.from(tokenTapScript, 'hex')),
Buffer.from(guardTapScript, 'hex'),
]);
const guardInputIndex = tokens.length;
const changeInfo = {
script: satoshisChangeScript.toHex(),
satoshis: (0, scrypt_ts_1.int2ByteString)(0n, 8n),
};
for (let i = 0; i < tokens.length; i++) {
await (0, functions_1.unlockToken)(fakeEcKey, tokens[i], i, tokenTxs[i].prevTx, tokenTxs[i].prevTokenInputIndex, tokenTxs[i].prevPrevTx, guardInfo, revealTx, minterP2TR, txCtxs[i], false);
}
await (0, functions_1.unlockGuard)(guardContract, guardInfo, guardInputIndex, newState, revealTx, receiverTokenState, changeTokenState, changeInfo, txCtxs[guardInputIndex], false);
fakeEcKey.signTx(revealTx);
const vsize = revealTx.vsize;
const fee = (0, utils_1.getFee)(revealTx);
(0, utils_1.resetTx)(revealTx);
revealTx.inputs[feeUtxoIndex].output.setScript(originalFeeScript);
return { vsize, fee };
};
exports.calcVsizeTransfer = calcVsizeTransfer;
//# sourceMappingURL=transfer.js.map