@unspent/phi
Version:
a collection of anyone can spend contracts
308 lines (269 loc) • 8.59 kB
text/typescript
import {
binToHex,
hexToBin,
bigIntToBinUintLE,
instantiateSha256,
} from "@bitauth/libauth";
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 {
toHex,
getRandomIntWeak,
sum,
sumNumber,
decodeNullDataScript,
binToBigInt,
} from "../../common/util.js";
import { artifact as v1 } from "./cash/v1.js";
export class Mine extends BaseUtxPhiContract implements UtxPhiIface {
public static c: string = "M";
private static fn: string = "execute";
public static minPayout: bigint = DUST_UTXO_THRESHOLD + 392n + 10n;
constructor(
public period: bigint | number = 1n,
public payout: bigint | number = 5000n,
public difficulty: bigint | number = 3n,
public canary: string = binToHex(new Uint8Array(7)),
public options: ContractOptions = DefaultOptions
) {
let script: Artifact;
if (options.version === 1) {
script = v1;
} else {
throw Error(`Unrecognized Mine Contract Version`);
}
if (payout < Mine.minPayout)
throw Error(`Payout below minimum usable level ${Mine.minPayout}`);
super(options.network!, script, [
BigInt(period),
BigInt(payout),
BigInt(difficulty),
hexToBin(canary),
]);
this.options = options;
}
static fromString(str: string, network = "mainnet"): Mine {
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 (p.options.version != 1)
throw Error(`${this.name} contract version not recognized`);
if (p.args.length != 4)
throw `invalid number of arguments ${p.args.length}`;
const period = BigInt(parseInt(p.args.shift()!));
const payout = BigInt(parseInt(p.args.shift()!));
const difficulty = BigInt(parseInt(p.args.shift()!));
const canary = p.args.shift()!;
const mine = new Mine(period, payout, difficulty, canary, p.options);
// check that the address is correct
mine.checkLockingBytecode(p.lockingBytecode);
return mine;
}
// Create a Mine contract from an OpReturn by building a serialized string.
static fromOpReturn(
opReturn: Uint8Array | string,
network = "mainnet"
): Mine {
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 (p.options.version !== 1)
throw Error(
`Wrong version code passed to ${this.name} class: ${p.options.version}`
);
const period = binToBigInt(p.args.shift()!);
const payout = binToBigInt(p.args.shift()!);
const difficulty = binToBigInt(p.args.shift()!);
const canary = binToHex(p.args.shift()!);
const mine = new Mine(period, payout, difficulty, canary, p.options);
// check that the address
mine.checkLockingBytecode(p.lockingBytecode);
return mine;
}
static async getSpendableBalance(
opReturn: Uint8Array | string,
network = "mainnet",
networkProvider: NetworkProvider,
blockHeight: number
): Promise<bigint> {
const p = this.parseOpReturn(opReturn, network);
const period = binToBigInt(p.args.shift()!);
const payout = binToBigInt(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);
return binToBigInt(p.args.at(1)!);
}
override toString() {
return [
`${Mine.c}`,
`${this.options!.version}`,
`${this.period}`,
`${this.payout}`,
`${this.difficulty}`,
`${this.canary}`,
`${this.getLockingBytecode()}`,
].join(Mine.delimiter);
}
override asText(): string {
return `A mineable contract, with difficulty ${this.difficulty}, paying ${this.payout} (sat), every ${this.period} blocks`;
}
toOpReturn(hex = false): string | Uint8Array {
const chunks = [
Mine._PROTOCOL_ID,
Mine.c,
toHex(this.options!.version!),
toHex(this.period),
toHex(this.payout),
toHex(this.difficulty),
`0x${this.canary}`,
"0x" + this.getLockingBytecode(true),
];
return this.asOpReturn(chunks, hex);
}
async getNonce(verbose = false): Promise<string> {
let nonce = new Uint8Array([]);
let result = new Uint8Array([]);
let mined = false;
let best = 9007199254740991;
const sha256 = await instantiateSha256();
if (verbose) console.log("mining...");
// keep mining 'til the number of zeros are reached
while (!mined) {
const nonceNumber = getRandomIntWeak(9007199254740991);
nonce = bigIntToBinUintLE(BigInt(nonceNumber));
const msg = new Uint8Array([
...hexToBin(this.getRedeemScriptHex()),
...nonce,
]);
result = sha256.hash(msg);
const newBest = result.slice(0, Number(this.difficulty)).reduce(sumNumber);
if (newBest <= best) {
best = newBest;
if (verbose) console.log(newBest, result.slice(0, Number(this.difficulty)));
}
if (result.slice(0, Number(this.difficulty)).reduce(sumNumber) === 0) mined = true;
}
// if the number is smaller than the space allowed, prepend it by adding zeros to the right
if (nonce.length < this.canary.length / 2) {
const zeros = this.canary.length / 2 - nonce.length;
nonce = new Uint8Array([...nonce, ...new Uint8Array(zeros)]);
}
const nonceHex = binToHex(nonce);
if (verbose) console.log("success: ", nonceHex);
return nonceHex;
}
getOutputLockingBytecodes(hex = true) {
hex;
return [];
}
isSpecial(): boolean {
return false;
}
async execute(
exAddress?: string,
fee?: bigint,
utxos?: Utxo[],
debug?: boolean,
nonce?: string | Uint8Array,
verbose = false
): Promise<string> {
const balance = await this.getBalance();
let fn = this.getFunction(Mine.fn)!;
const newPrincipal = balance - BigInt(this.payout);
const minerFee = fee ? fee : BigInt(400);
const reward = BigInt(this.payout) - minerFee;
if (!nonce) {
this.canary = await this.getNonce(verbose);
} else {
this.canary = typeof nonce === "string" ? nonce : binToHex(nonce);
}
const nextContract = new Mine(
this.period,
this.payout,
this.difficulty,
this.canary,
this.options
);
const opReturn = nextContract.toOpReturn(false);
const chunks = decodeNullDataScript(opReturn).map(
(c) => "0x" + binToHex(c)
);
const to = [
{
to: nextContract.getAddress(),
amount: BigInt(newPrincipal),
},
];
if (exAddress)
to.push({
to: exAddress,
amount: BigInt(reward),
});
const canaryHex = "0x" + this.canary;
fn = this.getFunction(Mine.fn)!;
let tx = fn(canaryHex)!;
if (utxos) tx = tx.from(utxos);
const size = await tx
.withOpReturn(chunks)
.to(to)
.withAge(Number(this.period))
.withHardcodedFee(minerFee)
.build();
if (exAddress) {
const minerFee = fee ? fee : BigInt(size.length) / 2n;
//console.log(minerFee)
const reward = BigInt(this.payout) - (minerFee + 10n);
to.pop();
to.push({
to: exAddress,
amount: reward,
});
}
// assure cluster is connected
// @ts-ignore
await this.provider?.connectCluster();
tx = fn(canaryHex)!;
if (utxos) tx = tx.from(utxos);
tx.withOpReturn(chunks)
.to(to)
.withAge(Number(this.period))
.withoutChange();
let txn = ""
if (debug) {
txn = await tx.bitauthUri();
} else {
txn = (await tx.send()).txid;
}
return txn;
}
}