UNPKG

@unspent/phi

Version:

a collection of anyone can spend contracts

424 lines (365 loc) 12.5 kB
import { binToHex, hexToBin, cashAddressToLockingBytecode, lockingBytecodeToCashAddress, } from "@bitauth/libauth"; import type { Artifact, Utxo, NetworkProvider } from "cashscript"; import type { UtxPhiIface, ContractOptions } from "../../common/interface.js"; 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 implements UtxPhiIface { public static c: string = "A"; //A private static fn: string = "execute"; public static minAllowance: bigint = DUST_UTXO_THRESHOLD + 222n + 40n; public recipientLockingBytecode: Uint8Array; constructor( public period: bigint | number = 4000n, public recipientAddress: any, public installment: bigint | number, public executorAllowance: bigint | number = 800n, public options: ContractOptions = DefaultOptions ) { let script: Artifact; 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.recipientLockingBytecode = lock.bytecode; if (SPECIALS.includes(binToHex(lock.bytecode))) throw Error("Contract is too special") this.options = options; } refresh(): void { this._refresh([ BigInt(this.period), this.recipientLockingBytecode, BigInt(this.installment), BigInt(this.executorAllowance), ]); } static fromString(str: string, network = "mainnet"): Annuity { 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: Uint8Array | string, network = "mainnet" ): Annuity { 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: Uint8Array | string, network = "mainnet" ): bigint { const p = this.parseOpReturn(opReturn, network); return binToBigInt(p.args.pop()!); } static async getSpendableBalance( opReturn: Uint8Array | string, network = "mainnet", networkProvider: NetworkProvider, blockHeight: number ): Promise<bigint> { 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; } } override toString() { return [ `${Annuity.c}`, `${this.options!.version!}`, `${this.period}`, `${deriveLockingBytecodeHex(this.recipientAddress)}`, `${this.installment}`, `${this.executorAllowance}`, `${this.getLockingBytecode()}`, ].join(Annuity.delimiter); } override asText() { return `Annuity paying ${this.installment} (sat), every ${this.period} blocks, after a ${this.executorAllowance} (sat) executor allowance`; } override asCommand(): string { 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): string | Uint8Array { 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(): boolean { let out = this.getOutputLockingBytecodes(true).pop()! as string; return SPECIALS.includes(out) } async asSeries(): Promise<any> { const currentHeight = await this.provider!.getBlockHeight(); const currentTime = Math.floor(Date.now() / 1000); let utxos = await this.getUtxos(); let series: any = []; 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?: string, fee?: bigint, utxos?: Utxo[], debug?: boolean ): Promise<string> { 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: any = []; 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; } }