scrypt-ts
Version:
A toolset for building sCrypt smart contract applications on Bitcoin SV network written in typescript.
339 lines • 14.2 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.TestWallet = void 0;
const abstract_signer_1 = require("../abstract-signer");
const dist_1 = require("scryptlib/dist");
const utils_1 = require("../utils");
const DEFAULT_SIGHASH_TYPE = dist_1.bsv.crypto.Signature.ALL;
/**
* An implemention of a simple wallet which should just be used in dev/test environments.
* It can hold multiple private keys and have a feature of cachable in-memory utxo management.
*
* Reminder: DO NOT USE IT IN PRODUCTION ENV.
*/
class TestWallet extends abstract_signer_1.Signer {
constructor(privateKey, provider) {
super(provider);
this.splitFeeTx = true;
if (privateKey instanceof Array) {
this._privateKeys = privateKey;
}
else {
this._privateKeys = [privateKey];
}
this.checkPrivateKeys();
this._utxoManagers = new Map();
}
static random(provider, network = dist_1.bsv.Networks.testnet) {
return new TestWallet(dist_1.bsv.PrivateKey.fromRandom(network), provider);
}
enableSplitFeeTx(on) {
this.splitFeeTx = on;
}
isAuthenticated() {
return __awaiter(this, void 0, void 0, function* () {
return Promise.resolve(true);
});
}
requestAuth() {
return __awaiter(this, void 0, void 0, function* () {
return Promise.resolve({ isAuthenticated: true, error: '' });
});
}
get network() {
return this.provider.getNetwork();
}
get addresses() {
return this._privateKeys.map(p => p.toAddress(this.network).toString());
}
addPrivateKey(privateKey) {
const keys = privateKey instanceof Array ? privateKey : [privateKey];
this._privateKeys.push(...keys);
this.checkPrivateKeys();
return this;
}
checkPrivateKeys() {
const networks = this._privateKeys.map(key => key.toAddress().network);
if (!networks.every(n => n.name === networks[0].name)) {
throw new Error(`All private keys should be ${networks[0].name} private key`);
}
return networks[0];
}
setProvider(provider) {
throw new Error("cannot alter provider");
}
getDefaultAddress() {
return Promise.resolve(this._defaultPrivateKey.toAddress());
}
getDefaultPubKey() {
return Promise.resolve(this._defaultPrivateKey.toPublicKey());
}
getPubKey(address) {
return Promise.resolve(this._getPrivateKeys(address)[0].toPublicKey());
}
getNetwork() {
return Promise.resolve(this._defaultPrivateKey.toAddress().network);
}
getBalance(address) {
const _super = Object.create(null, {
getBalance: { get: () => super.getBalance }
});
return __awaiter(this, void 0, void 0, function* () {
address = address || (yield this.getDefaultAddress());
let utxoManager = this._utxoManagers.get(address.toString());
if (utxoManager) {
return {
confirmed: utxoManager.balance(),
unconfirmed: 0,
};
}
return _super.getBalance.call(this, address);
});
}
signTransaction(tx, options) {
return __awaiter(this, void 0, void 0, function* () {
const addresses = options === null || options === void 0 ? void 0 : options.address;
this._checkAddressOption(addresses);
// TODO: take account of SignatureRequests in options.
return Promise.resolve(tx.sign(this._getPrivateKeys(addresses)));
});
}
signMessage(message, address) {
throw new Error("Method #signMessage not implemented.");
}
getSignatures(rawTxHex, sigRequests) {
this._checkAddressOption(this._getAddressesIn(sigRequests));
const tx = new dist_1.bsv.Transaction(rawTxHex);
const sigResponses = sigRequests.flatMap(sigReq => {
const script = sigReq.scriptHex ? new dist_1.bsv.Script(sigReq.scriptHex) : dist_1.bsv.Script.buildPublicKeyHashOut((0, utils_1.parseAddresses)(sigReq.address, this.network)[0]);
tx.inputs[sigReq.inputIndex].output = new dist_1.bsv.Transaction.Output({
// TODO: support multiSig?
script: script,
satoshis: sigReq.satoshis
});
const privkeys = this._getPrivateKeys(sigReq.address);
return privkeys.map(privKey => {
// Split to subscript if OP_CODESEPARATOR is being employed.
const subScript = sigReq.csIdx !== undefined ? script.subScript(sigReq.csIdx) : script;
const sig = (0, dist_1.signTx)(tx, privKey, subScript, sigReq.satoshis, sigReq.inputIndex, sigReq.sigHashType);
return {
sig: sig,
publicKey: privKey.publicKey.toString(),
inputIndex: sigReq.inputIndex,
sigHashType: sigReq.sigHashType || DEFAULT_SIGHASH_TYPE,
csIdx: sigReq.csIdx,
};
});
});
return Promise.resolve(sigResponses);
}
listUnspent(address, options) {
return __awaiter(this, void 0, void 0, function* () {
if (this.splitFeeTx) {
let utxoManager = this._utxoManagers.get(address.toString());
if (!utxoManager) {
utxoManager = new CacheableUtxoManager(address, this);
this._utxoManagers.set(address.toString(), utxoManager);
yield utxoManager.init();
}
if (options) {
const unspentValue = options.unspentValue;
const estimateSize = options.estimateSize;
const feePerKb = options.feePerKb;
// providerAmount = ((estimateSize + (180 * n)) / 1000 * feePerKb) - unspentValue
let providerAmount = Math.ceil((estimateSize / 1000 * feePerKb) - unspentValue);
providerAmount += Math.ceil(((options.additional || 0) + 180) / 1000 * feePerKb);
return utxoManager.fetchUtxos(providerAmount);
}
else {
return utxoManager.fetchUtxos(0);
}
}
else {
return this.provider.listUnspent(address, options);
}
});
}
_getAddressesIn(sigRequests) {
return (sigRequests || []).flatMap((req) => {
return req.address instanceof Array ? req.address : [req.address];
});
}
_checkAddressOption(address) {
if (!address)
return;
if (address instanceof Array) {
address.forEach(address => this._checkAddressOption(address));
}
else {
if (!this.addresses.includes(address.toString())) {
throw new Error(`the address ${address.toString()} does not belong to this TestWallet`);
}
}
}
get _defaultPrivateKey() {
return this._privateKeys[0];
}
_getPrivateKeys(address) {
if (!address)
return [this._defaultPrivateKey];
this._checkAddressOption(address);
let addresses = [];
if (address instanceof Array) {
address.forEach(addr => addresses.push(addr.toString()));
}
else {
addresses.push(address.toString());
}
return this._privateKeys.filter(priv => addresses.includes(priv.toAddress(this.network).toString()));
}
signAndsendTransaction(tx, options) {
const _super = Object.create(null, {
signAndsendTransaction: { get: () => super.signAndsendTransaction }
});
return __awaiter(this, void 0, void 0, function* () {
const signedTx = yield _super.signAndsendTransaction.call(this, tx, options);
const address = yield this.getDefaultAddress();
let utxoManager = this._utxoManagers.get(address.toString());
if (utxoManager) {
utxoManager.collectUtxoFromTx(tx);
}
return signedTx;
});
}
}
exports.TestWallet = TestWallet;
var InitState;
(function (InitState) {
InitState[InitState["UNINITIALIZED"] = 0] = "UNINITIALIZED";
InitState[InitState["INITIALIZING"] = 1] = "INITIALIZING";
InitState[InitState["INITIALIZED"] = 2] = "INITIALIZED";
})(InitState || (InitState = {}));
;
class CacheableUtxoManager {
constructor(address, signer) {
this.availableUtxos = [];
this.initStates = InitState.UNINITIALIZED;
this.initUtxoCnt = 0;
this.feePerkb = 50;
this.address = address;
this.signer = signer;
}
init() {
return __awaiter(this, void 0, void 0, function* () {
if (this.initStates === InitState.INITIALIZED) {
return this;
}
if (this.initStates === InitState.UNINITIALIZED) {
this.initStates = InitState.INITIALIZING;
this.availableUtxos = yield this.signer.connectedProvider.listUnspent(this.address);
this.feePerkb = yield this.signer.connectedProvider.getFeePerKb();
this.initStates = InitState.INITIALIZED;
this.initUtxoCnt = this.availableUtxos.length;
}
while (this.initStates === InitState.INITIALIZING) {
yield sleep(1);
}
return this;
});
}
balance() {
return this.availableUtxos.reduce((r, utxo) => r + utxo.satoshis, 0);
}
fetchUtxos(targetSatoshis) {
return __awaiter(this, void 0, void 0, function* () {
if (this.initStates === InitState.INITIALIZED
&& this.initUtxoCnt > 0
&& this.availableUtxos.length === 0) {
const timeoutSec = 30;
for (let i = 0; i < timeoutSec; i++) {
console.log('waiting for available utxos');
yield sleep(1);
if (this.availableUtxos.length > 0) {
break;
}
}
}
if (targetSatoshis <= 0) {
return this.availableUtxos.splice(0);
}
const sortedUtxos = this.availableUtxos.sort((a, b) => a.satoshis - b.satoshis);
if (targetSatoshis > sortedUtxos.reduce((r, utxo) => r + utxo.satoshis, 0)) {
throw new Error('no sufficient utxos to pay the fee of ' + targetSatoshis);
}
let idx = 0;
let accAmt = 0;
const dustLimit = 1; //min change amount
let expectedAmt = 0;
for (let i = 0; i < sortedUtxos.length; i++) {
accAmt += sortedUtxos[i].satoshis;
// estimateFee of splitTx
// 180 - Input.BASE_SIZE + PublicKeyHashInput.SCRIPT_MAX_SIZE
// 10 + 34 * 2 - transation header and two output
const estimateFee = Math.ceil((180 * (i + 1) + (10 + 34) * 2) * this.feePerkb / 1000);
expectedAmt = targetSatoshis + estimateFee + dustLimit;
if (accAmt >= expectedAmt) {
idx = i;
break;
}
}
const usedUtxos = sortedUtxos.slice(0, idx + 1);
// update the available utxos, remove used ones
this.availableUtxos = sortedUtxos.slice(idx + 1);
if (accAmt >= expectedAmt) {
// split `accAmt` to `targetSatoshis` + `change`
const splitTx = new dist_1.bsv.Transaction().from(usedUtxos)
.addOutput(new dist_1.bsv.Transaction.Output({
script: dist_1.bsv.Script.buildPublicKeyHashOut(this.address),
satoshis: targetSatoshis
}))
.feePerKb(this.feePerkb)
.change(this.address); // here generates a new available utxo for address
yield this.signer.signAndsendTransaction(splitTx);
// return the new created utxo which has value of `targetSatoshis`
const index = this.availableUtxos.findIndex(utxo => utxo.satoshis === targetSatoshis);
return this.availableUtxos.splice(index, 1);
}
else {
return usedUtxos;
}
});
}
collectUtxoFromTx(tx) {
const txId = tx.id;
tx.outputs.forEach((output, index) => {
this.collectUtxoFrom(output, txId, index);
});
}
collectUtxoFrom(output, txId, outputIndex) {
if (output.script.toHex() === this.utxoScriptHex) {
this.availableUtxos.push({
txId,
outputIndex,
satoshis: output.satoshis,
script: output.script.toHex()
});
}
}
get utxoScriptHex() {
// all managed utxos should have the same P2PKH script for `this.address`
return dist_1.bsv.Script.buildPublicKeyHashOut(this.address).toHex();
}
}
const sleep = (seconds) => __awaiter(void 0, void 0, void 0, function* () {
return new Promise((resolve) => {
setTimeout(() => {
resolve({});
}, seconds * 1000);
});
});
//# sourceMappingURL=test-wallet.js.map