UNPKG

@unspent/phi

Version:

a collection of anyone can spend contracts

223 lines (188 loc) 6.55 kB
import type { Artifact, Utxo, NetworkProvider } from "cashscript"; import type { ContractOptions } from "../../common/interface.js"; import { binToBigInt, decodeNullDataScript } from "../../common/util.js"; import { DefaultOptions, DUST_UTXO_THRESHOLD } from "../../common/constant.js"; import { BaseUtxPhiContract } from "../../common/contract.js"; import { artifact as v1 } from "./cash/v1.js"; import { artifact as v2 } from "./cash/v2.js"; import { hash160, sum, toHex, parseBigInt } from "../../common/util.js"; import { getDefaultElectrumProvider } from "../../common/network.js"; import { binToHex, hexToBin } from "@bitauth/libauth"; export class Record extends BaseUtxPhiContract { static c: string = "R"; private static fn: string = "execute"; public static minMaxFee: bigint = 310n; constructor( public maxFee: bigint | number = 850n, public index: bigint | number = 0n, public options: ContractOptions = DefaultOptions ) { let script: Artifact; if (options.version === 2) { script = v2; maxFee = 900n; } else if (options.version === 1) { script = v1; maxFee = 850n; } else { throw Error("Unrecognized Divide Contract Version"); } if (maxFee < Record.minMaxFee) throw Error( `Allowed fee < ${Record.minMaxFee} may result in unusable outputs` ); super(options.network!, script, [BigInt(maxFee), BigInt(index)]); this.provider = getDefaultElectrumProvider(options.network!); this.options = options; } static fromString(str: string, network = "mainnet"): Record { 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 (![1, 2].includes(p.options.version)) throw Error(`${this.name} contract version not recognized`); const maxFee = parseBigInt(p.args.shift()!); const index = parseBigInt(p.args.shift()!); const record = new Record(maxFee, index, p.options); // check that the address record.checkLockingBytecode(p.lockingBytecode); return record; } static async getSpendableBalance( opReturn: Uint8Array | string, network = "mainnet", networkProvider: NetworkProvider, blockHeight: number ): Promise<bigint> { const p = this.parseOpReturn(opReturn, network); blockHeight; 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 > DUST_UTXO_THRESHOLD) { return spendable; } else { return 0n; } } static getExecutorAllowance( opReturn: Uint8Array | string, network = "mainnet" ): bigint { opReturn; network; return 0n; } override toString() { return [ `${Record.c}`, `${this.options!.version}`, `${this.maxFee}`, `${this.index}`, `${this.getLockingBytecode()}`, ].join(Record.delimiter); } override asText(): string { return `Recording contract with up to ${this.maxFee} per broadcast, index ${this.index}`; } override asCommand(): string{ let chipnetFlag = this.options.network == 'mainnet' ? '': "--chipnet "; return `unspent record ${chipnetFlag} --version ${this.options.version} --maxFee ${this.maxFee} --index ${this.index} --contract <PAYLOAD>`; } toOpReturn(hex = false): string | Uint8Array { const chunks = [ Record._PROTOCOL_ID, Record.c, toHex(this.options!.version!), toHex(this.maxFee), toHex(this.index), "0x" + this.getLockingBytecode(true), ]; return this.asOpReturn(chunks, hex); } // Create a Record contract from an OpReturn by building a serialized string. static fromOpReturn( opReturn: Uint8Array | string, network = "mainnet" ): Record { 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}` ); let [maxFee, index]: [bigint?, bigint?] = [undefined, undefined]; if ([1, 2].includes(p.options.version)) { maxFee = binToBigInt(p.args.shift()!); index = binToBigInt(p.args.shift()!); } else { throw Error("Record contract version not recognized"); } const record = new Record(maxFee, index, p.options); // check that the address record.checkLockingBytecode(p.lockingBytecode); return record; } getOutputLockingBytecodes(hex = true) { hex; return []; } isSpecial(): boolean { return false; } async broadcast( opReturn?: Uint8Array | string, utxos?: Utxo[], debug?: boolean ): Promise<string> { // Don't attempt to broadcast from an unfunded contract if (!(await this.isFunded())) throw Error(`Record contract is not funded: ${this.getAddress()}`); opReturn = opReturn ? opReturn : this.toOpReturn(false); // .withOpReturn likes hex to be prefixed with 0x. const chunks = decodeNullDataScript(opReturn).map( (c) => "0x" + binToHex(c) ); // regardless of how many inputs, filter to one if more than two utxos are available if (!utxos || utxos.length == 0) { const allUtxos = await this.getUtxos(); if (allUtxos && allUtxos.length > 0) { utxos = [allUtxos[0]!]; }else{ console.log("No utxos found") } } if (typeof opReturn === "string") opReturn = hexToBin(opReturn); const checkHash = await hash160(opReturn); const fn = this.getFunction(Record.fn)!; let tx = fn(checkHash)!; let estimator = fn(checkHash)!; if (utxos && utxos.length == 1) { tx = tx.from(utxos); estimator = estimator.from(utxos); } else { console.log("Record from Utxos: ", utxos) throw ("Cannot broadcast from multiple inputs"); } const size = BigInt(( await estimator.withOpReturn(chunks).withHardcodedFee(669n).build() ).length); tx = tx .withOpReturn(chunks) .withHardcodedFee(size / 2n) .withMinChange(1000n); let txn = "" if (debug) { txn = await tx.bitauthUri(); } else { txn = (await tx.send()).txid; } return txn; } }