UNPKG

@unspent/phi

Version:

a collection of anyone can spend contracts

228 lines 9.43 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 { deriveLockingBytecodeHex, getPrefixFromNetwork, parseBigInt, toHex, sum, binToBigInt, } from "../../common/util.js"; import { artifact as v1_2 } from "./cash/2.v1.js"; import { artifact as v1_3 } from "./cash/3.v1.js"; import { artifact as v1_4 } from "./cash/4.v1.js"; import { artifact as v2_2 } from "./cash/2.v2.js"; import { artifact as v2_3 } from "./cash/3.v2.js"; import { artifact as v2_4 } from "./cash/4.v2.js"; const scriptMap = [ [v1_2, v1_3, v1_4], [v2_2, v2_3, v2_4] ]; export class Divide extends BaseUtxPhiContract { constructor(executorAllowance = 1200n, payees, options = DefaultOptions) { let scriptFn; if ([1, 2].includes(options.version)) { scriptFn = scriptMap; } else { throw Error("Unrecognized Divide Contract Version"); } const usableThreshold = Divide.minAllowance + 66n * BigInt(payees.length); if (executorAllowance < usableThreshold) throw Error(`Executor Allowance below usable threshold (${usableThreshold}) for ${payees.length} addresses`); const divisor = BigInt(payees.length); if (!(divisor >= 2n && divisor <= 4n)) throw Error(`Divide contract range must be 2-4, ${divisor} out of range`); const script = scriptFn[options.version - 1][Number(divisor - 2n)]; const payeeLocks = [...payees].map((c) => { const lock = cashAddressToLockingBytecode(c); if (typeof lock === "string") throw lock; return lock.bytecode; }); super(options.network, script, [ BigInt(executorAllowance), divisor, ...payeeLocks, ]); this.executorAllowance = executorAllowance; this.payees = payees; this.options = options; this.payeeLocks = payeeLocks; this.divisor = divisor; this.options = options; } refresh() { this.payeeLocks = [...this.payees].map((c) => { const lock = cashAddressToLockingBytecode(c); if (typeof lock === "string") throw lock; return lock.bytecode; }); this._refresh([BigInt(this.executorAllowance), this.divisor, ...this.payeeLocks]); } static fromString(str, network = "mainnet") { const p = this.parseSerializedString(str, network); // if the contract shortcode doesn't match, error if (!(Divide.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`); const prefix = getPrefixFromNetwork(p.options.network); const executorAllowance = parseBigInt(p.args.shift()); const payees = p.args.map((lock) => { const cashAddrResponse = lockingBytecodeToCashAddress({ prefix: prefix, bytecode: hexToBin(lock) }); if (typeof cashAddrResponse === "string") throw Error("non-standard address" + cashAddrResponse); return cashAddrResponse.address; }); const divide = new Divide(executorAllowance, payees, p.options); if (divide.isSpecial()) throw Error("Contract is too special"); // check that the address divide.checkLockingBytecode(p.lockingBytecode); return divide; } // Create a Divide 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 prefix = getPrefixFromNetwork(p.options.network); const executorAllowance = binToBigInt(p.args.shift()); const payeesLocks = p.args; const payees = payeesLocks.map((lock) => { const CashAddrResult = lockingBytecodeToCashAddress({ prefix: prefix, bytecode: lock }); if (typeof CashAddrResult === "string") throw Error("non-standard address: " + CashAddrResult); return CashAddrResult.address; }); const divide = new Divide(executorAllowance, payees, p.options); // check that the address divide.checkLockingBytecode(p.lockingBytecode); return divide; } static getExecutorAllowance(opReturn, network = "mainnet") { const p = this.parseOpReturn(opReturn, network); return binToBigInt(p.args.shift()); } static async getSpendableBalance(opReturn, network = "mainnet", networkProvider, blockHeight) { const p = this.parseOpReturn(opReturn, network); blockHeight; const executorAllowance = binToBigInt(p.args.shift()); const utxos = await networkProvider.getUtxos(p.address); const spendableUtxos = utxos.map((u) => { return u.satoshis; }); const spendable = spendableUtxos.length > 0 ? spendableUtxos.reduce(sum) : 0n; if (spendable > BigInt(p.args.length) * DUST_UTXO_THRESHOLD + executorAllowance) { return spendable; } else { return 0n; } } toString() { const payees = this.payees .map((cashaddr) => deriveLockingBytecodeHex(cashaddr)) .join(Divide.delimiter); return [ `${Divide.c}`, `${this.options.version}`, `${this.executorAllowance}`, `${payees}`, `${this.getLockingBytecode()}`, ].join(Divide.delimiter); } asText() { return `A divide contract with executor allowance of ${this.executorAllowance}`; } asCommand() { let chipnetFlag = this.options.network == 'mainnet' ? "" : "--chipnet "; let addressList = this.payees.join(","); return `unspent divide --version ${this.options.version} ${chipnetFlag} --addresses ${addressList} --allowance ${this.executorAllowance}`; } toOpReturn(hex = false) { const chunks = [ Divide._PROTOCOL_ID, Divide.c, toHex(this.options.version), toHex(this.executorAllowance), ...this.payees.map((a) => "0x" + deriveLockingBytecodeHex(a)), "0x" + this.getLockingBytecode(true), ]; return this.asOpReturn(chunks, hex); } getOutputLockingBytecodes(hex = true) { if (hex) { return this.payeeLocks.map((b) => binToHex(b)); } else { return this.payeeLocks; } } isSpecial() { let out = this.getOutputLockingBytecodes(true); let a = new Set(SPECIALS); let b = new Set(out); let intersect = [...new Set([...a].filter(i => b.has(i)))]; return intersect.length > 0; } async execute(exAddress, fee, utxos, debug) { let balance = 0n; // Populate a list of utxos if (!utxos) utxos = await this.getUtxos(); // 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(Divide.fn); const distributedValue = balance - BigInt(this.executorAllowance); const divisor = BigInt(this.payees.length); const installment = distributedValue / divisor + 1n; if (installment < 546n) throw "Installment less than dust limit... bailing"; const to = []; for (let i = 0; i < divisor; i++) { to.push({ to: this.payees[i], amount: installment }); } if (exAddress) { to.push({ to: exAddress, amount: 577n, }); const size = await fn().to(to).withoutChange().build(); const feeEstimate = fee ? fee : BigInt(size.length) / 2n; to.pop(); const executorPayout = BigInt(this.executorAllowance) - (feeEstimate + 2n * divisor + 8n); if (executorPayout > 577n) to.push({ to: exAddress, amount: executorPayout, }); } let tx = fn(); tx.to(to).withoutChange(); let txn = ""; if (debug) { txn = await tx.bitauthUri(); } else { txn = (await tx.send()).txid; } return txn; } } Divide.c = "D"; Divide.fn = "execute"; Divide.minAllowance = 200n + DUST_UTXO_THRESHOLD + 7n; //# sourceMappingURL=Divide.js.map