UNPKG

meta-contract-debug

Version:

Meta Contract SDK

986 lines (985 loc) 99.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NftManager = void 0; const DustCalculator_1 = require("../common/DustCalculator"); const tx_composer_1 = require("../tx-composer"); const mvc = require("../mvc"); const __1 = require(".."); const nftGenesis_1 = require("./contract-factory/nftGenesis"); const nftSell_1 = require("./contract-factory/nftSell"); const transactionHelpers_1 = require("../helpers/transactionHelpers"); const nft_1 = require("./contract-factory/nft"); const scryptlib_1 = require("../scryptlib"); const mvc_1 = require("../mvc"); const TokenUtil = require("../common/tokenUtil"); const nftProto = require("./contract-proto/nft.proto"); const nftSellProto = require("./contract-proto/nftSell.proto"); const Signature = mvc.crypto.Signature; const contractUtil_1 = require("./contractUtil"); const utils_1 = require("../common/utils"); const Prevouts_1 = require("../common/Prevouts"); const error_1 = require("../common/error"); const SizeTransaction_1 = require("../common/SizeTransaction"); const protoheader_1 = require("../common/protoheader"); const contractHelpers_1 = require("../helpers/contractHelpers"); const proofHelpers_1 = require("../helpers/proofHelpers"); const constants_1 = require("../mcp02/constants"); const nftUnlockContractCheck_1 = require("./contract-factory/nftUnlockContractCheck"); const dummy_1 = require("../common/dummy"); contractUtil_1.ContractUtil.init(); const jsonDescr = require('./contract-desc/txUtil_desc.json'); const { TxInputProof, TxOutputProof } = (0, scryptlib_1.buildTypeClasses)(jsonDescr); class NftManager { constructor({ purse, network = __1.API_NET.MAIN, apiTarget = __1.API_TARGET.MVC, apiHost, feeb = constants_1.FEEB, debug = false, }) { this.dustCalculator = new DustCalculator_1.DustCalculator(mvc_1.Transaction.DUST_AMOUNT, null); this.network = network; this._api = new __1.Api(network, apiTarget, apiHost); this.unlockContractCodeHashArray = contractUtil_1.ContractUtil.unlockContractCodeHashArray; if (feeb) this.feeb = feeb; this.debug = debug; if (purse) { const privateKey = mvc.PrivateKey.fromWIF(purse); const address = privateKey.toAddress(this.network); this.purse = { privateKey, address, }; } } get api() { return this._api; } get sensibleApi() { return this._api; } /** * Estimate the cost of genesis * The minimum cost required in the case of 10 utxo inputs * @param opreturnData * @param utxoMaxCount Maximum number of BSV UTXOs supported * @returns */ getGenesisEstimateFee({ opreturnData, utxoMaxCount = 10, }) { return __awaiter(this, void 0, void 0, function* () { let p2pkhInputNum = utxoMaxCount; let stx = new SizeTransaction_1.SizeTransaction(this.feeb, this.dustCalculator); for (let i = 0; i < p2pkhInputNum; i++) { stx.addP2PKHInput(); } stx.addOutput(nftGenesis_1.NftGenesisFactory.getLockingScriptSize()); if (opreturnData) { stx.addOpReturnOutput(mvc.Script.buildSafeDataOut(opreturnData).toBuffer().length); } stx.addP2PKHOutput(); return stx.getFee(); }); } getIssueEstimateFee({ sensibleId, opreturnData, utxoMaxCount = 10, }) { return __awaiter(this, void 0, void 0, function* () { const { genesisUtxo } = (yield (0, transactionHelpers_1.getLatestGenesisInfo)({ sensibleId, api: this.api, address: this.purse.address, type: 'nft', })); return yield this._calIssueEstimateFee({ genesisUtxoSatoshis: genesisUtxo.satoshis, opreturnData, utxoMaxCount, }); }); } getTransferEstimateFee({ tokenIndex, codehash, genesis, opreturnData, utxoMaxCount = 10, }) { return __awaiter(this, void 0, void 0, function* () { let { nftUtxo } = yield (0, transactionHelpers_1.getNftInfo)({ tokenIndex, codehash, genesis, api: this.api, network: this.network, }); nftUtxo = yield this.pretreatNftUtxo(nftUtxo, codehash, genesis); const genesisScript = new scryptlib_1.Bytes(nftUtxo.preLockingScript.toHex()); return yield this._calTransferEstimateFee({ nftUtxoSatoshis: nftUtxo.satoshis, genesisScript, opreturnData, utxoMaxCount, }); }); } genesis({ genesisWif, totalSupply, opreturnData, utxos: utxosInput, changeAddress, noBroadcast = false, calcFee = false, }) { return __awaiter(this, void 0, void 0, function* () { const startTime = Date.now(); if (calcFee) { return { fee: yield this.getGenesisEstimateFee({ opreturnData }), feeb: this.feeb, }; } const { utxos, utxoPrivateKeys } = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput); if (changeAddress) { changeAddress = new mvc.Address(changeAddress, this.network); } else { changeAddress = utxos[0].address; } const { txComposer, genesisContract } = yield this.createGenesisTx({ totalSupply, utxos, utxoPrivateKeys, opreturnData, changeAddress, }); if (calcFee) { // const unlockSize = // txComposer.tx.inputs.filter((v) => v.output.script.isPublicKeyHashOut()).length * // P2PKH_UNLOCK_SIZE // let fee = Math.ceil( // (txComposer.tx.toBuffer().length + unlockSize + mvc.Transaction.CHANGE_OUTPUT_MAX_SIZE) * // this.feeb // ) let fee = Math.ceil(txComposer.tx._estimateSize() * this.feeb); return { fee, feeb: this.feeb }; } let txHex = txComposer.getRawHex(); let txid; if (!noBroadcast) { txid = yield this.api.broadcast(txHex); } let { codehash, genesis, sensibleId } = (0, contractHelpers_1.getGenesisIdentifiers)({ genesisTx: txComposer.getTx(), purse: this.purse, unlockContractCodeHashArray: this.unlockContractCodeHashArray, type: 'nft', }); const runtime = Date.now() - startTime; return { codehash, genesis, sensibleId, tx: txComposer.tx, txid: txComposer.tx.id, txHex, genesisContract, broadcastStatus: noBroadcast ? 'pending' : txid ? 'success' : 'fail', runtime, }; }); } createGenesisTx({ totalSupply, utxos, utxoPrivateKeys, opreturnData, changeAddress, }) { return __awaiter(this, void 0, void 0, function* () { const txComposer = new tx_composer_1.TxComposer(); // 构建合约 const genesisContract = (0, contractHelpers_1.createNftGenesisContract)({ totalSupply, address: this.purse.address }); // 添加付钱输入、添加创世输出、添加找零输出、解锁输入 const p2pkhInputIndexes = (0, transactionHelpers_1.addP2PKHInputs)(txComposer, utxos); (0, transactionHelpers_1.addContractOutput)({ txComposer, contract: genesisContract, dustCalculator: this.dustCalculator, }); // 添加opreturn输出 if (opreturnData) { (0, transactionHelpers_1.addOpreturnOutput)(txComposer, opreturnData); } (0, transactionHelpers_1.addChangeOutput)(txComposer, changeAddress, this.feeb); (0, transactionHelpers_1.unlockP2PKHInputs)(txComposer, p2pkhInputIndexes, utxoPrivateKeys); // 检查最终费率 (0, transactionHelpers_1.checkFeeRate)(txComposer, this.feeb); return { txComposer, genesisContract }; }); } issue(options) { return __awaiter(this, void 0, void 0, function* () { return this.mint(options); }); } mint({ sensibleId, metaTxId, metaOutputIndex, opreturnData, utxos: utxosInput, receiverAddress, changeAddress, noBroadcast = false, calcFee = false, }) { return __awaiter(this, void 0, void 0, function* () { const startTime = Date.now(); if (calcFee) { return { fee: yield this.getIssueEstimateFee({ sensibleId, opreturnData }), feeb: this.feeb, }; } const { utxos, utxoPrivateKeys } = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput); const genesisPrivateKey = this.purse.privateKey; const genesisPublicKey = genesisPrivateKey.toPublicKey(); if (receiverAddress) { receiverAddress = new mvc.Address(receiverAddress, this.network); } else { receiverAddress = this.purse.address; } if (changeAddress) { changeAddress = new mvc.Address(changeAddress, this.network); } else { changeAddress = utxos[0].address; } if (calcFee) { return yield this.createMintTx({ utxos, utxoPrivateKeys, sensibleId, metaTxId, metaOutputIndex, opreturnData, receiverAddress, changeAddress, calcFee, }); } const { txComposer, tokenIndex } = yield this.createMintTx({ utxos, utxoPrivateKeys, sensibleId, metaTxId, metaOutputIndex, opreturnData, receiverAddress, changeAddress, }); let txHex = txComposer.getRawHex(); if (!noBroadcast) { const res = yield this.api.broadcast(txHex); } const runtime = Date.now() - startTime; return { txHex, txid: txComposer.getTxId(), tx: txComposer.getTx(), tokenIndex, runtime }; }); } transfer({ genesis, codehash, tokenIndex, senderWif, receiverAddress, opreturnData, utxos: utxosInput, noBroadcast = false, }) { return __awaiter(this, void 0, void 0, function* () { const startTime = Date.now(); const { utxos, utxoPrivateKeys } = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput); receiverAddress = new mvc.Address(receiverAddress, this.network); const { txComposer } = yield this.createTransferTx({ utxos, utxoPrivateKeys, genesis, codehash, tokenIndex, receiverAddress, opreturnData, }); let txHex = txComposer.getRawHex(); if (!noBroadcast) { yield this.api.broadcast(txHex); } const runtime = Date.now() - startTime; return { txHex, txid: txComposer.getTxId(), tx: txComposer.getTx(), runtime }; }); } sell({ genesis, codehash, tokenIndex, sellerWif, price, changeAddress, opreturnData, utxos: utxosInput, noBroadcast = false, middleChangeAddress, middleWif, }) { return __awaiter(this, void 0, void 0, function* () { const startTime = Date.now(); // checkParamGenesis(genesis) // checkParamCodehash(codehash) // 检查售价:不能低于22000聪 if (price < 22000) { throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, 'Selling Price must be greater than or equals to 22000 satoshis. 销售价格最低为22000聪。'); } // 准备钱💰;utxo不能超过3个 const { utxos, utxoPrivateKeys } = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput); if (utxos.length > 3) { throw new error_1.CodeError(error_1.ErrCode.EC_UTXOS_MORE_THAN_3, '销售合约使用的utxo数量应当少于等于3个,请先归集utxo。MVC utxos should be no more than 3 in this operation, please merge it first.'); } // 检查此NFT是否属于卖家 const sellerPrivateKey = new mvc.PrivateKey(sellerWif); const sellerPublicKey = sellerPrivateKey.publicKey; let { nftUtxo } = yield (0, transactionHelpers_1.getNftInfo)({ tokenIndex, codehash, genesis, api: this.api, network: this.network, }); if (nftUtxo.nftAddress.toString() != sellerPublicKey.toAddress(this.network).toString()) { throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, 'nft销售者应当为nft持有者!nft seller should be the nft owner!'); } // 准备找零地址 if (changeAddress) { changeAddress = new mvc.Address(changeAddress, this.network); } else { changeAddress = utxos[0].address; } // 准备中间找零地址 let middlePrivateKey; if (middleChangeAddress) { middleChangeAddress = new mvc.Address(middleChangeAddress, this.network); middlePrivateKey = new mvc.PrivateKey(middleWif); } else { middleChangeAddress = utxos[0].address; middlePrivateKey = utxoPrivateKeys[0]; } const { sellTxComposer, txComposer } = yield this.createSellTx({ utxos, utxoPrivateKeys, genesis, codehash, tokenIndex, nftUtxo, price, opreturnData, changeAddress, middlePrivateKey, middleChangeAddress, }); let nftSellTxHex = sellTxComposer.getRawHex(); let txHex = txComposer.getRawHex(); if (!noBroadcast) { yield this.api.broadcast(nftSellTxHex); yield this.api.broadcast(txHex); } const runtime = Date.now() - startTime; return { tx: txComposer.tx, txHex, txid: txComposer.tx.id, sellTxId: sellTxComposer.getTxId(), sellTx: sellTxComposer.getTx(), sellTxHex: nftSellTxHex, runtime, }; }); } cancelSell({ genesis, codehash, tokenIndex, sellerWif, sellUtxo, opreturnData, utxos: utxosInput, changeAddress, noBroadcast = false, middleChangeAddress, middlePrivateKey, }) { return __awaiter(this, void 0, void 0, function* () { const startTime = Date.now(); // checkParamGenesis(genesis) // checkParamCodehash(codehash) const sellerPrivateKey = new mvc.PrivateKey(sellerWif); // 准备钱💰;utxo不能超过3个 const { utxos, utxoPrivateKeys } = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput); if (utxos.length > 3) { throw new error_1.CodeError(error_1.ErrCode.EC_UTXOS_MORE_THAN_3, '下架合约使用的utxo数量应当少于等于3个,请先归集utxo。MVC utxos should be no more than 3 in this operation, please merge it first.'); } // 准备找零地址 if (changeAddress) { changeAddress = new mvc.Address(changeAddress, this.network); } else { changeAddress = utxos[0].address; } // 准备中间找零地址 if (middleChangeAddress) { middleChangeAddress = new mvc.Address(middleChangeAddress, this.network); middlePrivateKey = new mvc.PrivateKey(middlePrivateKey); } else { middleChangeAddress = utxos[0].address; middlePrivateKey = utxoPrivateKeys[0]; } const { unlockCheckTxComposer, txComposer } = yield this.createCancelSellTx({ utxos, utxoPrivateKeys, genesis, codehash, tokenIndex, sellUtxo, sellerPrivateKey, opreturnData, changeAddress, middlePrivateKey, middleChangeAddress, }); let unlockCheckTxHex = unlockCheckTxComposer.getRawHex(); let txHex = txComposer.getRawHex(); if (!noBroadcast) { yield this.api.broadcast(unlockCheckTxHex); yield this.api.broadcast(txHex); } const runtime = Date.now() - startTime; return { tx: txComposer.tx, txHex, txid: txComposer.tx.id, unlockCheckTxId: unlockCheckTxComposer.getTxId(), unlockCheckTx: unlockCheckTxComposer.getTx(), unlockCheckTxHex: unlockCheckTxHex, runtime, }; }); } createCancelSellTx({ utxos, utxoPrivateKeys, genesis, codehash, tokenIndex, sellUtxo, sellerPrivateKey, opreturnData, changeAddress, middlePrivateKey, middleChangeAddress, }) { return __awaiter(this, void 0, void 0, function* () { // 第一步:找回并准备NFT Utxo // 1.1 找回nft Utxo let { nftUtxo } = yield (0, transactionHelpers_1.getNftInfo)({ tokenIndex, codehash, genesis, api: this.api, network: this.network, }); // 1.2 验证nft Utxo nftUtxo = yield this.pretreatNftUtxo(nftUtxo, codehash, genesis); // 第二步:找到并重建销售utxo // 2.1 查找销售utxo if (!sellUtxo) { sellUtxo = yield this.api.getNftSellUtxo(codehash, genesis, tokenIndex); } if (!sellUtxo) { throw new error_1.CodeError(error_1.ErrCode.EC_NFT_NOT_ON_SELL, 'The NFT is not for sale because the corresponding SellUtxo cannot be found.'); } // 2.2 重建销售utxo let nftAddress = sellerPrivateKey.toAddress(this.network); let nftSellTxHex = yield this.api.getRawTxData(sellUtxo.txId); let nftSellTx = new mvc.Transaction(nftSellTxHex); let nftSellUtxo = { txId: sellUtxo.txId, outputIndex: sellUtxo.outputIndex, satoshis: nftSellTx.outputs[sellUtxo.outputIndex].satoshis, lockingScript: nftSellTx.outputs[sellUtxo.outputIndex].script, }; // 第三步:确保余额充足(需要构造三个交易) // let genesisScript = nftUtxo.preNftAddress.hashBuffer.equals(Buffer.alloc(20, 0)) // ? new Bytes(nftUtxo.preLockingScript.toHex()) // : new Bytes('') let genesisScript = new scryptlib_1.Bytes(nftUtxo.preLockingScript.toHex()); let balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0); let estCancelSellFee = yield this._calCancelSellEstimateFee({ codehash, nftUtxoSatoshis: nftUtxo.satoshis, nftSellUtxo, genesisScript, utxoMaxCount: utxos.length, opreturnData, }); if (balance < estCancelSellFee) { throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_BSV, `Insufficient balance.It take more than ${estCancelSellFee}, but only ${balance}.`); } // 第四步:构建解锁交易 // 4.1 准备nft解锁数据 let nftInput = nftUtxo; let nftID = nftProto.getNftID(nftInput.lockingScript.toBuffer()); let unlockContract = nftUnlockContractCheck_1.NftUnlockContractCheckFactory.createContract(nftUnlockContractCheck_1.NFT_UNLOCK_CONTRACT_TYPE.OUT_6); unlockContract.setFormatedDataPart({ nftCodeHash: Buffer.from(codehash, 'hex'), nftID, }); // 解锁合约交易构建器 const unlockCheckTxComposer = new tx_composer_1.TxComposer(); // 4.2 往解锁合约交易中塞钱💰 const unlockCheck_p2pkhInputIndexes = (0, transactionHelpers_1.addP2PKHInputs)(unlockCheckTxComposer, utxos); // 4.3 往解锁合约交易中添加解锁输出(重要) const unlockCheckOutputIndex = (0, transactionHelpers_1.addContractOutput)({ txComposer: unlockCheckTxComposer, lockingScript: unlockContract.lockingScript, dustCalculator: this.dustCalculator, }); // 4.4 解锁交易找零 let changeOutputIndex = (0, transactionHelpers_1.addChangeOutput)(unlockCheckTxComposer, middleChangeAddress, this.feeb); (0, transactionHelpers_1.unlockP2PKHInputs)(unlockCheckTxComposer, unlockCheck_p2pkhInputIndexes, utxoPrivateKeys); // 4.5 检查费率 (0, transactionHelpers_1.checkFeeRate)(unlockCheckTxComposer, this.feeb); // 4.6 重新集结此次操作后的钱 utxos = [ { txId: unlockCheckTxComposer.getTxId(), satoshis: unlockCheckTxComposer.getOutput(changeOutputIndex).satoshis, outputIndex: changeOutputIndex, address: middleChangeAddress, }, ]; utxoPrivateKeys = utxos.map((v) => middlePrivateKey).filter((v) => v); // 4.7 构建解锁交易的Utxo let unlockCheckUtxo = { txId: unlockCheckTxComposer.getTxId(), outputIndex: unlockCheckOutputIndex, satoshis: unlockCheckTxComposer.getOutput(unlockCheckOutputIndex).satoshis, lockingScript: unlockCheckTxComposer.getOutput(unlockCheckOutputIndex).script, }; // 第五步:构建NFT转移交易 // 输入:1.销售 2.nft 3.钱 4.解锁合约 // 输出:1.nft 2.opreturn 3.找零 (相比于buy,没有发给销售者的所得) // 转移合约交易构建器 const txComposer = new tx_composer_1.TxComposer(); let prevouts = new Prevouts_1.Prevouts(); // 5.1 放入销售输入 const sellInputIndex = txComposer.appendInput(nftSellUtxo); prevouts.addVout(nftSellUtxo.txId, nftSellUtxo.outputIndex); // 5.2 放入NFT输入 const nftInputIndex = txComposer.appendInput(nftInput); prevouts.addVout(nftInput.txId, nftInput.outputIndex); // 5.3 放入钱输入 const p2pkhInputIndexes = (0, transactionHelpers_1.addP2PKHInputs)(txComposer, utxos); utxos.forEach((utxo) => { prevouts.addVout(utxo.txId, utxo.outputIndex); }); // 5.4 放入解锁合约输入 const unlockCheckInputIndex = txComposer.appendInput(unlockCheckUtxo); prevouts.addVout(unlockCheckUtxo.txId, unlockCheckUtxo.outputIndex); // 5.5 重建销售合约 let nftSellContract = nftSell_1.NftSellFactory.createContract(new scryptlib_1.Ripemd160((0, scryptlib_1.toHex)(new mvc.Address(sellUtxo.sellerAddress, this.network).hashBuffer)), sellUtxo.price, new scryptlib_1.Bytes(codehash), new scryptlib_1.Bytes((0, scryptlib_1.toHex)(nftID))); nftSellContract.setFormatedDataPart(nftSellProto.parseDataPart(nftSellUtxo.lockingScript.toBuffer())); // 5.6 不存在的啦(不用打销售款) // 5.7 添加nft输出 // 5.7.1 构造nft脚本(将nft的所有权转移给销售者) const lockingScriptBuf = (0, contractHelpers_1.rebuildNftLockingScript)(nftInput, nftAddress); // 5.7.2 添加进输出 const nftOutputIndex = (0, transactionHelpers_1.addContractOutput)({ txComposer, lockingScript: mvc.Script.fromBuffer(lockingScriptBuf), dustCalculator: this.dustCalculator, }); // 5.8 添加opreturn输出 let opreturnScriptHex = ''; if (opreturnData) { const opreturnOutputIndex = txComposer.appendOpReturnOutput(opreturnData); opreturnScriptHex = txComposer.getOutput(opreturnOutputIndex).script.toHex(); } // 5.9 解锁nft合约,并找零 for (let c = 0; c < 2; c++) { /** 5.9.1 解锁NFT合约 */ txComposer.clearChangeOutput(); const changeOutputIndex = txComposer.appendChangeOutput(changeAddress, this.feeb); const nftContract = nft_1.NftFactory.createContract(this.unlockContractCodeHashArray, codehash); let dataPartObj = nftProto.parseDataPart(nftUtxo.lockingScript.toBuffer()); nftContract.setFormatedDataPart(dataPartObj); // 准备数据 const prevNftInputIndex = nftUtxo.satotxInfo.preNftInputIndex; const nftTx = new mvc.Transaction(nftUtxo.satotxInfo.txHex); const inputRes = TokenUtil.getTxInputProof(nftTx, prevNftInputIndex); const nftTxInputProof = new TxInputProof(inputRes[0]); const nftTxHeader = inputRes[1]; const prevNftTxProof = new TxOutputProof(TokenUtil.getTxOutputProof(nftUtxo.satotxInfo.preTx, nftUtxo.satotxInfo.preOutputIndex)); // 重要:解锁相关参数 const contractInputIndex = sellInputIndex; const contractTxProof = new TxOutputProof(TokenUtil.getTxOutputProof(nftSellTx, nftSellUtxo.outputIndex)); const amountCheckHashIndex = 1; // 对应out_6 const amountCheckInputIndex = unlockCheckInputIndex; const unlockCheckTx = unlockCheckTxComposer.getTx(); const amountCheckTxProof = new TxOutputProof(TokenUtil.getTxOutputProof(unlockCheckTx, unlockCheckOutputIndex)); const amountCheckScriptBuf = unlockCheckTx.outputs[unlockCheckOutputIndex].script.toBuffer(); const amountCheckScrypt = new scryptlib_1.Bytes(amountCheckScriptBuf.toString('hex')); const unlockingContract = nftContract.unlock({ txPreimage: txComposer.getInputPreimage(nftInputIndex), prevouts: new scryptlib_1.Bytes(prevouts.toHex()), prevNftInputIndex, prevNftAddress: new scryptlib_1.Bytes((0, scryptlib_1.toHex)(nftUtxo.preNftAddress.hashBuffer)), nftTxHeader, nftTxInputProof, prevNftTxProof, genesisScript, contractInputIndex, contractTxProof, amountCheckHashIndex, amountCheckInputIndex, amountCheckTxProof, amountCheckScrypt, operation: nftProto.NFT_OP_TYPE.UNLOCK_FROM_CONTRACT, }); if (this.debug) { let txContext = { tx: txComposer.tx, inputIndex: nftInputIndex, inputSatoshis: txComposer.getInput(nftInputIndex).output.satoshis, }; let ret = unlockingContract.verify(txContext); if (ret.success == false) throw ret; } txComposer.getInput(nftInputIndex).setScript(unlockingContract.toScript()); /** 5.9.1.5 其他输出 */ let otherOutputs = Buffer.alloc(0); txComposer.tx.outputs.forEach((output, index) => { if (index != nftOutputIndex) { let outputBuf = output.toBufferWriter().toBuffer(); let lenBuf = Buffer.alloc(4); lenBuf.writeUInt32LE(outputBuf.length); otherOutputs = Buffer.concat([otherOutputs, lenBuf, outputBuf]); } }); /** 5.9.2 解锁检查合约 */ const nftOutputProof = (0, proofHelpers_1.createTxOutputProof)(nftTx, nftUtxo.satotxInfo.outputIndex); let sub = unlockCheckUtxo.lockingScript; sub = sub.subScript(0); const txPreimage = new scryptlib_1.SigHashPreimage((0, scryptlib_1.toHex)((0, scryptlib_1.getPreimage)(txComposer.getTx(), sub, unlockCheckUtxo.satoshis, unlockCheckInputIndex))); let unlockCall = unlockContract.unlock({ txPreimage, prevouts: new scryptlib_1.Bytes(prevouts.toHex()), nftInputIndex, nftScript: new scryptlib_1.Bytes(nftInput.lockingScript.toHex()), nftTxHeader: nftOutputProof.txHeader, nftTxHashProof: nftOutputProof.hashProof, nftSatoshiBytes: nftOutputProof.satoshiBytes, nOutputs: txComposer.tx.outputs.length, txNftOutputIndex: nftOutputIndex, nftOutputAddress: new scryptlib_1.Bytes((0, scryptlib_1.toHex)(nftAddress.hashBuffer)), nftOutputSatoshis: txComposer.getOutput(nftOutputIndex).satoshis, otherOutputArray: new scryptlib_1.Bytes((0, scryptlib_1.toHex)(otherOutputs)), }); if (this.debug) { let txContext = { tx: txComposer.getTx(), inputIndex: unlockCheckInputIndex, inputSatoshis: txComposer.getInput(unlockCheckInputIndex).output.satoshis, }; let ret = unlockCall.verify(txContext); if (ret.success == false) throw ret; } txComposer.getInput(unlockCheckInputIndex).setScript(unlockCall.toScript()); /** 5.9.3 解锁销售合约 */ let sellUtxo = txComposer.getInput(sellInputIndex).output; let sellSubScript = sellUtxo.script; sellSubScript = sellSubScript.subScript(0); const sellTxPreimage = new scryptlib_1.SigHashPreimage((0, scryptlib_1.toHex)((0, scryptlib_1.getPreimage)(txComposer.getTx(), sellSubScript, sellUtxo.satoshis, sellInputIndex, Signature.SIGHASH_SINGLE | Signature.SIGHASH_FORKID))); const unlockCall2 = nftSellContract.unlock({ txPreimage: sellTxPreimage, // 以下4个参数只有在cancelSell中才有 nftScript: new scryptlib_1.Bytes(nftInput.lockingScript.toHex()), senderPubKey: new scryptlib_1.PubKey((0, scryptlib_1.toHex)(sellerPrivateKey.publicKey.toBuffer())), senderSig: new scryptlib_1.Sig((0, scryptlib_1.toHex)(txComposer.getTxFormatSig(sellerPrivateKey, sellInputIndex))), nftOutputSatoshis: txComposer.getOutput(nftOutputIndex).satoshis, op: nftSell_1.NFT_SELL_OP.CANCEL, }); if (this.debug) { let txContext = { tx: txComposer.getTx(), inputIndex: sellInputIndex, inputSatoshis: txComposer.getInput(sellInputIndex).output.satoshis, }; let ret = unlockCall2.verify(txContext); if (ret.success == false) throw ret; } txComposer.getInput(sellInputIndex).setScript(unlockCall2.toScript()); } // 6. 解锁输入,检查费率 (0, transactionHelpers_1.unlockP2PKHInputs)(txComposer, p2pkhInputIndexes, utxoPrivateKeys); (0, transactionHelpers_1.checkFeeRate)(txComposer, this.feeb); return { unlockCheckTxComposer, txComposer }; }); } buy({ genesis, codehash, tokenIndex, buyerWif, sellUtxo, opreturnData, utxos: utxosInput, changeAddress, noBroadcast = false, middleChangeAddress, middleWif, publisherAddress, publisherFee, publisherFeeRate, creatorAddress, creatorFee, creatorFeeRate, }) { return __awaiter(this, void 0, void 0, function* () { const startTime = Date.now(); // checkParamGenesis(genesis) // checkParamCodehash(codehash) // 准备钱💰 const { utxos, utxoPrivateKeys } = yield (0, transactionHelpers_1.prepareUtxos)(this.purse, this.api, this.network, utxosInput); if (utxos.length > 3) { throw new error_1.CodeError(error_1.ErrCode.EC_UTXOS_MORE_THAN_3, 'MVC utxos should be no more than 3 in this operation, please merge it first.'); } const buyerPrivateKey = new mvc.PrivateKey(buyerWif); // 准备找零地址 if (changeAddress) { changeAddress = new mvc.Address(changeAddress, this.network); } else { changeAddress = utxos[0].address; } // 准备中间找零地址 let middlePrivateKey; if (middleChangeAddress) { middleChangeAddress = new mvc.Address(middleChangeAddress, this.network); middlePrivateKey = new mvc.PrivateKey(middleWif); } else { middleChangeAddress = utxos[0].address; middlePrivateKey = utxoPrivateKeys[0]; } // 查找销售utxo if (!sellUtxo) { sellUtxo = yield this.api.getNftSellUtxo(codehash, genesis, tokenIndex); } if (!sellUtxo) { throw new error_1.CodeError(error_1.ErrCode.EC_NFT_NOT_ON_SELL, 'The NFT is not for sale because the corresponding SellUtxo cannot be found.'); } const price = sellUtxo.price; // 检查发行者和创作者的地址和费率参数 this._checkRoyaltyParams({ price, publisherAddress, publisherFee, publisherFeeRate, creatorAddress, creatorFee, creatorFeeRate, }); let { unlockCheckTxComposer, txComposer } = yield this.createBuyTx({ utxos, utxoPrivateKeys, genesis, codehash, tokenIndex, sellUtxo, buyerPrivateKey: buyerPrivateKey, opreturnData, changeAddress, middlePrivateKey, middleChangeAddress, publisherAddress, publisherFee, publisherFeeRate, creatorAddress, creatorFee, creatorFeeRate, }); let unlockCheckTxHex = unlockCheckTxComposer.getRawHex(); let txHex = txComposer.getRawHex(); if (!noBroadcast) { yield this.api.broadcast(unlockCheckTxHex); yield this.api.broadcast(txHex); } const runtime = Date.now() - startTime; return { tx: txComposer.tx, txHex, txid: txComposer.tx.id, unlockCheckTxId: unlockCheckTxComposer.getTxId(), unlockCheckTx: unlockCheckTxComposer.getTx(), unlockCheckTxHex: unlockCheckTxHex, runtime, }; }); } createBuyTx({ utxos, utxoPrivateKeys, genesis, codehash, tokenIndex, sellUtxo, buyerPrivateKey, opreturnData, changeAddress, middlePrivateKey, middleChangeAddress, publisherAddress, publisherFee, publisherFeeRate, creatorAddress, creatorFee, creatorFeeRate, }) { return __awaiter(this, void 0, void 0, function* () { // 第一步:找回并准备NFT Utxo // 1.1 找回nft Utxo let { nftUtxo } = yield (0, transactionHelpers_1.getNftInfo)({ tokenIndex, codehash, genesis, api: this.api, network: this.network, }); // 1.2 验证nft Utxo nftUtxo = yield this.pretreatNftUtxo(nftUtxo, codehash, genesis); // 第二步:找到并重建销售utxo // 2.1 查找销售utxo的步骤在上面已经完成(为了拿到价格,进行版税费用检查) // 2.2 重建销售utxo let nftSellTxHex = yield this.api.getRawTxData(sellUtxo.txId); let nftSellTx = new mvc.Transaction(nftSellTxHex); let nftSellUtxo = { txId: sellUtxo.txId, outputIndex: sellUtxo.outputIndex, satoshis: nftSellTx.outputs[sellUtxo.outputIndex].satoshis, lockingScript: nftSellTx.outputs[sellUtxo.outputIndex].script, }; // 第三步:确保余额充足(需要构造三个交易) const genesisScript = new scryptlib_1.Bytes(nftUtxo.preLockingScript.toHex()); let balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0); let estBuyFee = yield this._calBuyEstimateFee({ codehash, nftUtxoSatoshis: nftUtxo.satoshis, nftSellUtxo, sellUtxo, genesisScript, utxoMaxCount: utxos.length, opreturnData, }); if (balance < estBuyFee) { throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_BSV, `Insufficient balance.It take more than ${estBuyFee}, but only ${balance}.`); } // 第四步:构建解锁交易 // 4.1 准备nft解锁数据 let nftInput = nftUtxo; let nftID = nftProto.getNftID(nftInput.lockingScript.toBuffer()); let unlockContract = nftUnlockContractCheck_1.NftUnlockContractCheckFactory.createContract(nftUnlockContractCheck_1.NFT_UNLOCK_CONTRACT_TYPE.OUT_6); unlockContract.setFormatedDataPart({ nftCodeHash: Buffer.from(codehash, 'hex'), nftID, }); // 解锁合约交易构建器 const unlockCheckTxComposer = new tx_composer_1.TxComposer(); // 4.2 往解锁合约交易中塞钱💰 const unlockCheck_p2pkhInputIndexes = (0, transactionHelpers_1.addP2PKHInputs)(unlockCheckTxComposer, utxos); // 4.3 往解锁合约交易中添加解锁输出(重要) const unlockCheckOutputIndex = (0, transactionHelpers_1.addContractOutput)({ txComposer: unlockCheckTxComposer, lockingScript: unlockContract.lockingScript, dustCalculator: this.dustCalculator, }); // 4.4 解锁交易找零 let changeOutputIndex = (0, transactionHelpers_1.addChangeOutput)(unlockCheckTxComposer, middleChangeAddress, this.feeb); (0, transactionHelpers_1.unlockP2PKHInputs)(unlockCheckTxComposer, unlockCheck_p2pkhInputIndexes, utxoPrivateKeys); // 4.5 检查费率 (0, transactionHelpers_1.checkFeeRate)(unlockCheckTxComposer, this.feeb); // 4.6 重新集结此次操作后的钱 utxos = [ { txId: unlockCheckTxComposer.getTxId(), satoshis: unlockCheckTxComposer.getOutput(changeOutputIndex).satoshis, outputIndex: changeOutputIndex, address: middleChangeAddress, }, ]; utxoPrivateKeys = utxos.map((v) => middlePrivateKey).filter((v) => v); // 4.7 构建解锁交易的Utxo let unlockCheckUtxo = { txId: unlockCheckTxComposer.getTxId(), outputIndex: unlockCheckOutputIndex, satoshis: unlockCheckTxComposer.getOutput(unlockCheckOutputIndex).satoshis, lockingScript: unlockCheckTxComposer.getOutput(unlockCheckOutputIndex).script, }; // 第五步:构建NFT转移交易 // 输入:1.销售 2.nft 3.钱 4.解锁合约 // 输出:1.销售者所得 (1.5 版税:发行者、创作者) 2.nft 3.opreturn 4.找零 // 转移合约交易构建器 const txComposer = new tx_composer_1.TxComposer(); let prevouts = new Prevouts_1.Prevouts(); // 5.1 放入销售输入 const sellInputIndex = txComposer.appendInput(nftSellUtxo); prevouts.addVout(nftSellUtxo.txId, nftSellUtxo.outputIndex); // 5.2 放入NFT输入 const nftInputIndex = txComposer.appendInput(nftInput); prevouts.addVout(nftInput.txId, nftInput.outputIndex); // 5.3 放入钱输入 const p2pkhInputIndexes = (0, transactionHelpers_1.addP2PKHInputs)(txComposer, utxos); utxos.forEach((utxo) => { prevouts.addVout(utxo.txId, utxo.outputIndex); }); // 5.4 放入解锁合约输入 const unlockCheckInputIndex = txComposer.appendInput(unlockCheckUtxo); prevouts.addVout(unlockCheckUtxo.txId, unlockCheckUtxo.outputIndex); // 5.5 重建销售合约 let nftSellContract = nftSell_1.NftSellFactory.createContract(new scryptlib_1.Ripemd160((0, scryptlib_1.toHex)(new mvc.Address(sellUtxo.sellerAddress, this.network).hashBuffer)), sellUtxo.price, new scryptlib_1.Bytes(codehash), new scryptlib_1.Bytes((0, scryptlib_1.toHex)(nftID))); const parsed = nftSellProto.parseDataPart(nftSellUtxo.lockingScript.toBuffer()); nftSellContract.setFormatedDataPart(parsed); // 5.6 取得销售者地址,将销售所得构建输出 const sellerAddress = mvc.Address.fromPublicKeyHash(Buffer.from(nftSellContract.constuctParams.senderAddress.value, 'hex'), this.network); const sellerSatoshis = nftSellContract.constuctParams.bsvRecAmount; txComposer.appendP2PKHOutput({ address: sellerAddress, satoshis: sellerSatoshis, }); // 5.6.5 版税:发行者、创作者 if (publisherAddress) { // 有发行者地址,则根据费用或费率构建发行者费用输出 const publisherAmount = publisherFee || Math.ceil(sellerSatoshis * publisherFeeRate); txComposer.appendP2PKHOutput({ address: new mvc.Address(publisherAddress, this.network), satoshis: publisherAmount, }); } if (creatorAddress) { // 有创作者地址,则根据费用或费率构建创作者费用输出 const creatorAmount = creatorFee || Math.ceil(sellerSatoshis * creatorFeeRate); txComposer.appendP2PKHOutput({ address: new mvc.Address(creatorAddress, this.network), satoshis: creatorAmount, }); } // 5.7 添加nft输出 // 5.7.1 构造nft脚本(将nft的所有权转移给买家) const buyerAddress = buyerPrivateKey.toAddress(this.network); const lockingScriptBuf = (0, contractHelpers_1.rebuildNftLockingScript)(nftInput, buyerAddress); // 5.7.2 添加进输出 const nftOutputIndex = (0, transactionHelpers_1.addContractOutput)({ txComposer, lockingScript: mvc.Script.fromBuffer(lockingScriptBuf), dustCalculator: this.dustCalculator, }); // 5.8 添加opreturn输出 let opreturnScriptHex = ''; if (opreturnData) { const opreturnOutputIndex = txComposer.appendOpReturnOutput(opreturnData); opreturnScriptHex = txComposer.getOutput(opreturnOutputIndex).script.toHex(); } // 5.9 解锁nft合约,并找零 for (let c = 0; c < 2; c++) { /** 5.9.1 解锁NFT合约 */ txComposer.clearChangeOutput(); const changeOutputIndex = txComposer.appendChangeOutput(changeAddress, this.feeb); const nftContract = nft_1.NftFactory.createContract(this.unlockContractCodeHashArray, codehash); let dataPartObj = nftProto.parseDataPart(nftUtxo.lockingScript.toBuffer()); nftContract.setFormatedDataPart(dataPartObj); // 准备数据 const prevNftInputIndex = nftUtxo.satotxInfo.preNftInputIndex; const nftTx = new mvc.Transaction(nftUtxo.satotxInfo.txHex); const inputRes = TokenUtil.getTxInputProof(nftTx, prevNftInputIndex); const nftTxInputProof = new TxInputProof(inputRes[0]); const nftTxHeader = inputRes[1]; const prevNftTxProof = new TxOutputProof(TokenUtil.getTxOutputProof(nftUtxo.satotxInfo.preTx, nftUtxo.satotxInfo.preOutputIndex)); // 重要:解锁相关参数 const contractInputIndex = sellInputIndex; const contractTxProof = new TxOutputProof(TokenUtil.getTxOutputProof(nftSellTx, nftSellUtxo.outputIndex)); const amountCheckHashIndex = 1; // 对应out_6 const amountCheckInputIndex = unlockCheckInputIndex; const unlockCheckTx = unlockCheckTxComposer.getTx(); const amountCheckTxProof = new TxOutputProof(TokenUtil.getTxOutputProof(unlockCheckTx, unlockCheckOutputIndex)); const amountCheckScriptBuf = unlockCheckTx.outputs[unlockCheckOutputIndex].script.toBuffer(); const amountCheckScrypt = new scryptlib_1.Bytes(amountCheckScriptBuf.toString('hex')); const unlockingContract = nftContract.unlock({ txPreimage: txComposer.getInputPreimage(nftInputIndex), prevouts: new scryptlib_1.Bytes(prevouts.toHex()), prevNftInputIndex, prevNftAddress: new scryptlib_1.Bytes((0, scryptlib_1.toHex)(nftUtxo.preNftAddress.hashBuffer)), nftTxHeader, nftTxInputProof, prevNftTxProof, genesisScript, contractInputIndex, contractTxProof, amountCheckHashIndex, amountCheckInputIndex, amountCheckTxProof, amountCheckScrypt, operation: nftProto.NFT_OP_TYPE.UNLOCK_FROM_CONTRACT, }); if (this.debug) { let txContext = { tx: txComposer.tx, inputIndex: nftInputIndex, inputSatoshis: txComposer.getInput(nftInputIndex).output.satoshis, }; let ret = unlockingContract.verify(txContext); if (ret.success == false) throw ret; } txComposer.getInput(nftInputIndex).setScript(unlockingContract.toScript()); /** 5.9.1.5 其他输出 */ let otherOutputs = Buffer.alloc(0); txComposer.tx.outputs.forEach((output, index) => { if (index != nftOutputIndex) { let outputBuf = output.toBufferWriter().toBuffer(); let lenBuf = Buffer.alloc(4); lenBuf.writeUInt32LE(outputBuf.length); otherOutputs = Buffer.concat([otherOutputs, lenBuf, outputBuf]); } }); /** 5.9.2 解锁检查合约 */ const nftOutputProof = (0, proofHelpers_1.createTxOutputProof)(nftTx, nftUtxo.satotxInfo.outputIndex); let sub = unlockCheckUtxo.lockingScript; sub = sub.subScript(0); const txPreimage = new scryptlib_1.SigHashPreimage((0, scryptlib_1.toHex)((0, scryptlib_1.getPreimage)(txComposer.getTx(), sub, unlockCheckUtxo.satoshis, unlockCheckInputIndex))); let unlockCall = unlockContract.unlock({ // txPreimage: txComposer.getInputPreimage(unlockCheckInputIndex), txPreimage, prevouts: new scryptlib_1.Bytes(prevouts.toHex()), nftInputIndex, nftScript: new scryptlib_1.Bytes(nftInput.lockingScript.toHex()), nftTxHeader: nftOutputProof.txHeader, nftTxHashProof: nftOutputProof.hashProof, nftSatoshiBytes: nftOutputProof.satoshiBytes, nOutputs: txComposer.tx.outputs.length, txNftOutputIndex: nftOutputIndex, nftOutputAddress: new scryptlib_1.Bytes((0, scryptlib_1.toHex)(buyerAddress.hashBuffer)), nftOutputSatoshis: txComposer.getOutput(nftOutputIndex).satoshis, otherOutputArray: new scryptlib_1.Bytes((0, scryptlib_1.toHex)(otherOutputs)), }); if (this.debug) { let txContext = { tx: txComposer.getTx(), inputIndex: unlockCheckInputIndex, inputSatoshis: txComposer.getInput(unlockCheckInputIndex).output.satoshis, }; let ret = unlockCall.verify(txContext);