UNPKG

@unspent/phi

Version:

a collection of anyone can spend contracts

257 lines (218 loc) 7.24 kB
import type { Artifact, Utxo, NetworkProvider } from "cashscript"; import type { UtxPhiIface, ContractOptions } from "../../common/interface.js"; import { DefaultOptions, DUST_UTXO_THRESHOLD } from "../../common/constant.js"; import { BaseUtxPhiContract } from "../../common/contract.js"; import { binToNumber, sum, toHex, parseBigInt, binToBigInt } 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 Faucet extends BaseUtxPhiContract implements UtxPhiIface { public static c: string = "F"; private static fn: string = "drip"; public static minPayout: bigint = 158n + DUST_UTXO_THRESHOLD + 10n; constructor( public period: bigint | number = 1n, public payout: bigint | number = 1000n, public index: bigint | number = 1n, public options: ContractOptions = DefaultOptions ) { let script: Artifact; if (options.version === 2) { script = v2; if (payout < Faucet.minPayout) throw Error("Payout below dust threshold"); } else if (options.version === 1) { script = v1; } else if (options.version === 0) { script = v0; } else { throw Error("Unrecognized Faucet Version"); } super(options.network!, script, [BigInt(period), BigInt(payout), BigInt(index)]); this.options = options; } refresh(): void { this._refresh([BigInt(this.period), BigInt(this.payout), BigInt(this.index)]); } static fromString(str: string, network = "mainnet"): Faucet { const p = this.parseSerializedString(str, network); // if the contract shortcode doesn't match, error if (!(Faucet.c == p.code)) throw "non-faucet serialized string passed to faucet constructor"; if (![0, 1, 2].includes(p.options.version)) throw Error("faucet contract version not recognized"); if (p.args.length != 3) throw `invalid number of arguments ${p.args.length}`; const [period, payout, index] = [...p.args.map((i) => parseBigInt(i))]; const faucet = new Faucet(period, payout, index, p.options); faucet.checkLockingBytecode(p.lockingBytecode); return faucet; } // Create a Faucet contract from an OpReturn by building a serialized string. static fromOpReturn( opReturn: Uint8Array | string, network = "mainnet" ): Faucet { 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}` ); // parse arguments if (p.args.length != 3) throw `invalid number of arguments ${p.args.length}`; const [period, payout, index] = [ ...p.args.map((i) => binToBigInt(i)), ]; const faucet = new Faucet(period, payout, index, p.options); faucet.checkLockingBytecode(p.lockingBytecode); return faucet; } 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()!); const payout = binToNumber(p.args.shift()!); const utxos = await networkProvider.getUtxos(p.address); const spendableUtxos = utxos.map((u: Utxo) => { // @ts-ignore if (u.height !== 0) { // @ts-ignore if (blockHeight - u.height > period) { return u.satoshis; } else { return 0n; } } else { return 0n; } }); const spendable = spendableUtxos.length > 0 ? spendableUtxos.reduce(sum) : 0n; if (spendable > payout) { return spendable; } else { return 0n; } } static getExecutorAllowance( opReturn: Uint8Array | string, network = "mainnet" ): bigint { const p = this.parseOpReturn(opReturn, network); // pop the index to get to the payout p.args.pop()!; return binToBigInt(p.args.pop()!); } override toString() { return [ `${Faucet.c}`, `${this.options!.version}`, `${this.period}`, `${this.payout}`, `${this.index}`, `${this.getLockingBytecode()}`, ].join(Faucet.delimiter); } override asText() { return `A faucet paying ${this.payout} (sat), every ${this.period} blocks`; } override asCommand(): string{ let chipnetFlag = this.options.network == 'mainnet' ? '': "--chipnet "; return `unspent faucet ${chipnetFlag} --version ${this.options.version} --address $CASHADDR --period ${this.period} --payout ${this.payout} --index ${this.index}`; } toOpReturn(hex = false): string | Uint8Array { const chunks = [ Faucet._PROTOCOL_ID, Faucet.c, toHex(this.options!.version!), toHex(this.period), toHex(this.payout), toHex(this.index), "0x" + this.getLockingBytecode(true), ]; return this.asOpReturn(chunks, hex); } getOutputLockingBytecodes(hex = true) { hex; return []; } isSpecial(): boolean { return false; } 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(Faucet.fn)!; let tx = fn(); if (utxos) tx = tx.from(utxos); const newPrincipal = balance - BigInt(this.payout); const minerFee = fee ? fee : 253n; let sendAmount = BigInt(this.payout) - minerFee; const to = [] // if enough remains for an additional payout if (balance > this.payout) to.push({ to: this.getAddress(), amount: newPrincipal, }); if (exAddress) to.push({ to: exAddress, amount: 577n, }); const size = await tx.to(to).withAge(Number(this.period)).withoutChange().build(); if (exAddress) { if(balance < this.payout){ sendAmount = balance; }else{ sendAmount = BigInt(this.payout); } const minerFee = fee ? fee : BigInt(size.length) / 2n; sendAmount -= (minerFee + 10n) // remove the old executor amount // replace with new fee to.pop(); to.push({ to: exAddress, amount: sendAmount, }); } 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; } }