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