sensible-sdk
Version:
Sensible-SDK
929 lines (928 loc) • 91.5 kB
JavaScript
"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.SensibleFT = exports.defaultSignerConfigs = exports.sighashType = void 0;
const scryptlib_1 = require("scryptlib");
const BN = require("../bn.js");
const bsv = require("../bsv");
const $ = require("../common/argumentCheck");
const dummy_1 = require("../common/dummy");
const DustCalculator_1 = require("../common/DustCalculator");
const error_1 = require("../common/error");
const Prevouts_1 = require("../common/Prevouts");
const protoheader_1 = require("../common/protoheader");
const SatotxSigner_1 = require("../common/SatotxSigner");
const satotxSignerUtil_1 = require("../common/satotxSignerUtil");
const SizeTransaction_1 = require("../common/SizeTransaction");
const TokenUtil = require("../common/tokenUtil");
const Utils = require("../common/utils");
const utils_1 = require("../common/utils");
const sensible_api_1 = require("../sensible-api");
const tx_composer_1 = require("../tx-composer");
const token_1 = require("./contract-factory/token");
const tokenGenesis_1 = require("./contract-factory/tokenGenesis");
const tokenTransferCheck_1 = require("./contract-factory/tokenTransferCheck");
const ftProto = require("./contract-proto/token.proto");
const contractUtil_1 = require("./contractUtil");
const Signature = bsv.crypto.Signature;
exports.sighashType = Signature.SIGHASH_ALL | Signature.SIGHASH_FORKID;
const PLACE_HOLDER_UNSIGN_TXID = "4444444444444444444444444444444888888888888888888888888888888888";
const PLACE_HOLDER_UNSIGN_TXID_REVERSE = "8888888888888888888888888888888884444444444444444444444444444444";
const PLACE_HOLDER_UNSIGN_PREVOUTS = "4444444444444444444444444444444444444444444444444444444444444444";
const PLACE_HOLDER_UNSIGN_CHECKTX = "PLACE_HOLDER_UNSIGN_CHECKTX";
const _ = bsv.deps._;
exports.defaultSignerConfigs = [
{
satotxApiPrefix: "https://s1.satoplay.cn,https://s1.satoplay.com",
satotxPubKey: "2c8c0117aa5edba9a4539e783b6a1bdbc1ad88ad5b57f3d9c5cba55001c45e1fedb877ebc7d49d1cfa8aa938ccb303c3a37732eb0296fee4a6642b0ff1976817b603404f64c41ec098f8cd908caf64b4a3aada220ff61e252ef6d775079b69451367eda8fdb37bc55c8bfd69610e1f31b9d421ff44e3a0cfa7b11f334374827256a0b91ce80c45ffb798798e7bd6b110134e1a3c3fa89855a19829aab3922f55da92000495737e99e0094e6c4dbcc4e8d8de5459355c21ff055d039a202076e4ca263b745a885ef292eec0b5a5255e6ecc45534897d9572c3ebe97d36626c7b1e775159e00b17d03bc6d127260e13a252afd89bab72e8daf893075f18c1840cb394f18a9817913a9462c6ffc8951bee50a05f38da4c9090a4d6868cb8c955e5efb4f3be4e7cf0be1c399d78a6f6dd26a0af8492dca67843c6da9915bae571aa9f4696418ab1520dd50dd05f5c0c7a51d2843bd4d9b6b3b79910e98f3d98099fd86d71b2fac290e32bdacb31943a8384a7668c32a66be127b74390b4b0dec6455",
},
{
satotxApiPrefix: "https://satotx.showpay.top,https://cnsatotx.showpay.top",
satotxPubKey: "5b94858991d384c61ffd97174e895fcd4f62e4fea618916dc095fe4c149bbdf1188c9b33bc15cbe963a63b2522e70b80a5b722ac0e6180407917403755df4de27d69cc115c683a99face8c823cbccf73c7f0d546f1300b9ee2e96aea85542527f33b649f1885caebe19cf75d9a645807f03565c65bd4c99c8f6bb000644cfb56969eac3e9331c254b08aa279ceb64c47ef66be3f071e28b3a5a21e48cdfc3335d8b52e80a09a104a791ace6a2c1b4da88c52f9cc28c54a324e126ec91a988c1fe4e21afc8a84d0e876e01502386f74e7fc24fc32aa249075dd222361aea119d4824db2a797d58886e93bdd60556e504bb190b76a451a4e7b0431973c0410e71e808d0962415503931bbde3dfce5186b371c5bf729861f239ef626b7217d071dfd62bac877a847f2ac2dca07597a0bb9dc1969bed40606c025c4ff7b53a4a6bd921642199c16ede8165ed28da161739fa8d33f9f483212759498c1219d246092d14c9ae63808f58f03c8ca746904ba51fa326d793cea80cda411c85d35894bdb5",
},
{
satotxApiPrefix: "https://satotx.volt.id",
satotxPubKey: "3a62ce90c189ae322150cfc68cd00739cd681babf46a9b27793413ad780ea7c4ef22afd0042bc3711588587c2b8a953ced78496cb95579b1272b8979183ea3c66d204c8eeffebfa115c596c0c561f3569fe6d6e8e06d7e82192a24a84b739838ac846db8594a565679d617695f184eb85a3902a036eb8e82f95b83acc207f0deeac87291539865765899d97cfe41169c555480372195729269ae30b6c39324a6731d6f4e46da5ba1789c6e9bd14b16426d35fd4449eecd177e2834e87fb65d9d469176ffe0c12097fcc7e2393dbaa504631487a3ad725235b4d25fe3d09c2460f8a6c0bf4defc1ffe65d5fa28e85fae11eace2a66e48a0ae2ed6bcfb4bb94296717a4a5b1b3fa9b0fb3c165e517b9b69fa8aaca7fdc7351a0ac14d110258f442f423a780bebd87ac10173ca00ee4e9f56ced0510e7f53ed41411b91286f288438c361d2a15868d1c84d6a73510ef23eee9312ae2a7162c1fcd5438788236c0571ee822c326ebd123b8a6636e7b192db2911725a20da027bfaa79c33f58174285",
},
{
satotxApiPrefix: "https://satotx.metasv.com",
satotxPubKey: "19d9193ee2e95d09445d28408e8a3da730b2d557cd8d39a7ae4ebbfbceb17ed5d745623529ad33d043511f3e205c1f92b6322833424d19823c3b611b3adabb74e1006e0e93a8f1e0b97ab801c6060a4c060f775998d9f003568ab4ea7633a0395eb761c36106e229394f2c271b8522a44a5ae759254f5d22927923ba85b3729460ecccca07a5556299aa7f2518814c74a2a4d48b48013d609002631f2d93c906d07077ef58d473e3d971362d1129c1ab9b8f9b1365519f0c023c1cadad5ab57240d19e256e08022fd0708951ff90a8af0655aff806c6382d0a72c13f1e52b88222d7dfc6357179b06ffcf937f9da3b0419908aa589a731e26bbaba2fa0b754bf722e338c5627b11dc24aadc4d83c35851c034936cf0df18167e856a5f0a7121d23cd48b3f8a420869a37bd1362905d7f76ff18a991f75a0f9d1bcfc18416d76691cc357cbdcc8cc0df9dbd9318a40e08adb2fb4e78b3c47bdf07eeed4f3f4e0f7e81e37460a09b857e0194c72ec03bb564b5b409d8a1b84c153186ecbb4cfdfd",
},
{
satotxApiPrefix: "https://satotx.tswap.io",
satotxPubKey: "a36531727b324b34baef257d223b8ba97bac06d6b631cccb271101f20ef1de2523a0a3a5367d89d98ff354fe1a07bcfb00425ab252950ce10a90dc9040930cf86a3081f0c68ea05bfd40aab3e8bfaaaf6b5a1e7a2b202892dc9b1c0fe478210799759b31ee04e842106a58d901eb5bc538c1b58b7eb774a382e7ae0d6ed706bb0b12b9b891828da5266dd9f0b381b05ecbce99fcde628360d281800cf8ccf4630b2a0a1a25cf4d103199888984cf61edaa0dad578b80dbce25b3316985a8f846ada9bf9bdb8c930e2a43e69994a9b15ea33fe6ee29fa1a6f251f8d79a5de9f1f24152efddedc01b63e2f2468005ecce7da382a64d7936b22a7cac697e1b0a48419101a802d3be554a9b582a80e5c5d8b998e5eb9684c7aaf09ef286d3d990c71be6e3f3340fdaeb2dac70a0be928b6de6ef79f353c868def3385bccd36aa871eb7c8047d3f10b0a38135cdb3577eaafa512111a7af088e8f77821a27b195c95bf80da3c59fda5ff3dd1d40f60d61c099a608b58b6de4a76146cf7b89444c1055",
},
];
contractUtil_1.ContractUtil.init();
function checkParamUtxoFormat(utxo) {
if (utxo) {
if (!utxo.txId || !utxo.satoshis || !utxo.wif) {
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `UtxoFormatError-valid format example :{
txId:'85f583e7a8e8b9cf86e265c2594c1e4eb45db389f6781c3b1ec9aa8e48976caa',
satoshis:1000,
outputIndex:1,
wif:'L3J1A6Xyp7FSg9Vtj3iBKETyVpr6NibxUuLhw3uKpUWoZBLkK1hk'
}`);
}
}
}
function checkParamSigners(signers) {
if (signers.length != ftProto.SIGNER_NUM) {
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `only support ${ftProto.SIGNER_NUM} signers`);
}
let signer = signers[0];
if (Utils.isNull(signer.satotxApiPrefix) ||
Utils.isNull(signer.satotxPubKey)) {
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `SignerFormatError-valid format example :
signers:[{
satotxApiPrefix: "https://api.satotx.com",
satotxPubKey:
"25108ec89eb96b99314619eb5b124f11f00307a833cda48f5ab1865a04d4cfa567095ea4dd47cdf5c7568cd8efa77805197a67943fe965b0a558216011c374aa06a7527b20b0ce9471e399fa752e8c8b72a12527768a9fc7092f1a7057c1a1514b59df4d154df0d5994ff3b386a04d819474efbd99fb10681db58b1bd857f6d5",
},...]`);
}
}
function checkParamNetwork(network) {
if (!["mainnet", "testnet"].includes(network)) {
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `NetworkFormatError:only support 'mainnet' and 'testnet'`);
}
}
function checkParamGenesis(genesis) {
$.checkArgument(_.isString(genesis), "Invalid Argument: genesis should be a string");
$.checkArgument(genesis.length == 40, `Invalid Argument: genesis.length must be 40`);
}
function checkParamCodehash(codehash) {
$.checkArgument(_.isString(codehash), "Invalid Argument: codehash should be a string");
$.checkArgument(codehash.length == 40, `Invalid Argument: codehash.length must be 40`);
$.checkArgument(codehash == contractUtil_1.ContractUtil.tokenCodeHash, `a valid codehash should be ${contractUtil_1.ContractUtil.tokenCodeHash}, but the provided is ${codehash} `);
}
function checkParamReceivers(receivers) {
const ErrorName = "ReceiversFormatError";
if (Utils.isNull(receivers)) {
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `${ErrorName}: param should not be null`);
}
if (receivers.length > 0) {
let receiver = receivers[0];
if (Utils.isNull(receiver.address) || Utils.isNull(receiver.amount)) {
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `${ErrorName}-valid format example
[
{
address: "mtjjuRuA84b2qVyo28AyJQ8AoUmpbWEqs3",
amount: "1000",
},
]
`);
}
let amount = new BN(receiver.amount.toString());
if (amount.lten(0)) {
throw `receiver amount must greater than 0 but now is ${receiver.amount}`;
}
}
}
/**
* 解析sensibleID的值
* @param genesis
* @returns
*/
function parseSensibleID(sensibleID) {
let sensibleIDBuf = Buffer.from(sensibleID, "hex");
let genesisTxId = sensibleIDBuf.slice(0, 32).reverse().toString("hex");
let genesisOutputIndex = sensibleIDBuf.readUIntLE(32, 4);
return {
genesisTxId,
genesisOutputIndex,
};
}
/**
Sensible Fungible Token
*/
class SensibleFT {
/**
*
* @param signers
* @param signerSelecteds (Optional) the indexs of the signers which is decided to verify
* @param feeb (Optional) the fee rate. default is 0.5
* @param network (Optional) mainnet/testnet default is mainnet
* @param purse (Optional) the private key to offer transacions fee. If not provided, bsv utoxs must be provided in genesis/issue/transfer.
* @param debug (Optional) specify if verify the tx when genesis/issue/transfer, default is false
* @param apiTarget (Optional) SENSIBLE/METASV, default is SENSIBLE.
* @param dustLimitFactor (Optional) specify the output dust rate, default is 0.25 .If the value is equal to 0, the final dust will be at least 1.
* @param dustAmount (Optional) specify the output dust.
*/
constructor({ signers = exports.defaultSignerConfigs, signerSelecteds, feeb = 0.05, network = sensible_api_1.API_NET.MAIN, purse, debug = false, apiTarget = sensible_api_1.API_TARGET.SENSIBLE, apiUrl, mockData, dustLimitFactor = 300, dustAmount, }) {
this.signerSelecteds = [];
checkParamNetwork(network);
if (mockData) {
this.signers = mockData.satotxSigners;
}
else {
checkParamSigners(signers);
this.signers = signers.map((v) => new SatotxSigner_1.SatotxSigner(v.satotxApiPrefix, v.satotxPubKey));
}
this.feeb = feeb;
this.network = network;
if (mockData) {
this.sensibleApi = mockData.sensibleApi;
}
else {
this.sensibleApi = new sensible_api_1.SensibleApi(network, apiTarget, apiUrl);
}
this.debug = debug;
this.dustCalculator = new DustCalculator_1.DustCalculator(dustLimitFactor, dustAmount);
if (network == sensible_api_1.API_NET.MAIN) {
this.zeroAddress = new bsv.Address("1111111111111111111114oLvT2");
}
else {
this.zeroAddress = new bsv.Address("mfWxJ45yp2SFn7UciZyNpvDKrzbhyfKrY8");
}
if (purse) {
const privateKey = bsv.PrivateKey.fromWIF(purse);
const address = privateKey.toAddress(this.network);
this.purse = {
privateKey,
address,
};
}
if (signerSelecteds) {
if (signerSelecteds.length < ftProto.SIGNER_VERIFY_NUM) {
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, `the length of signerSeleteds should not less than ${ftProto.SIGNER_VERIFY_NUM}`);
}
this.signerSelecteds = signerSelecteds;
}
else {
for (let i = 0; i < ftProto.SIGNER_VERIFY_NUM; i++) {
this.signerSelecteds.push(i);
}
}
this.signerSelecteds.sort((a, b) => a - b);
let rabinPubKeys = this.signers.map((v) => v.satotxPubKey);
let rabinPubKeyHashArray = TokenUtil.getRabinPubKeyHashArray(rabinPubKeys);
this.rabinPubKeyHashArrayHash =
bsv.crypto.Hash.sha256ripemd160(rabinPubKeyHashArray);
this.rabinPubKeyHashArray = new scryptlib_1.Bytes((0, scryptlib_1.toHex)(rabinPubKeyHashArray));
this.rabinPubKeyArray = rabinPubKeys.map((v) => new scryptlib_1.Int(v.toString(10)));
this.transferCheckCodeHashArray = contractUtil_1.ContractUtil.transferCheckCodeHashArray;
this.unlockContractCodeHashArray = contractUtil_1.ContractUtil.unlockContractCodeHashArray;
}
/**
* Pick the signer with the best connectivity
* @param signerConfigs
* @returns
*/
static selectSigners(signerConfigs = exports.defaultSignerConfigs) {
return __awaiter(this, void 0, void 0, function* () {
return yield (0, satotxSignerUtil_1.selectSigners)(signerConfigs, ftProto.SIGNER_NUM, ftProto.SIGNER_VERIFY_NUM);
});
}
/**
* set dust. DustAmount has a higher priority than dustLimitFactor
* @param dustLimitFactor specify the output dust rate, default is 0.25 .If the value is equal to 0, the final dust will be at least 1.
* @param dustAmount specify the output dust
*/
setDustThreshold({ dustLimitFactor, dustAmount, }) {
this.dustCalculator.dustAmount = dustAmount;
this.dustCalculator.dustLimitFactor = dustLimitFactor;
}
getDustThreshold(size) {
return this.dustCalculator.getDustThreshold(size);
}
_pretreatUtxos(paramUtxos) {
return __awaiter(this, void 0, void 0, function* () {
let utxoPrivateKeys = [];
let utxos = [];
//If utxos are not provided, use purse to fetch utxos
if (!paramUtxos) {
if (!this.purse)
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, "Utxos or Purse must be provided.");
paramUtxos = yield this.sensibleApi.getUnspents(this.purse.address.toString());
paramUtxos.forEach((v) => {
utxoPrivateKeys.push(this.purse.privateKey);
});
}
else {
paramUtxos.forEach((v) => {
if (v.wif) {
let privateKey = new bsv.PrivateKey(v.wif);
utxoPrivateKeys.push(privateKey);
v.address = privateKey.toAddress(this.network).toString(); //Compatible with the old version, only wif is provided but no address is provided
}
});
}
paramUtxos.forEach((v) => {
utxos.push({
txId: v.txId,
outputIndex: v.outputIndex,
satoshis: v.satoshis,
address: new bsv.Address(v.address, this.network),
});
});
if (utxos.length == 0)
throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_BSV, "Insufficient balance.");
return { utxos, utxoPrivateKeys };
});
}
_pretreatFtUtxos(paramFtUtxos, codehash, genesis, senderPrivateKey, senderPublicKey) {
return __awaiter(this, void 0, void 0, function* () {
let ftUtxos = [];
let ftUtxoPrivateKeys = [];
let publicKeys = [];
if (!paramFtUtxos) {
if (senderPrivateKey) {
senderPublicKey = senderPrivateKey.toPublicKey();
}
if (!senderPublicKey)
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_ARGUMENT, "ftUtxos or senderPublicKey or senderPrivateKey must be provided.");
paramFtUtxos = yield this.sensibleApi.getFungibleTokenUnspents(codehash, genesis, senderPublicKey.toAddress(this.network).toString(), 20);
paramFtUtxos.forEach((v) => {
if (senderPrivateKey) {
ftUtxoPrivateKeys.push(senderPrivateKey);
}
publicKeys.push(senderPublicKey);
});
}
else {
paramFtUtxos.forEach((v) => {
if (v.wif) {
let privateKey = new bsv.PrivateKey(v.wif);
ftUtxoPrivateKeys.push(privateKey);
publicKeys.push(privateKey.toPublicKey());
}
});
}
paramFtUtxos.forEach((v, index) => {
ftUtxos.push({
txId: v.txId,
outputIndex: v.outputIndex,
tokenAddress: new bsv.Address(v.tokenAddress, this.network),
tokenAmount: new BN(v.tokenAmount.toString()),
publicKey: publicKeys[index],
});
});
if (ftUtxos.length == 0)
throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_FT, "Insufficient token.");
return { ftUtxos, ftUtxoPrivateKeys };
});
}
/**
* Create a transaction for genesis
* @param tokenName token name, limited to 20 bytes
* @param tokenSymbol the token symbol, limited to 10 bytes
* @param decimalNum the decimal number, range 0-255
* @param utxos (Optional) specify bsv utxos
* @param changeAddress (Optional) specify bsv changeAddress
* @param opreturnData (Optional) append an opReturn output
* @param genesisWif the private key of the token genesiser
* @param noBroadcast (Optional) whether not to broadcast the transaction, the default is false
* @returns
*/
genesis({ tokenName, tokenSymbol, decimalNum, utxos, changeAddress, opreturnData, genesisWif, noBroadcast = false, }) {
return __awaiter(this, void 0, void 0, function* () {
//validate params
$.checkArgument(_.isString(tokenName) && Buffer.from(tokenName).length <= 20, `tokenName should be a string and not be larger than 20 bytes`);
$.checkArgument(_.isString(tokenSymbol) && Buffer.from(tokenSymbol).length <= 10, "tokenSymbol should be a string and not be larger than 10 bytes");
$.checkArgument(_.isNumber(decimalNum) && decimalNum >= 0 && decimalNum <= 255, "decimalNum should be a number and must be between 0 and 255");
$.checkArgument(genesisWif, "genesisWif is required");
let utxoInfo = yield this._pretreatUtxos(utxos);
if (changeAddress) {
changeAddress = new bsv.Address(changeAddress, this.network);
}
else {
changeAddress = utxoInfo.utxos[0].address;
}
let genesisPrivateKey = new bsv.PrivateKey(genesisWif);
let genesisPublicKey = genesisPrivateKey.toPublicKey();
let { txComposer } = yield this._genesis({
tokenName,
tokenSymbol,
decimalNum,
utxos: utxoInfo.utxos,
utxoPrivateKeys: utxoInfo.utxoPrivateKeys,
changeAddress: changeAddress,
opreturnData,
genesisPublicKey,
});
let txHex = txComposer.getRawHex();
if (!noBroadcast) {
yield this.sensibleApi.broadcast(txHex);
}
let { codehash, genesis, sensibleId } = this.getCodehashAndGensisByTx(txComposer.getTx());
return {
txHex,
txid: txComposer.getTxId(),
tx: txComposer.getTx(),
codehash,
genesis,
sensibleId,
};
});
}
/**
* create an unsigned transaction for genesis
* @param tokenName token name, limited to 20 bytes
* @param tokenSymbol the token symbol, limited to 10 bytes
* @param decimalNum the decimal number, range 0-255
* @param utxos (Optional) specify bsv utxos
* @param changeAddress (Optional) specify bsv changeAddress
* @param opreturnData (Optional) append an opReturn output
* @param genesisPublicKey the public key of the token genesiser
* @param noBroadcast (Optional) whether not to broadcast the transaction, the default is false
* @returns
* @returns
*/
unsignGenesis({ tokenName, tokenSymbol, decimalNum, utxos, changeAddress, opreturnData, genesisPublicKey, }) {
return __awaiter(this, void 0, void 0, function* () {
//validate params
$.checkArgument(_.isString(tokenName) && Buffer.from(tokenName).length <= 20, `tokenName should be a string and Buffer.from(tokenName).length must not be larger than 20`);
$.checkArgument(_.isString(tokenSymbol) && Buffer.from(tokenSymbol).length <= 10, "tokenSymbol should be a string and Buffer.from(tokenSymbol).length must not be larger than 10");
$.checkArgument(_.isNumber(decimalNum) && decimalNum >= 0 && decimalNum <= 255, "decimalNum should be a number and must be between 0 and 255");
$.checkArgument(genesisPublicKey, "genesisPublicKey is required");
let utxoInfo = yield this._pretreatUtxos(utxos);
if (changeAddress) {
changeAddress = new bsv.Address(changeAddress, this.network);
}
else {
changeAddress = utxoInfo.utxos[0].address;
}
genesisPublicKey = new bsv.PublicKey(genesisPublicKey);
let { txComposer } = yield this._genesis({
tokenName,
tokenSymbol,
decimalNum,
utxos: utxoInfo.utxos,
changeAddress: changeAddress,
opreturnData,
genesisPublicKey,
});
return { tx: txComposer.getTx(), sigHashList: txComposer.getSigHashLit() };
});
}
_genesis({ tokenName, tokenSymbol, decimalNum, utxos, utxoPrivateKeys, changeAddress, opreturnData, genesisPublicKey, }) {
return __awaiter(this, void 0, void 0, function* () {
//create genesis contract
let genesisContract = tokenGenesis_1.TokenGenesisFactory.createContract(genesisPublicKey);
genesisContract.setFormatedDataPart({
tokenName,
tokenSymbol,
decimalNum,
rabinPubKeyHashArrayHash: (0, scryptlib_1.toHex)(this.rabinPubKeyHashArrayHash),
});
let estimateSatoshis = yield this.getGenesisEstimateFee({
opreturnData,
utxoMaxCount: utxos.length,
});
const balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0);
if (balance < estimateSatoshis) {
throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_BSV, `Insufficient balance.It take more than ${estimateSatoshis}, but only ${balance}.`);
}
const txComposer = new tx_composer_1.TxComposer();
const p2pkhInputIndexs = utxos.map((utxo) => {
const inputIndex = txComposer.appendP2PKHInput(utxo);
txComposer.addSigHashInfo({
inputIndex,
address: utxo.address.toString(),
sighashType: exports.sighashType,
contractType: utils_1.CONTRACT_TYPE.P2PKH,
});
return inputIndex;
});
const genesisOutputIndex = txComposer.appendOutput({
lockingScript: genesisContract.lockingScript,
satoshis: this.getDustThreshold(genesisContract.lockingScript.toBuffer().length),
});
//If there is opReturn, add it to the second output
if (opreturnData) {
txComposer.appendOpReturnOutput(opreturnData);
}
txComposer.appendChangeOutput(changeAddress, this.feeb);
if (utxoPrivateKeys && utxoPrivateKeys.length > 0) {
p2pkhInputIndexs.forEach((inputIndex) => {
let privateKey = utxoPrivateKeys.splice(0, 1)[0];
txComposer.unlockP2PKHInput(privateKey, inputIndex);
});
}
this._checkTxFeeRate(txComposer);
return { txComposer };
});
}
/**
* Issue tokens.
* @param genesis the genesis of token.
* @param codehash the codehash of token.
* @param sensibleId the sensibleId of token.
* @param genesisWif the private key of the token genesiser
* @param receiverAddress the token receiver address
* @param tokenAmount the token amount to issue
* @param allowIncreaseIssues (Optional) if allow to increase issues.default is true
* @param utxos (Optional) specify bsv utxos
* @param changeAddress (Optional) specify bsv changeAddress
* @param opreturnData (Optional) append an opReturn output
* @param noBroadcast (Optional) whether not to broadcast the transaction, the default is false
* @returns
*/
issue({ genesis, codehash, sensibleId, genesisWif, receiverAddress, tokenAmount, allowIncreaseIssues = true, utxos, changeAddress, opreturnData, noBroadcast = false, }) {
return __awaiter(this, void 0, void 0, function* () {
checkParamGenesis(genesis);
checkParamCodehash(codehash);
$.checkArgument(sensibleId, "sensibleId is required");
$.checkArgument(genesisWif, "genesisWif is required");
$.checkArgument(receiverAddress, "receiverAddress is required");
$.checkArgument(tokenAmount, "tokenAmount is required");
let utxoInfo = yield this._pretreatUtxos(utxos);
if (changeAddress) {
changeAddress = new bsv.Address(changeAddress, this.network);
}
else {
changeAddress = utxoInfo.utxos[0].address;
}
let genesisPrivateKey = new bsv.PrivateKey(genesisWif);
let genesisPublicKey = genesisPrivateKey.toPublicKey();
receiverAddress = new bsv.Address(receiverAddress, this.network);
tokenAmount = new BN(tokenAmount.toString());
let { txComposer } = yield this._issue({
genesis,
codehash,
sensibleId,
receiverAddress,
tokenAmount,
allowIncreaseIssues,
utxos: utxoInfo.utxos,
utxoPrivateKeys: utxoInfo.utxoPrivateKeys,
changeAddress,
opreturnData,
genesisPrivateKey,
genesisPublicKey,
});
let txHex = txComposer.getRawHex();
if (!noBroadcast) {
yield this.sensibleApi.broadcast(txHex);
}
return { txHex, txid: txComposer.getTxId(), tx: txComposer.getTx() };
});
}
/**
* Create the unsigned transaction for issue tokens,
* @param genesis the genesis of token.
* @param codehash the codehash of token.
* @param genesisPublicKey the public key of the token genesiser
* @param receiverAddress the token receiver address
* @param tokenAmount the token amount to issue
* @param allowIncreaseIssues (Optional) if allow to increase issues.default is true
* @param utxos (Optional) specify bsv utxos
* @param changeAddress (Optional) specify bsv changeAddress
* @param opreturnData (Optional) append an opReturn output
* @returns
*/
unsignIssue({ genesis, codehash, sensibleId, genesisPublicKey, receiverAddress, tokenAmount, allowIncreaseIssues = true, utxos, changeAddress, opreturnData, }) {
return __awaiter(this, void 0, void 0, function* () {
checkParamGenesis(genesis);
checkParamCodehash(codehash);
$.checkArgument(sensibleId, "sensibleId is required");
$.checkArgument(genesisPublicKey, "genesisPublicKey is required");
$.checkArgument(receiverAddress, "receiverAddress is required");
$.checkArgument(tokenAmount, "tokenAmount is required");
let utxoInfo = yield this._pretreatUtxos(utxos);
if (changeAddress) {
changeAddress = new bsv.Address(changeAddress, this.network);
}
else {
changeAddress = utxoInfo.utxos[0].address;
}
let _genesisPublicKey = new bsv.PublicKey(genesisPublicKey);
receiverAddress = new bsv.Address(receiverAddress, this.network);
tokenAmount = new BN(tokenAmount.toString());
let { txComposer } = yield this._issue({
genesis,
codehash,
sensibleId,
receiverAddress,
tokenAmount,
allowIncreaseIssues,
utxos: utxoInfo.utxos,
utxoPrivateKeys: utxoInfo.utxoPrivateKeys,
changeAddress,
opreturnData,
genesisPublicKey: _genesisPublicKey,
});
return { tx: txComposer.getTx(), sigHashList: txComposer.getSigHashLit() };
});
}
_getIssueUtxo(codehash, genesisTxId, genesisOutputIndex) {
return __awaiter(this, void 0, void 0, function* () {
let unspent;
let firstGenesisTxHex = yield this.sensibleApi.getRawTxData(genesisTxId);
let firstGenesisTx = new bsv.Transaction(firstGenesisTxHex);
let scriptBuffer = firstGenesisTx.outputs[genesisOutputIndex].script.toBuffer();
let originGenesis = ftProto.getQueryGenesis(scriptBuffer);
let genesisUtxos = yield this.sensibleApi.getFungibleTokenUnspents(codehash, originGenesis, this.zeroAddress.toString());
unspent = genesisUtxos.find((v) => v.txId == genesisTxId && v.outputIndex == genesisOutputIndex);
if (!unspent) {
let _dataPartObj = ftProto.parseDataPart(scriptBuffer);
_dataPartObj.sensibleID = {
txid: genesisTxId,
index: genesisOutputIndex,
};
let newScriptBuf = ftProto.updateScript(scriptBuffer, _dataPartObj);
let issueGenesis = ftProto.getQueryGenesis(newScriptBuf);
let issueUtxos = yield this.sensibleApi.getFungibleTokenUnspents(codehash, issueGenesis, this.zeroAddress.toString());
if (issueUtxos.length > 0) {
unspent = issueUtxos[0];
}
}
if (unspent) {
return {
txId: unspent.txId,
outputIndex: unspent.outputIndex,
};
}
});
}
_prepareIssueUtxo({ sensibleId, genesisPublicKey, }) {
return __awaiter(this, void 0, void 0, function* () {
let genesisContract = tokenGenesis_1.TokenGenesisFactory.createContract(genesisPublicKey);
//Looking for UTXO for issue
let { genesisTxId, genesisOutputIndex } = parseSensibleID(sensibleId);
let genesisUtxo = yield this._getIssueUtxo(genesisContract.getCodeHash(), genesisTxId, genesisOutputIndex);
if (!genesisUtxo) {
throw new error_1.CodeError(error_1.ErrCode.EC_FIXED_TOKEN_SUPPLY, "token supply is fixed");
}
let txHex = yield this.sensibleApi.getRawTxData(genesisUtxo.txId);
const tx = new bsv.Transaction(txHex);
let preTxId = tx.inputs[0].prevTxId.toString("hex");
let preOutputIndex = tx.inputs[0].outputIndex;
let preTxHex = yield this.sensibleApi.getRawTxData(preTxId);
genesisUtxo.satotxInfo = {
txId: genesisUtxo.txId,
outputIndex: genesisUtxo.outputIndex,
txHex,
preTxId,
preOutputIndex,
preTxHex,
};
let output = tx.outputs[genesisUtxo.outputIndex];
genesisUtxo.satoshis = output.satoshis;
genesisUtxo.lockingScript = output.script;
genesisContract.setFormatedDataPartFromLockingScript(genesisUtxo.lockingScript);
return {
genesisContract,
genesisTxId,
genesisOutputIndex,
genesisUtxo,
};
});
}
_issue({ genesis, codehash, sensibleId, receiverAddress, tokenAmount, allowIncreaseIssues = true, utxos, utxoPrivateKeys, changeAddress, opreturnData, genesisPrivateKey, genesisPublicKey, }) {
return __awaiter(this, void 0, void 0, function* () {
let { genesisContract, genesisTxId, genesisOutputIndex, genesisUtxo } = yield this._prepareIssueUtxo({ sensibleId, genesisPublicKey });
let balance = utxos.reduce((pre, cur) => pre + cur.satoshis, 0);
let estimateSatoshis = yield this._calIssueEstimateFee({
genesisUtxoSatoshis: genesisUtxo.satoshis,
opreturnData,
allowIncreaseIssues,
utxoMaxCount: utxos.length,
});
if (balance < estimateSatoshis) {
throw new error_1.CodeError(error_1.ErrCode.EC_INSUFFICIENT_BSV, `Insufficient balance.It take more than ${estimateSatoshis}, but only ${balance}.`);
}
let newGenesisContract = genesisContract.clone();
newGenesisContract.setFormatedDataPart({
sensibleID: {
txid: genesisTxId,
index: genesisOutputIndex,
},
});
let tokenContract = token_1.TokenFactory.createContract(this.transferCheckCodeHashArray, this.unlockContractCodeHashArray);
tokenContract.setFormatedDataPart(Object.assign({}, newGenesisContract.getFormatedDataPart(), {
tokenAddress: (0, scryptlib_1.toHex)(receiverAddress.hashBuffer),
tokenAmount,
genesisHash: newGenesisContract.getScriptHash(),
}));
if (genesisContract.getFormatedDataPart().rabinPubKeyHashArrayHash !=
(0, scryptlib_1.toHex)(this.rabinPubKeyHashArrayHash)) {
throw new error_1.CodeError(error_1.ErrCode.EC_INVALID_SIGNERS, "Invalid signers.");
}
let { rabinData, rabinPubKeyIndexArray, rabinPubKeyVerifyArray } = yield (0, satotxSignerUtil_1.getRabinData)(this.signers, this.signerSelecteds, genesisContract.isFirstGenesis() ? null : genesisUtxo.satotxInfo);
const txComposer = new tx_composer_1.TxComposer();
//The first input is the genesis contract
const genesisInputIndex = txComposer.appendInput(genesisUtxo);
txComposer.addSigHashInfo({
inputIndex: genesisInputIndex,
address: genesisPublicKey.toAddress(this.network).toString(),
sighashType: exports.sighashType,
contractType: utils_1.CONTRACT_TYPE.BCP02_TOKEN_GENESIS,
});
const p2pkhInputIndexs = utxos.map((utxo) => {
const inputIndex = txComposer.appendP2PKHInput(utxo);
txComposer.addSigHashInfo({
inputIndex,
address: utxo.address.toString(),
sighashType: exports.sighashType,
contractType: utils_1.CONTRACT_TYPE.P2PKH,
});
return inputIndex;
});
//If increase issues is allowed, add a new issue contract as the first output
let newGenesisOutputIndex = -1;
if (allowIncreaseIssues) {
newGenesisOutputIndex = txComposer.appendOutput({
lockingScript: newGenesisContract.lockingScript,
satoshis: this.getDustThreshold(newGenesisContract.lockingScript.toBuffer().length),
});
}
//The following output is the Token
const tokenOutputIndex = txComposer.appendOutput({
lockingScript: tokenContract.lockingScript,
satoshis: this.getDustThreshold(tokenContract.lockingScript.toBuffer().length),
});
//If there is opReturn, add it to the output
let opreturnScriptHex = "";
if (opreturnData) {
const opreturnOutputIndex = txComposer.appendOpReturnOutput(opreturnData);
opreturnScriptHex = txComposer
.getOutput(opreturnOutputIndex)
.script.toHex();
}
//The first round of calculations get the exact size of the final transaction, and then change again
//Due to the change, the script needs to be unlocked again in the second round
//let the fee to be exact in the second round
for (let c = 0; c < 2; c++) {
txComposer.clearChangeOutput();
const changeOutputIndex = txComposer.appendChangeOutput(changeAddress, this.feeb);
let unlockResult = genesisContract.unlock({
txPreimage: txComposer.getInputPreimage(genesisInputIndex),
sig: new scryptlib_1.Sig(genesisPrivateKey
? (0, scryptlib_1.toHex)(txComposer.getTxFormatSig(genesisPrivateKey, genesisInputIndex))
: utils_1.PLACE_HOLDER_SIG),
rabinMsg: rabinData.rabinMsg,
rabinPaddingArray: rabinData.rabinPaddingArray,
rabinSigArray: rabinData.rabinSigArray,
rabinPubKeyIndexArray,
rabinPubKeyVerifyArray,
rabinPubKeyHashArray: this.rabinPubKeyHashArray,
genesisSatoshis: newGenesisOutputIndex != -1
? txComposer.getOutput(newGenesisOutputIndex).satoshis
: 0,
tokenScript: new scryptlib_1.Bytes(txComposer.getOutput(tokenOutputIndex).script.toHex()),
tokenSatoshis: txComposer.getOutput(tokenOutputIndex).satoshis,
changeAddress: new scryptlib_1.Ripemd160((0, scryptlib_1.toHex)(changeAddress.hashBuffer)),
changeSatoshis: changeOutputIndex != -1
? txComposer.getOutput(changeOutputIndex).satoshis
: 0,
opReturnScript: new scryptlib_1.Bytes(opreturnScriptHex),
});
if (this.debug && genesisPrivateKey && c == 1) {
let ret = unlockResult.verify({
tx: txComposer.tx,
inputIndex: genesisInputIndex,
inputSatoshis: txComposer.getInput(genesisInputIndex).output.satoshis,
});
if (ret.success == false)
throw ret;
}
txComposer
.getInput(genesisInputIndex)
.setScript(unlockResult.toScript());
}
if (utxoPrivateKeys && utxoPrivateKeys.length > 0) {
p2pkhInputIndexs.forEach((inputIndex) => {
let privateKey = utxoPrivateKeys.splice(0, 1)[0];
txComposer.unlockP2PKHInput(privateKey, inputIndex);
});
}
this._checkTxFeeRate(txComposer);
return { txComposer };
});
}
/**
* After deciding which ftUxtos to use, perfect the information of FtUtxo
* txHex,preTxId,preOutputIndex,preTxHex,preTokenAddress,preTokenAmount
*/
perfectFtUtxosInfo(ftUtxos, codehash, genesis) {
return __awaiter(this, void 0, void 0, function* () {
//Cache txHex to prevent redundant queries
let cachedHexs = {};
//Get txHex
for (let i = 0; i < ftUtxos.length; i++) {
let ftUtxo = ftUtxos[i];
if (!cachedHexs[ftUtxo.txId]) {
cachedHexs[ftUtxo.txId] = {
waitingRes: this.sensibleApi.getRawTxData(ftUtxo.txId), //async request
};
}
}
for (let id in cachedHexs) {
//Wait for all async requests to complete
if (cachedHexs[id].waitingRes && !cachedHexs[id].hex) {
cachedHexs[id].hex = yield cachedHexs[id].waitingRes;
}
}
ftUtxos.forEach((v) => {
v.satotxInfo = v.satotxInfo || {};
v.satotxInfo.txHex = cachedHexs[v.txId].hex;
v.satotxInfo.txId = v.txId;
v.satotxInfo.outputIndex = v.outputIndex;
});
//Get preTxHex
let curDataPartObj;
for (let i = 0; i < ftUtxos.length; i++) {
let ftUtxo = ftUtxos[i];
const tx = new bsv.Transaction(ftUtxo.satotxInfo.txHex);
if (!curDataPartObj) {
let tokenScript = tx.outputs[ftUtxo.outputIndex].script;
curDataPartObj = ftProto.parseDataPart(tokenScript.toBuffer());
if (curDataPartObj.rabinPubKeyHashArrayHash !=
(0, scryptlib_1.toHex)(this.rabinPubKeyHashArrayHash)) {
throw new error_1.CodeError(error_1.ErrCode.EC_INNER_ERROR, "The currently used signers does not correspond to the token.");
}
}
//Find a valid preTx
let input = tx.inputs.find((input) => {
let script = new bsv.Script(input.script);
if (script.chunks.length > 0) {
const lockingScriptBuf = TokenUtil.getLockingScriptFromPreimage(script.chunks[0].buf);
if (lockingScriptBuf) {
if (ftProto.getQueryGenesis(lockingScriptBuf) == genesis) {
return true;
}
let dataPartObj = ftProto.parseDataPart(lockingScriptBuf);
dataPartObj.sensibleID = curDataPartObj.sensibleID;
const newScriptBuf = ftProto.updateScript(lockingScriptBuf, dataPartObj);
let genesisHash = (0, scryptlib_1.toHex)(bsv.crypto.Hash.sha256ripemd160(newScriptBuf));
if (genesisHash == curDataPartObj.genesisHash) {
return true;
}
}
}
});
if (!input)
throw new error_1.CodeError(error_1.ErrCode.EC_INNER_ERROR, "There is no valid preTx of the ftUtxo. ");
let preTxId = input.prevTxId.toString("hex");
let preOutputIndex = input.outputIndex;
ftUtxo.satotxInfo.preTxId = preTxId;
ftUtxo.satotxInfo.preOutputIndex = preOutputIndex;
ftUtxo.satoshis = tx.outputs[ftUtxo.outputIndex].satoshis;
ftUtxo.lockingScript = tx.outputs[ftUtxo.outputIndex].script;
if (!cachedHexs[preTxId]) {
cachedHexs[preTxId] = {
waitingRes: this.sensibleApi.getRawTxData(preTxId),
};
}
}
for (let id in cachedHexs) {
//Wait for all async requests to complete
if (cachedHexs[id].waitingRes && !cachedHexs[id].hex) {
cachedHexs[id].hex = yield cachedHexs[id].waitingRes;
}
}
ftUtxos.forEach((v) => {
v.satotxInfo.preTxHex = cachedHexs[v.satotxInfo.preTxId].hex;
const preTx = new bsv.Transaction(v.satotxInfo.preTxHex);
let dataPartObj = ftProto.parseDataPart(preTx.outputs[v.satotxInfo.preOutputIndex].script.toBuffer());
v.preTokenAmount = dataPartObj.tokenAmount;
if (dataPartObj.tokenAddress == "0000000000000000000000000000000000000000") {
v.preTokenAddress = this.zeroAddress;
}
else {
v.preTokenAddress = bsv.Address.fromPublicKeyHash(Buffer.from(dataPartObj.tokenAddress, "hex"), this.network);
}
v.preLockingScript = preTx.outputs[v.satotxInfo.preOutputIndex].script;
});
ftUtxos.forEach((v) => {
v.preTokenAmount = new BN(v.preTokenAmount.toString());
});
return ftUtxos;
});
}
/**
* Transfer tokens
* @param genesis the genesis of token.
* @param codehash the codehash of token.
* @param receivers token receivers.[{address:'xxx',amount:'1000'}]
* @param senderWif the private key of the token sender,can be wif or other format
* @param ftUtxos (Optional) specify token utxos
* @param ftChangeAddress (Optional) specify ft changeAddress
* @param utxos (Optional) specify bsv utxos which should be no more than 3
* @param changeAddress (Optional) specify bsv changeAddress
* @param middleChangeAddress (Optional) the middle bsv changeAddress
* @param middlePrivateKey (Optional) the private key of the middle changeAddress
* @param isMerge (Optional) do not use this param.Please use function Merge.
* @param opreturnData (Optional) append an opReturn output
* @param noBroadcast (Optional) whether not to broadcast the transaction, the default is false
* @returns
*/
transfer({ codehash, genesis, receivers, senderWif, ftUtxos, ftChangeAddress, utxos, changeAddress, middleChangeAddress, middlePrivateKey, minUtxoSet = true, isMerge, opreturnData, noBroadcast = false, }) {
return __awaiter(this, void 0, void 0, function* () {
checkParamGenesis(genesis);
checkParamCodehash(codehash);
checkParamReceivers(receivers);
let senderPrivateKey;
let senderPublicKey;
if (senderWif) {
senderPrivateKey = new bsv.PrivateKey(senderWif);
}
let utxoInfo = yield this._pretreatUtxos(utxos);
if (changeAddress) {
changeAddress = new bsv.Address(changeAddress, this.network);
}
else {
changeAddress = utxoInfo.utxos[0].address;
}
if (middleChangeAddress) {
middleChangeAddress = new bsv.Address(middleChangeAddress, this.network);
middlePrivateKey = new bsv.PrivateKey(middlePrivateKey);
}
else {
middleChangeAddress = utxoInfo.utxos[0].address;
middlePrivateKey = utxoInfo.utxoPrivateKeys[0];
}
let ftUtxoInfo = yield this._pretreatFtUtxos(ftUtxos, codehash, genesis, senderPrivateKey, senderPublicKey);
if (ftChangeAddress) {
ftChangeAddress = new bsv.Address(ftChangeAddress, this.network);
}
else {
ftChangeAddress = ftUtxoInfo.ftUtxos[0].tokenAddress;
}
let { txComposer, transferCheckTxComposer } = yield this._transfer({
codehash,
genesis,
receivers,
ftUtxos: ftUtxoInfo.ftUtxos,
ftPrivateKeys: ftUtxoInfo.ftUtxoPrivateKeys,
ftChangeAddress,
utxos: utxoInfo.utxos,
utxoPrivateKeys: utxoInfo.utxoPrivateKeys,
changeAddress,
opreturnData,
isMerge,
middleChangeAddress,
middlePrivateKey,
minUtxoSet,
});
let routeCheckTxHex = transferCheckTxComposer.getRawHex();
let txHex = txComposer.getRawHex();
if (!noBroadcast) {
yield this.sensibleApi.broadcast(routeCheckTxHex);
yield this.sensibleApi.broadcast(txHex);
}
return {
tx: txComposer.getTx(),
txHex,
routeCheckTx: transferCheckTxComposer.getTx(),
routeCheckTxHex,
txid: txComposer.getTxId(),
};
});
}
/**
* create the first part of unsigned transaction to transfer tokens.
* @param genesis the genesis of token.
* @param codehash the codehash of token.
* @param receivers token receivers.[{address:'xxx',amount:'1000'}]
* @param senderPublicKey the public key of the token sender
* @param ftUtxos (Optional) specify token u