UNPKG

@unspent/phi

Version:

a collection of anyone can spend contracts

314 lines (264 loc) 8.54 kB
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; } } }