@unspent/phi
Version:
a collection of anyone can spend contracts
223 lines (188 loc) • 6.55 kB
text/typescript
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;
}
}