UNPKG

@unspent/phi

Version:

a collection of anyone can spend contracts

359 lines 14.9 kB
import { binToHex, cashAddressToLockingBytecode, hexToBin, lockingBytecodeToCashAddress, } from "@bitauth/libauth"; import { DefaultOptions, DUST_UTXO_THRESHOLD, SPECIALS } from "../../common/constant.js"; import { BaseUtxPhiContract } from "../../common/contract.js"; import { assurePkh, binToBigInt, deriveLockingBytecode, deriveLockingBytecodeHex, derivePublicKeyHash, getPrefixFromNetwork, sum, toHex, } from "../../common/util.js"; import { artifact as v0 } from "./cash/v0.js"; import { artifact as v1 } from "./cash/v1.js"; import { artifact as v2 } from "./cash/v2.js"; export class Perpetuity extends BaseUtxPhiContract { constructor(period = 4000n, address, executorAllowance, decay, options = DefaultOptions) { let script; let lock; if (options.version === 2) { script = v2; const lockingBytecode = cashAddressToLockingBytecode(address); if (typeof lockingBytecode === "string") throw lockingBytecode; executorAllowance = executorAllowance ? executorAllowance : 1500n; lock = lockingBytecode.bytecode; } else if (options.version === 1) { script = v1; const lockingBytecode = cashAddressToLockingBytecode(address); if (typeof lockingBytecode === "string") throw lockingBytecode; lock = lockingBytecode.bytecode; } else if (options.version === 0) { script = v0; assurePkh(address); const publicKeyHash = derivePublicKeyHash(address); lock = publicKeyHash; } else { throw Error("Unrecognized Perpetuity Version"); } if ((options.version >= 2) && (executorAllowance < Perpetuity.minAllowance)) throw Error(`Executor Allowance below usable threshold ${Perpetuity.minAllowance}`); super(options.network, script, [ BigInt(period), lock, BigInt(executorAllowance), BigInt(decay), ]); this.period = period; this.address = address; this.executorAllowance = executorAllowance; this.decay = decay; this.options = options; this.recipientLockingBytecode = deriveLockingBytecode(address); if (SPECIALS.includes(binToHex(lock))) throw Error("Contract is too special"); this.options = options; } refresh() { this._refresh([ BigInt(this.period), this.recipientLockingBytecode, BigInt(this.executorAllowance), BigInt(this.decay) ]); } static fromString(str, network = "mainnet") { const p = this.parseSerializedString(str, network); // if the contract shortcode doesn't match, error if (!(this.c == p.code)) throw `non-${this.name} serialized string passed to ${this.name} constructor`; if (![0, 1, 2].includes(p.options.version)) throw Error(`${this.name} contract version not recognized`); if (p.args.length != 4) throw `invalid number of arguments ${p.args.length}`; const period = BigInt(parseInt(p.args.shift())); const lock = p.args.shift(); const prefix = getPrefixFromNetwork(network); const CashAddrResult = lockingBytecodeToCashAddress({ prefix: prefix, bytecode: hexToBin(lock) }); if (typeof CashAddrResult === "string") throw Error("non-standard address" + CashAddrResult); const executorAllowance = BigInt(parseInt(p.args.shift())); const decay = BigInt(parseInt(p.args.shift())); const perpetuity = new Perpetuity(period, CashAddrResult.address, executorAllowance, decay, p.options); // check that the address matches perpetuity.checkLockingBytecode(p.lockingBytecode); return perpetuity; } // Create a Perpetuity contract from an OpReturn by building a serialized string. static fromOpReturn(opReturn, network = "mainnet") { const p = this.parseOpReturn(opReturn, network); // check code if (p.code !== this.c) throw Error(`Wrong short code passed to ${this.name} class: ${p.code}`); // version if (![0, 1, 2].includes(p.options.version)) throw Error(`Wrong version code passed to ${this.name} class: ${p.options.version}`); const period = binToBigInt(p.args.shift()); const lock = p.args.shift(); const prefix = getPrefixFromNetwork(network); const CashAddrResult = lockingBytecodeToCashAddress({ prefix: prefix, bytecode: lock }); if (typeof CashAddrResult === "string") throw Error("non-standard address" + CashAddrResult); const executorAllowance = binToBigInt(p.args.shift()); const decay = binToBigInt(p.args.shift()); const perpetuity = new Perpetuity(period, CashAddrResult.address, executorAllowance, decay, p.options); // check that the address matches perpetuity.checkLockingBytecode(p.lockingBytecode); return perpetuity; } static async getSpendableBalance(opReturn, network = "mainnet", networkProvider, blockHeight) { const p = this.parseOpReturn(opReturn, network); const period = binToBigInt(p.args.shift()); // discard the address p.args.shift(); const decay = binToBigInt(p.args.shift()); const utxos = await networkProvider.getUtxos(p.address); const spendableUtxos = utxos.map((u) => { // @ts-ignore if (u.height !== 0) { // @ts-ignore if (blockHeight - u.height > period) { return u.satoshis; } else { return 0n; } } else { return 0n; } }); if (spendableUtxos.length > 0) { const spendableBalance = spendableUtxos.reduce(sum); const dustLocked = decay * DUST_UTXO_THRESHOLD; const spendable = spendableBalance - dustLocked; return spendable > 0 ? spendable : 0n; } else { return 0n; } } static getExecutorAllowance(opReturn, network = "mainnet") { const p = this.parseOpReturn(opReturn, network); p.args.pop(); return binToBigInt(p.args.pop()); } toString() { return [ `${Perpetuity.c}`, `${this.options.version}`, `${this.period}`, `${deriveLockingBytecodeHex(this.address)}`, `${this.executorAllowance}`, `${this.decay}`, `${this.getLockingBytecode()}`, ].join(Perpetuity.delimiter); } asText() { return `Perpetuity to pay 1/${this.decay} the input, every ${this.period} blocks, after a ${this.executorAllowance} (sat) executor allowance`; } asCommand() { let chipnetFlag = this.options.network == 'mainnet' ? '' : "--chipnet "; return `unspent perpetuity ${chipnetFlag} --version ${this.options.version} --address ${this.address} --period ${this.period} --allowance ${this.executorAllowance} --decay ${this.decay}`; } toOpReturn(hex = false) { const chunks = [ Perpetuity._PROTOCOL_ID, Perpetuity.c, toHex(this.options.version), toHex(this.period), "0x" + deriveLockingBytecodeHex(this.address), toHex(this.executorAllowance), toHex(this.decay), "0x" + this.getLockingBytecode(), ]; return this.asOpReturn(chunks, hex); } getOutputLockingBytecodes(hex = true) { if (hex) { return [binToHex(this.recipientLockingBytecode)]; } else { return [this.recipientLockingBytecode]; } } isSpecial() { let out = this.getOutputLockingBytecodes(true).pop(); return SPECIALS.includes(out); } async asSeries() { const currentHeight = await this.provider.getBlockHeight(); const currentTime = Number(Math.floor(Date.now() / 1000)); let utxos = await this.getUtxos(); let series = []; if (!utxos || utxos?.length == 0) utxos = [ { satoshis: 10000000n, txid: "<example 10,000,000 (0.1 BCH) unspent output>", vout: 0, // @ts-ignore height: 0, }, ]; if (utxos) { for (const utxo of utxos) { const time = []; const payout = []; const installment = []; const principal = []; const allowance = []; let blocksToWait = 0; // @ts-ignore if (utxo.height <= 0) { blocksToWait = Number(this.period); } else { // @ts-ignore blocksToWait = Number(this.period) - Number(currentHeight - utxo.height); } const seriesStartTime = currentTime + blocksToWait * 600; installment.push((utxo.satoshis / BigInt(this.decay)) - BigInt(this.executorAllowance)); time.push(Number(seriesStartTime)); payout.push(Number(installment.at(-1))); principal.push(Number(utxo.satoshis - installment.at(-1))); allowance.push(Number(this.executorAllowance)); const intervalSeconds = Number(this.period) * 600; let nextPayout = 0; let lastPrincipal = principal.at(-1); for (let i = 1; i < 5000; i++) { lastPrincipal = Number(principal.at(-1)); nextPayout = lastPrincipal / Number(this.decay); if (nextPayout < Number(DUST_UTXO_THRESHOLD)) { break; } if (this.options.version && this.options.version >= 2) { if (nextPayout > 1000n) { time.push(Number(seriesStartTime + i * intervalSeconds)); installment.push(Number(nextPayout)); payout.push(payout.at(-1) + nextPayout); principal.push(Number(lastPrincipal - nextPayout - Number(this.executorAllowance))); allowance.push(Number(this.executorAllowance) * i); } else { time.push(Number(seriesStartTime + i * intervalSeconds)); installment.push(Number(lastPrincipal)); payout.push(payout.at(-1) + Number(lastPrincipal - Number(this.executorAllowance))); principal.push(0); allowance.push(Number(this.executorAllowance) * i); } } else { time.push(Number(seriesStartTime + i * intervalSeconds)); installment.push(Number(nextPayout)); payout.push(payout.at(-1) + nextPayout); principal.push(Number(lastPrincipal - nextPayout - Number(this.executorAllowance))); allowance.push(Number(this.executorAllowance) * i); } } const utxoId = `${utxo.txid}:${utxo.vout.toString()}`; series.push({ id: utxoId, data: { time: time, principal: principal, payout: payout, executorAllowance: allowance, }, }); } // for utxos } // if utxos console.log(series); return series; } async execute(exAddress, fee, utxos, debug) { let balance = 0n; // Filter to inputs of sufficient age if (!utxos) utxos = await this.getUtxos(Number(this.period)); // If the contract is version 2 or higher, restrict to one input. if (utxos) { if (this.options.version >= 2 && utxos.length > 1) utxos = utxos.slice(-1); } if (utxos && utxos?.length > 0) { balance = utxos.reduce((a, b) => a + b.satoshis, 0n); } else { balance = await this.getBalance(); } if (balance == 0n) { throw Error("No funds on contract"); } const fn = this.getFunction(Perpetuity.fn); let installment = (balance / BigInt(this.decay)); let newPrincipal = balance - installment - BigInt(this.executorAllowance); // round up installment += 1n; newPrincipal += 1n; let to = []; if (this.options.version == 2 && installment < 1000) { to = [ { to: this.address, amount: installment + newPrincipal, } ]; } else { to = [ { to: this.address, amount: installment, }, { to: this.getAddress(), amount: newPrincipal, }, ]; } let executorFee = DUST_UTXO_THRESHOLD; if (typeof exAddress === "string" && exAddress) to.push({ to: exAddress, amount: DUST_UTXO_THRESHOLD, }); let estimator = fn(); if (utxos) estimator = estimator.from(utxos); estimator .to(to) .withAge(Number(this.period)) .withoutChange(); const size = await estimator.build(); const minerFee = fee ? BigInt(fee) : BigInt(size.length) / 2n + 2n; if (exAddress) { executorFee = BigInt(this.executorAllowance) - minerFee; if (executorFee > DUST_UTXO_THRESHOLD) { to.pop(); to.push({ to: exAddress, amount: executorFee, }); } } let tx = fn(); if (utxos) tx = tx.from(utxos); tx.to(to).withAge(Number(this.period)).withoutChange(); let txn = ""; if (debug) { txn = await tx.bitauthUri(); } else { txn = (await tx.send()).txid; } return txn; } } Perpetuity.c = "P"; Perpetuity.fn = "execute"; Perpetuity.minAllowance = DUST_UTXO_THRESHOLD + 220n + 50n; //# sourceMappingURL=Perpetuity.js.map