@unspent/phi
Version:
a collection of anyone can spend contracts
314 lines (264 loc) • 8.54 kB
text/typescript
import {
binToHex,
hexToBin,
lockingBytecodeToBase58Address,
lockingBytecodeToCashAddress,
} from "@bitauth/libauth";
import {
ConstructorArgument as Argument,
Artifact,
Contract as CashScriptContract,
Utxo,
NetworkProvider
}
from "cashscript";
import { getDefaultProvider } from "./network.js";
import {
binToNumber,
createOpReturnData,
decodeNullDataScript,
deriveLockingBytecode,
deriveLockingBytecodeHex,
getPrefixFromNetwork,
sum
} from "./util.js";
import { DELIMITER, PROTOCOL_ID, _PROTOCOL_ID } from "./constant.js";
import { ParsedContractI } from "./interface.js"
import { ContractOptions } from "cashscript/dist/interfaces.js";
export class BaseUtxPhiContract {
private artifact: Artifact;
private contract: CashScriptContract;
protected testnet: boolean;
public provider?: NetworkProvider;
public addressType: ContractOptions["addressType"]
public static delimiter: string = DELIMITER;
public static _PROTOCOL_ID: string = _PROTOCOL_ID;
public static PROTOCOL_ID: string = PROTOCOL_ID;
constructor(
network: string,
artifact: Artifact,
constructorArguments: Argument[]
) {
const defaultProvider = getDefaultProvider(network);
this.provider = defaultProvider as NetworkProvider;
this.testnet = this.provider.network == "mainnet" ? false : true;
this.artifact = artifact;
this.addressType = this.artifact.compiler.version.startsWith("0.7") ? 'p2sh20' : 'p2sh32'
this.contract = new CashScriptContract(
artifact,
[...constructorArguments],
{
provider: this.provider,
addressType: this.addressType
}
);
}
_refresh(constructorArguments: Argument[]) {
this.contract = new CashScriptContract(
this.artifact,
[...constructorArguments],
{
provider: this.provider,
addressType: this.addressType
}
);
}
static parseSerializedString(str: string, network = "mainnet") {
const components = str.split(this.delimiter);
// if the contract shortcode doesn't match, error
const code = components.shift();
const version = parseInt(components.shift()!);
const lockingBytecode = components.splice(-1)[0]!;
const args = [...components];
const options = { version: version, network: network };
const prefix = getPrefixFromNetwork(network);
const CashAddrResult = lockingBytecodeToCashAddress({
prefix:prefix,
bytecode:hexToBin(lockingBytecode!)
});
if (typeof CashAddrResult === "string")
throw Error("non-standard address" + CashAddrResult);
return {
code: code,
options: options,
args: args,
lockingBytecode: lockingBytecode,
address: CashAddrResult.address,
};
}
static parseOpReturn(opReturn: Uint8Array | string, network = "mainnet"): ParsedContractI {
// transform to binary
if (typeof opReturn == "string") {
opReturn = hexToBin(opReturn);
}
// decode data
const components = decodeNullDataScript(opReturn);
const protocol = binToHex(components.shift()!);
if (protocol !== PROTOCOL_ID)
throw Error(
`Protocol specified in OpReturn didn't match the PROTOCOL_ID: ${protocol} v ${PROTOCOL_ID}`
);
// if the contract shortcode doesn't match, error
const code = String.fromCharCode(components.shift()![0]!);
const version = binToNumber(components.shift()!);
const lockingBytecode = components.splice(-1)[0]!;
const args = [...components];
const options = { version: version, network: network };
const prefix = getPrefixFromNetwork(network);
const CashAddrResult = lockingBytecodeToCashAddress({prefix:prefix, bytecode: lockingBytecode!});
if (typeof CashAddrResult === "string")
throw Error("non-standard address:" + CashAddrResult);
return {
code: code,
options: options,
args: args,
lockingBytecode: lockingBytecode,
address: CashAddrResult.address,
};
}
static parseOutputs(opReturn: Uint8Array | string): Uint8Array[] {
// transform to binary
if (typeof opReturn == "string") {
opReturn = hexToBin(opReturn);
}
// decode data
const components = decodeNullDataScript(opReturn);
binToHex(components.shift()!);
// if the contract shortcode doesn't match, error
String.fromCharCode(components.shift()![0]!);
binToNumber(components.shift()!);
const lockingBytecode = components.splice(-1)[0]!;
const args = [...components].filter(b => b.length == 23 || b.length == 25);
return [...args, lockingBytecode];
}
// @ts-ignore
// static async getSpendable(opReturn: Uint8Array | string, network = "mainnet", networkProvider: NetworkProvider, blockHeight?: number): Promise<number> {
// throw Error("Cannot get spendable amount from base class");
// }
static getExecutorAllowance(
opReturn: Uint8Array | string,
network = "mainnet"
): bigint {
throw Error(
`Cannot get executor allowance from base class, ${opReturn} on ${network}`
);
}
static async getSpendableBalance(
opReturn: Uint8Array | string,
network = "mainnet",
networkProvider?: NetworkProvider,
blockHeight?: number
): Promise<bigint> {
networkProvider;
blockHeight;
throw Error(
`Cannot get spendable amount from base class, ${opReturn} on ${network}`
);
}
static async getBalance(
address: string,
networkProvider: NetworkProvider
) {
const balance = (await networkProvider.getUtxos(address)).map(utxo => utxo.satoshis).filter((x) => x > 0).reduce(sum, 0n)
return balance
}
async getBalance(): Promise<bigint> {
const bal = await this.contract.getBalance();
return bal;
}
getAddress(): string {
return this.contract.address;
}
getLegacyAddress(): string {
const addr = lockingBytecodeToBase58Address(
this.getLockingBytecode(false) as Uint8Array,
this.testnet ? "testnet" : "mainnet"
);
if (typeof addr !== "string")
throw addr;
return addr;
}
async getUtxos(ageFilter?: number): Promise<Utxo[] | undefined> {
if (ageFilter) {
let utxos = await this.provider?.getUtxos(this.getAddress())
let nextHeight = await this.provider?.getBlockHeight()! + 1
return utxos?.filter(u => {
// @ts-ignore
if(u.height<=0){
// @ts-ignore
if(ageFilter==u.height){
return true;
}else{
return false;
}
}
// @ts-ignore
if (u.height && nextHeight) {
// @ts-ignore
return ((nextHeight - u.height) >= ageFilter)
} else {
return false;
}
});
} else {
return await this.provider?.getUtxos(this.getAddress());
}
}
getLockingBytecode(hex = true): string | Uint8Array {
if (hex) return deriveLockingBytecodeHex(this.contract.address);
return deriveLockingBytecode(this.contract.address);
}
checkLockingBytecode(lockingBytecode?: string | Uint8Array): boolean {
if (!lockingBytecode)
throw Error("Attempted to check an empty locking bytecode");
if (typeof lockingBytecode != "string") {
lockingBytecode = binToHex(lockingBytecode);
}
if (lockingBytecode !== this.getLockingBytecode())
throw `Deserialization resulted in different contract public key hash`;
return true;
}
asText(): string {
throw Error("Cannot get contract text description from base class");
}
asCommand(): string {
throw Error("Cannot get command from base class");
}
asSeries(): Promise<any> {
throw Error("Cannot get contract series from base class");
}
getRedeemScriptHex(): string {
return this.contract.bytecode
}
getFunction(fn: string) {
return this.contract.functions[fn];
}
isTestnet() {
return this.testnet;
}
asOpReturn(chunks: string[], hex: boolean) {
const opReturn = createOpReturnData(chunks);
if (hex) {
return binToHex(opReturn);
} else {
return opReturn;
}
}
async isFunded(): Promise<boolean> {
return (await this.getBalance()) > 0;
}
async info(cat = true): Promise<string | undefined> {
const bal = await this.getBalance();
const info =
`# ${this.asText()}\n` +
`# ${this.toString()}\n` +
`address: ${this.getAddress()}\n` +
`balance: ${bal}\n`;
if (cat) {
console.log(info);
return;
} else {
return info;
}
}
}