UNPKG

@unspent/phi

Version:

a collection of anyone can spend contracts

333 lines 13.6 kB
import { binToHex, hexToBin, cashAddressToLockingBytecode, lockingBytecodeToCashAddress, } from "@bitauth/libauth"; import { DefaultOptions, DUST_UTXO_THRESHOLD, SPECIALS } from "../../common/constant.js"; import { BaseUtxPhiContract } from "../../common/contract.js"; import { getPrefixFromNetwork, deriveLockingBytecodeHex, binToNumber, toHex, parseBigInt, sum, binToBigInt, } from "../../common/util.js"; import { artifact as v1 } from "./cash/v1.js"; import { artifact as v2 } from "./cash/v2.js"; export class Annuity extends BaseUtxPhiContract { constructor(period = 4000n, recipientAddress, installment, executorAllowance = 800n, options = DefaultOptions) { let script; if (options.version === 2) { script = v2; if (installment < DUST_UTXO_THRESHOLD) throw Error("Installment below dust threshold"); if (executorAllowance < Annuity.minAllowance) throw Error("Executor Allowance below usable threshold"); } else if (options.version === 1) { script = v1; } else { throw Error("Unrecognized Annuity Version"); } const lock = cashAddressToLockingBytecode(recipientAddress); if (typeof lock === "string") throw lock; super(options.network, script, [ BigInt(period), lock.bytecode, BigInt(installment), BigInt(executorAllowance), ]); this.period = period; this.recipientAddress = recipientAddress; this.installment = installment; this.executorAllowance = executorAllowance; this.options = options; this.recipientLockingBytecode = lock.bytecode; if (SPECIALS.includes(binToHex(lock.bytecode))) throw Error("Contract is too special"); this.options = options; } refresh() { this._refresh([ BigInt(this.period), this.recipientLockingBytecode, BigInt(this.installment), BigInt(this.executorAllowance), ]); } static fromString(str, network = "mainnet") { const p = this.parseSerializedString(str, network); // if the contract shortcode doesn't match, error if (!(Annuity.c == p.code)) throw "non-faucet serialized string passed to faucet constructor"; if (![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 = parseBigInt(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 installment = parseBigInt(p.args.shift()); const executorAllowance = parseBigInt(p.args.shift()); const annuity = new Annuity(period, CashAddrResult.address, installment, executorAllowance, p.options); // check that the address is the same annuity.checkLockingBytecode(p.lockingBytecode); return annuity; } // Create a Annuity 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 (![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); let [installment, executorAllowance] = [30000n, 3000n]; installment = binToBigInt(p.args.shift()); executorAllowance = binToBigInt(p.args.shift()); const annuity = new Annuity(period, CashAddrResult.address, installment, executorAllowance, p.options); if (annuity.isSpecial()) throw Error("Contract is too special"); // check that the address is the same annuity.checkLockingBytecode(p.lockingBytecode); return annuity; } static getExecutorAllowance(opReturn, network = "mainnet") { const p = this.parseOpReturn(opReturn, network); return binToBigInt(p.args.pop()); } static async getSpendableBalance(opReturn, network = "mainnet", networkProvider, blockHeight) { const p = this.parseOpReturn(opReturn, network); const period = binToNumber(p.args.shift()); // discard the address p.args.shift(); const installment = binToNumber(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 0; } } else { return 0; } }); if (spendableUtxos.length > 0) { const spendableBalance = BigInt(spendableUtxos.reduce(sum)); const remainder = spendableBalance % BigInt(installment); const spendable = spendableBalance - BigInt(remainder); return spendable > 0n ? spendable : 0n; } else { return 0n; } } toString() { return [ `${Annuity.c}`, `${this.options.version}`, `${this.period}`, `${deriveLockingBytecodeHex(this.recipientAddress)}`, `${this.installment}`, `${this.executorAllowance}`, `${this.getLockingBytecode()}`, ].join(Annuity.delimiter); } asText() { return `Annuity paying ${this.installment} (sat), every ${this.period} blocks, after a ${this.executorAllowance} (sat) executor allowance`; } asCommand() { let chipnetFlag = this.options.network == 'mainnet' ? "" : "--chipnet "; return `unspent annuity --version ${this.options.version} ${chipnetFlag} --address ${this.recipientAddress} --period ${this.period} --allowance ${this.executorAllowance} --installment ${this.installment}`; } toOpReturn(hex = false) { const chunks = [ Annuity._PROTOCOL_ID, Annuity.c, toHex(this.options.version), toHex(this.period), "0x" + deriveLockingBytecodeHex(this.recipientAddress), toHex(this.installment), toHex(this.executorAllowance), "0x" + this.getLockingBytecode(true), ]; 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 = 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) { let blocksToWait = 0; // @ts-ignore if (utxo.height == 0) { blocksToWait = Number(this.period); } else { // @ts-ignore blocksToWait = Number(this.period) - (currentHeight - utxo.height); } const seriesStartTime = currentTime + blocksToWait * 600; const initialPrincipal = utxo.satoshis; const seriesLength = (initialPrincipal - DUST_UTXO_THRESHOLD) / (BigInt(this.installment) + BigInt(this.executorAllowance)); const principal = []; const time = []; const totalFee = []; const totalPayout = []; const installment = BigInt(this.installment) + BigInt(this.executorAllowance); const intervalSeconds = Number(this.period) * 600; for (var i = 0; i < seriesLength; i++) { if (installment > 1000n) { time.push(Number(seriesStartTime + i * intervalSeconds)); principal.push(Number(initialPrincipal) - Number(installment) * i); totalPayout.push(Number(this.installment) * i); totalFee.push(Number(this.executorAllowance) * i); } else { time.push(Number(seriesStartTime + i * intervalSeconds)); principal.push(0); totalPayout.push(Number(initialPrincipal) - Number(installment) * i); totalFee.push(Number(this.executorAllowance) * i); break; } } const utxoId = `${utxo.txid}:${utxo.vout.toString()}`; series.push({ id: utxoId, data: { time: time, principal: principal, payout: totalPayout, executorAllowance: totalFee, }, }); } // for utxos } // if utxos 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(Annuity.fn); let newPrincipal = 0n; let to = []; if (balance < this.installment) { throw Error("Funds selected below installment amount"); } else if (balance < (BigInt(Number(this.installment) * 2))) { console.log("liquidating annuity;"); newPrincipal = balance - BigInt(Number(this.executorAllowance) - 100); } else { newPrincipal = balance - (BigInt(this.installment) + BigInt(this.executorAllowance)); } to.push({ to: this.recipientAddress, amount: BigInt(this.installment), }); if (this.options.version == 1 || balance > BigInt(this.installment) * 2n) { to.push({ to: this.getAddress(), amount: newPrincipal, }); } else if (this.options.version == 2 && balance < BigInt(this.installment) * 2n) { to.pop(); to.push({ to: this.recipientAddress, amount: newPrincipal, }); } let estimator = fn(); let tx = fn(); if (utxos) tx = tx.from(utxos); if (utxos) estimator = estimator.from(utxos); if (exAddress) to.push({ to: exAddress, amount: 577n, }); const size = await estimator .to(to) .withAge(Number(this.period)) .withoutChange() .build(); const minerFee = fee ? BigInt(fee) : BigInt(size.length) / 2n + 2n; const executorFee = balance - (BigInt(this.installment) + BigInt(newPrincipal) + minerFee) - 4n; if (exAddress) { to.pop(); if (executorFee > 577n) to.push({ to: exAddress, amount: executorFee, }); } tx .to(to) .withAge(Number(this.period)). withoutChange(); let txn = ""; if (debug) { txn = await tx.bitauthUri(); } else { txn = (await tx.send()).txid; } return txn; } } Annuity.c = "A"; //A Annuity.fn = "execute"; Annuity.minAllowance = DUST_UTXO_THRESHOLD + 222n + 40n; //# sourceMappingURL=Annuity.js.map