unspent
Version:
A command line utility for Unspent Phi (₿∙ϕ) contracts
520 lines (457 loc) • 15.6 kB
text/typescript
// @ts-ignore
import packageJson from "../package.json" with { type: "json" };
import "fake-indexeddb/auto";
import { Cli, Command, Option } from "clipanion";
import {
Annuity,
Divide,
Drip,
Faucet,
Mine,
Perpetuity,
Record
} from "@unspent/phi";
import {
parseBigInt,
opReturnToSerializedString,
opReturnToBalance,
getDefaultElectrumProvider,
stringToInstance,
} from "@unspent/phi";
import {
getRecords
} from "@unspent/psi";
import { lockingBytecodeToCashAddress, hexToBin } from "@bitauth/libauth";
abstract class VersionedCommand extends Command {
version = Option.String("--version", "2", {
description: "The unspent/phi contract version",
});
}
abstract class NetworkCommand extends VersionedCommand {
isChipnet = Option.Boolean("--chipnet", false, {
description: "Use chipnet",
});
isRegtest = Option.Boolean("--regtest", false,
{
description: "Use a regtest network",
});
}
abstract class CustomFeeCommand extends NetworkCommand {
fee = Option.String("--fee", {
required: false,
description: "transaction fee override",
});
}
export class DripCommand extends NetworkCommand {
static override usage = Command.Usage({
category: `Drip-Mine`,
description: `An Miner Extractable Value (MEV) faucet`,
});
static override paths = [[`drip`], [`x`]];
async execute() {
let network = this.isChipnet
? "chipnet"
: this.isRegtest
? "regtest"
: "mainnet";
let version = 3
let a = new Drip(
{ version: version, network: network }
);
await a.info();
await a.execute();
}
}
export class AnnuityCommand extends CustomFeeCommand {
static override usage = Command.Usage({
category: `Beneficiary`,
description: `Regular payments over time`,
});
static override paths = [[`annuity`], [`a`]];
getAddress = Option.Boolean("--deposit", false, {
description: "give the deposit address for the contract and exit",
});
address = Option.String("--address", {
required: true,
description:
"receiving cash address to send coins to, i.e. beneficiary address",
});
period = Option.String("--period", {
required: false,
description:
"how often (in blocks) the contract pays (default: 4000, about monthly)",
});
allowance = Option.String("--allowance", {
required: false,
description:
"the executor's allowance for miner fees & administration (default: 3400 sats)",
});
installment = Option.String("--installment", {
required: true,
description: "amount to be paid each period",
});
executorAddress = Option.String("--exAddress", {
required: false,
description: "address for fee taken by executor for submitting transaction",
});
async execute() {
let network = this.isChipnet
? "chipnet"
: this.isRegtest
? "regtest"
: "mainnet";
const defaultPeriod = this.isChipnet ? 1n : 4000n;
let periodInt = !this.period ? defaultPeriod : parseBigInt(this.period);
let allowanceInt = !this.allowance ? 3400n : parseBigInt(this.allowance);
let installmentInt = parseBigInt(this.installment);
let feeOverride = !this.fee ? undefined : parseBigInt(this.fee);
let version = parseInt(this.version)
if (!this.address) {
let a = new Annuity(
periodInt,
this.address,
installmentInt,
allowanceInt,
{ version: version, network: network }
);
await a.info();
if (await a.isFunded()) a.execute(this.executorAddress, feeOverride);
} else {
let a = new Annuity(
periodInt,
this.address,
installmentInt,
allowanceInt,
{ version: version, network: network }
);
await a.info();
}
}
}
export class DivideCommand extends CustomFeeCommand {
static override usage = Command.Usage({
category: `Beneficiary`,
description: `Divide money into equal payments, up to four addresses`,
});
static override paths = [[`divide`], [`d`]];
allowance = Option.String("--allowance", {
required: false,
description:
"the executor's allowance for miner fees & administration (default: 1200 sats)",
});
addresses = Option.String("--addresses", {
required: true,
description:
"a comma separated list of addresses to receive equal payments",
});
executorAddress = Option.String("--exAddress", {
required: false,
description: "address for fee taken by executor for submitting transaction",
});
async execute() {
let network = this.isChipnet
? "chipnet"
: this.isRegtest
? "regtest"
: "mainnet";
let allowanceInt = !this.allowance ? 1200n : parseBigInt(this.allowance);
let addresses = this.addresses.split(",");
let feeOverride = !this.fee ? undefined : parseBigInt(this.fee);
let version = parseInt(this.version)
let divide = new Divide(allowanceInt, addresses, {
version: version,
network: network,
});
if ((await divide.getBalance()) > addresses.length * 550) {
let response = await divide.execute(this.executorAddress, feeOverride);
console.log(response);
} else {
divide.info();
}
}
}
export class FaucetCommand extends CustomFeeCommand {
static override paths = [[`faucet`], [`f`]];
static override usage = Command.Usage({
category: `Distributive`,
description: `Distributes some free bitcoin per period`,
});
address = Option.String("--address", {
required: false,
description: "receiving address to send coins to, i.e. your address",
});
period = Option.String("--period", {
required: false,
description: "how often (in blocks) the contract can pay",
});
payout = Option.String("--payout", {
required: false,
description: "how much the contract pays (satoshi)",
});
index = Option.String("--index", {
required: false,
description: "a nonce to force uniqueness with identical parameters",
});
async execute() {
let network = this.isChipnet
? "chipnet"
: this.isRegtest
? "regtest"
: "mainnet";
let periodInt = !this.period ? 1n : parseBigInt(this.period);
let payoutInt = !this.payout ? 1000n : parseBigInt(this.payout);
let indexInt = !this.index ? 1n : parseBigInt(this.index);
let feeOverride = !this.fee ? undefined : parseBigInt(this.fee);
let version = parseInt(this.version)
if (this.address) {
let faucet = new Faucet(periodInt, payoutInt, indexInt, {
version: version,
network: network,
});
await faucet.info();
if (await faucet.isFunded()) await faucet.execute(this.address, feeOverride);
} else {
let faucet = await new Faucet(periodInt, payoutInt, indexInt, {
version: version,
network: network,
});
await faucet.info();
}
}
}
export class MineCommand extends CustomFeeCommand {
static override usage = Command.Usage({
category: `Distributive`,
description: `Distributes some bitcoin per period, for proof of work`,
});
static override paths = [[`mine`], [`m`]];
getAddress = Option.Boolean("--deposit", false, {
description: "give the deposit address for the contract and exit",
});
period = Option.String("--period", {
required: false,
description:
"how often (in blocks) the contract pays (default: 4383, about monthly)",
});
payout = Option.String("--payout", {
required: false,
description: "how much the contract pays (satoshi)",
});
difficulty = Option.String("--difficulty", {
required: false,
description: "Number of zeros required to 'win' block reward",
});
canary = Option.String("--canary", {
required: false,
description: "random value (hex) the contract was made with",
});
executorAddress = Option.String("--exAddress", {
required: true,
description: "address for reward",
});
async execute() {
let network = this.isChipnet
? "chipnet"
: this.isRegtest
? "regtest"
: "mainnet";
const defaultPeriod = this.isChipnet ? 1 : 4000;
let periodInt = !this.period ? defaultPeriod : parseBigInt(this.period);
let payoutInt = !this.payout ? 1000 : parseBigInt(this.payout);
let difficultyInt = !this.difficulty ? 3 : parseBigInt(this.difficulty);
let canaryHex = this.canary;
let feeOverride = !this.fee ? undefined : parseBigInt(this.fee);
let version = parseInt(this.version)
let m = new Mine(periodInt, payoutInt, difficultyInt, canaryHex, {
version: version,
network: network,
});
await m.execute(this.executorAddress, feeOverride);
}
}
export class PerpetuityCommand extends CustomFeeCommand {
static override usage = Command.Usage({
category: `Beneficiary`,
description: `Pay a fixed fraction of total value at intervals.`,
});
static override paths = [[`perpetuity`], [`p`]];
getAddress = Option.Boolean("--deposit", false, {
description: "give the deposit address for the contract and exit",
});
address = Option.String("--address", {
required: true,
description:
"receiving cash address to send coins to, i.e. beneficiary address",
});
period = Option.String("--period", {
required: false,
description:
"how often (in blocks) the contract pays (default: 4000, about monthly)",
});
allowance = Option.String("--allowance", {
required: false,
description:
"the executor's allowance for miner fees & administration (default: 3400 sats)",
});
decay = Option.String("--decay", {
required: false,
description:
"the divisor for the fraction taking in each installment (default: 120, i.e. 1/120 or 0.83% each installment)",
});
executorAddress = Option.String("--exAddress", {
required: false,
description: "address for fee taken by executor for submitting transaction",
});
async execute() {
let network = this.isChipnet
? "chipnet"
: this.isRegtest
? "regtest"
: "mainnet";
const defaultPeriod = this.isChipnet ? 0 : 4383;
const defaultDecay = this.isChipnet ? 8 : 96;
let periodInt = !this.period ? defaultPeriod : parseBigInt(this.period);
let allowanceInt = !this.allowance ? 1500 : parseBigInt(this.allowance);
let decayInt = !this.decay ? defaultDecay : parseBigInt(this.decay);
let feeOverride = !this.fee ? undefined : parseBigInt(this.fee);
let version = parseInt(this.version)
if (!this.getAddress) {
let perpetuity = new Perpetuity(
periodInt,
this.address,
allowanceInt,
decayInt,
{ version: version, network: network }
);
await perpetuity.info();
if (await perpetuity.isFunded()) perpetuity.execute(this.executorAddress, feeOverride);
} else {
let perpetuity = new Perpetuity(
periodInt,
this.address,
allowanceInt,
decayInt,
{ version: version, network: network }
);
await perpetuity.info();
}
}
}
export class QueryCommand extends NetworkCommand {
static override usage = Command.Usage({
category: `Informational`,
description: `Query a list of contracts.`,
});
static override paths = [[`query`], [`q`]];
network = this.isChipnet ? "chipnet" : this.isRegtest ? "regtest" : "mainnet";
chaingraph = Option.String("--chaingraph", {
required: false,
description: "A chaingraph service to query",
});
prefix = Option.String("--prefix", {
required: false,
description: "The contract prefix in hex",
});
offset = Option.String("--offset", {
required: false,
description: "starting index of records returned",
});
limit = Option.String("--limit", {
required: false,
description: "The maximum number of records returned, (25 default)",
});
async execute() {
let chaingraph = this.chaingraph
? this.chaingraph
: "https://demo.chaingraph.cash/v1/graphql";
let prefix = this.prefix ? this.prefix : "6a047574786f";
let node = this.isChipnet ? "chipnet" : this.isRegtest ? "rbchn" : "mainnet";
let networkProvider = getDefaultElectrumProvider(node)
let limit = !this.limit ? undefined : parseInt(this.limit);
let offset = !this.offset ? undefined : parseInt(this.offset);
let exclude = "6a047574786f014d0101"
let hexRecords = await getRecords(chaingraph, prefix, node, limit, offset, exclude);
let contracts = [];
let total = 0n;
for (let record of hexRecords) {
try {
let instance = opReturnToSerializedString(record, this.network);
if (instance) contracts.push(instance.toString());
//@ts-ignore
let subTotal = await opReturnToBalance(record, this.network, networkProvider)
if (instance) {
let prefix = this.isChipnet ? 'bchtest' : 'bitcoincash' as "bchtest" | "bitcoincash" | "bchreg" | undefined
let lockingBytecode = instance.split(",").pop() as string
let contractAddr = lockingBytecodeToCashAddress(hexToBin(lockingBytecode), prefix)
console.log(Number(subTotal), instance, contractAddr)
}
total += BigInt(subTotal);
} catch (e) {
//console.log(e)
// anyone can post an OP_RETURN that doesn't parse
//console.log('couldn\'t parse: ', record)
}
}
console.log("sum: ", total.toLocaleString())
console.log(`Built ${contracts.length} contracts`);
}
}
export class RecordCommand extends CustomFeeCommand {
static override usage = Command.Usage({
category: `Informational`,
description: `Broadcast a contract to the blockchain`,
});
static override paths = [[`record`], [`r`]];
maxFee = Option.String("--maxFee", {
required: false,
description: "transaction miner fee available to publish contracts",
});
index = Option.String("--index", {
required: false,
description: "a nonce to force uniqueness with identical parameters",
});
contract = Option.String("--contract", {
required: false,
description: "a serialized contract to publish",
});
selfPublish = Option.Boolean("--selfPublish", {
required: false,
description: "Whether or not to self publish the contract if funded",
});
network = this.isChipnet ? "chipnet" : this.isRegtest ? "regtest" : "mainnet";
async execute() {
let network = this.isChipnet
? "chipnet"
: this.isRegtest
? "regtest"
: "mainnet";
let maxFeeInt = !this.maxFee ? undefined : parseBigInt(this.maxFee);
let indexInt = !this.index ? undefined : parseBigInt(this.index);
let version = parseInt(this.version)
if (!this.contract) {
//console.log("no contract specified");
let r = new Record(maxFeeInt, indexInt, { version: version, network: network });
if (await r.isFunded()) {
await r.info();
if (this.selfPublish) await r.broadcast();
} else {
await r.info();
}
} else {
let r = new Record(maxFeeInt, indexInt, { version: version, network: network });
let i = stringToInstance(this.contract, network);
if (!i) throw Error(`Couldn't parse string ${this.contract}`);
console.log("broadcasting... ");
let tx = await r.broadcast(i.toOpReturn());
console.log(tx);
}
}
}
const cli = new Cli({
binaryName: "unspent",
binaryLabel: "@unspent/cli",
binaryVersion: packageJson.version,
enableColors: true,
enableCapture: true
});
export { cli };