From f19d87af94e661c6f8d506067f27ee558b11fdaf Mon Sep 17 00:00:00 2001 From: Adam A Date: Mon, 6 May 2024 18:38:35 +0300 Subject: [PATCH] wip: refactor(scrt): bring implementation up to date with 2.0 api --- packages/agent/chain.ts | 584 ++++++++++++++++++------------- packages/agent/token.ts | 5 +- packages/cw/cw-bank.ts | 26 +- packages/cw/cw-batch.ts | 17 +- packages/cw/cw-compute.ts | 102 +++--- packages/cw/cw-connection.ts | 217 +++++------- packages/cw/cw-governance.ts | 0 packages/cw/cw.test.ts | 2 +- packages/oci/oci.ts | 73 ++-- packages/scrt/scrt-bank.ts | 16 + packages/scrt/scrt-base.ts | 9 + packages/scrt/scrt-compute.ts | 310 ++++++++++++++++ packages/scrt/scrt-connection.ts | 412 +++++----------------- packages/scrt/scrt-governance.ts | 3 + packages/scrt/scrt-staking.ts | 3 + 15 files changed, 991 insertions(+), 788 deletions(-) create mode 100644 packages/cw/cw-governance.ts create mode 100644 packages/scrt/scrt-bank.ts create mode 100644 packages/scrt/scrt-compute.ts create mode 100644 packages/scrt/scrt-governance.ts create mode 100644 packages/scrt/scrt-staking.ts diff --git a/packages/agent/chain.ts b/packages/agent/chain.ts index b63acfc7b07..88ed783ca99 100644 --- a/packages/agent/chain.ts +++ b/packages/agent/chain.ts @@ -40,6 +40,33 @@ export type Message = string|Record /** A transaction hash, uniquely identifying an executed transaction on a chain. */ export type TxHash = string +/** Represents the backend of an [`Endpoint`](#abstract-class-endpoint), managed by us, + * such as: + * + * * Local devnet RPC endpoint. + * * Stub/mock implementation of chain. + * + * You shouldn't need to instantiate this class directly. + * Instead, see `Connection`, `Devnet`, and their subclasses. */ +export abstract class Backend extends Logged { + /** The chain ID that will be passed to the devnet node. */ + chainId?: ChainId + /** Denomination of base gas token for this chain. */ + gasToken?: Token.Native + + constructor (properties?: Partial) { + super(properties) + assign(this, properties, ["chainId"]) + } + + abstract connect (): + Promise + abstract connect (parameter?: string|Partial): + Promise + abstract getIdentity (name: string): + Promise<{ address?: Address, mnemonic?: string }> +} + /** Represents a remote API endpoint. * * You shouldn't need to instantiate this class directly. @@ -86,30 +113,6 @@ export abstract class Endpoint extends Logged { } } -/** Provides control over the service backing an [`Endpoint`](#abstract-class-endpoint), such as: - * - * * Local devnet RPC endpoint. - * * Stub/mock implementation of chain. - * - * You shouldn't need to instantiate this class directly. - * Instead, see `Connection`, `Devnet`, and their subclasses. */ -export abstract class Backend extends Logged { - /** The chain ID that will be passed to the devnet node. */ - chainId?: ChainId - /** Denomination of base gas token for this chain. */ - gasToken?: Token.Native - - constructor (properties?: Partial) { - super(properties) - assign(this, properties, ["chainId"]) - } - - abstract connect (): Promise - abstract connect (parameter?: string|Partial): Promise - - abstract getIdentity (name: string): Promise<{ address?: Address, mnemonic?: string }> -} - /** Represents a connection to a blockchain via a given endpoint. * * Use one of its subclasses in `@fadroma/scrt`, `@fadroma/cw`, `@fadroma/namada` * to connect to the corresponding chain. @@ -157,12 +160,52 @@ export abstract class Connection extends Endpoint { return tag } - /** The default gas token of the chain. */ - get defaultDenom (): string { - return (this.constructor as Function & {gasToken: Token.Native}).gasToken?.id + ///////////////////////////////////////////////////////////////////////////////////////////////// + + abstract authenticate (identity: Identity): Agent + + /** Construct a transaction batch. */ + batch (): Batch { + return new Batch({ connection: this }) } - ///////////////////////////////////////////////////////////////////////////////////////////////// + /// FETCH BLOCK /// + + /** Get info about the latest block. */ + fetchBlock (): Promise + /** Get info about the block with a specific height. */ + fetchBlock ({ height }: { height: number }): Promise + /** Get info about the block with a specific hash. */ + fetchBlock ({ hash }: { hash: string }): Promise + + fetchBlock (...args: unknown[]): Promise { + if (args[0]) { + if (typeof args[0] === 'object') { + if ('height' in args[0]) { + this.log.debug(`Querying block by height ${args[0].height}`) + return this.fetchBlockImpl({ height: args[0].height as number }) + } else if ('hash' in args[0]) { + this.log.debug(`Querying block by hash ${args[0].hash}`) + return this.fetchBlockImpl({ hash: args[0].hash as string }) + } + } else { + throw new Error('Invalid arguments, pass {height:number} or {hash:string}') + } + } else { + this.log.debug(`Querying latest block`) + return this.fetchBlockImpl() + } + } + protected abstract fetchBlockImpl (parameters?: { height: number }|{ hash: string }): + Promise + + /** Get the current block height. */ + get height (): Promise { + this.log.debug('Querying block height') + return this.fetchHeightImpl() + } + protected abstract fetchHeightImpl (): + Promise /** Time to ping for next block. */ blockInterval = 250 @@ -202,61 +245,13 @@ export abstract class Connection extends Endpoint { }) } - /** Get the current block height. */ - get height (): Promise { - this.log.debug('Querying block height') - return this.fetchHeightImpl() - } - protected abstract fetchHeightImpl (): Promise - - /** Get info about the latest block. */ - fetchBlock (): Promise - /** Get info about the block with a specific height. */ - fetchBlock ({ height }: { height: number }): Promise - /** Get info about the block with a specific hash. */ - fetchBlock ({ hash }: { hash: string }): Promise - fetchBlock (...args: unknown[]): Promise { - if (args[0]) { - if (typeof args[0] === 'object') { - if ('height' in args[0]) { - this.log.debug(`Querying block by height ${args[0].height}`) - return this.fetchBlockImpl({ height: args[0].height as number }) - } else if ('hash' in args[0]) { - this.log.debug(`Querying block by hash ${args[0].hash}`) - return this.fetchBlockImpl({ hash: args[0].hash as string }) - } - } else { - throw new Error('Invalid arguments, pass {height:number} or {hash:string}') - } - } else { - this.log.debug(`Querying latest block`) - return this.fetchBlockImpl() - } - } - protected abstract fetchBlockImpl (options?: { height: number }|{ hash: string }): - Promise - - ///////////////////////////////////////////////////////////////////////////////////////////////// + /// FETCH BALANCE /// - /** Query a contract. */ - query (contract: Address, message: Message): Promise - query (contract: { address: Address }, message: Message): Promise - async query (contract: Address|{ address: Address }, message: Message): Promise { - const _contract = (typeof contract === 'string') ? { address: contract } : contract - const result = await timed( - ()=>this.queryImpl(_contract, message), - ({ elapsed, result }) => this.log.debug( - `Queried in ${bold(elapsed)}s: `, JSON.stringify(result) - ) - ) - return result as Q + /** The default gas token of the chain. */ + get defaultDenom (): string { + return (this.constructor as Function & {gasToken: Token.Native}).gasToken?.id } - protected abstract queryImpl (contract: { address: Address }, message: Message): - Promise - - ///////////////////////////////////////////////////////////////////////////////////////////////// - /** Fetch balance of 1 or many addresses in 1 or many native tokens. */ fetchBalance (address: Address, token: string): Promise @@ -330,44 +325,48 @@ export abstract class Connection extends Endpoint { //) //} } - protected abstract fetchBalanceImpl (token?: string, address?: string): + protected abstract fetchBalanceImpl (parameters: { token?: string, address?: string }): Promise - ///////////////////////////////////////////////////////////////////////////////////////////////// + /// FETCH CODE INFO /// /** Fetch info about all code IDs uploaded to the chain. */ fetchCodeInfo (): - Promise> + Promise> /** Fetch info about a single code ID. */ - fetchCodeInfo (id: Deploy.CodeId): - Promise + fetchCodeInfo (codeId: Deploy.CodeId, options: { parallel?: boolean }): + Promise /** Fetch info about multiple code IDs. */ - fetchCodeInfo (ids: Iterable): - Promise> - async fetchCodeInfo (...args: unknown[]): Promise { + fetchCodeInfo (codeIds: Iterable, options: { parallel?: boolean }): + Promise> + + fetchCodeInfo (...args: unknown[]): Promise { if (args.length === 0) { this.log.debug('Querying all codes...') return timed( - this.fetchCodeInfoImpl.bind(this, {}), + ()=>this.fetchCodeInfoImpl(), ({ elapsed, result }) => this.log.debug( `Queried in ${bold(elapsed)}: all codes` )) - } else if (args.length === 1) { + } + if (args.length === 1) { if (args[0] instanceof Array) { - const ids = args[0] as Array - this.log.debug(`Querying info about ${ids.length} code ids...`) + const codeIds = args[0] as Array + const { parallel } = args[1] as { parallel?: boolean } + this.log.debug(`Querying info about ${codeIds.length} code IDs...`) return timed( - this.fetchCodeInfoImpl.bind(this, { ids }), + ()=>this.fetchCodeInfoImpl({ codeIds, parallel }), ({ elapsed, result }) => this.log.debug( - `Queried in ${bold(elapsed)}: info about ${ids.length} code ids` + `Queried in ${bold(elapsed)}: info about ${codeIds.length} code IDs` )) } else { - const ids = [args[0] as Deploy.CodeId] + const codeIds = [args[0] as Deploy.CodeId] + const { parallel } = args[1] as { parallel?: boolean } this.log.debug(`Querying info about code id ${args[0]}...`) return timed( - this.fetchCodeInfoImpl.bind(this, { ids }), - ({ elapsed, result }) => this.log.debug( - `Queried in ${bold(elapsed)}: info about code id ${ids[0]}` + ()=>this.fetchCodeInfoImpl({ codeIds, parallel }), + ({ elapsed }) => this.log.debug( + `Queried in ${bold(elapsed)}: info about code id ${codeIds[0]}` )) } } else { @@ -375,139 +374,237 @@ export abstract class Connection extends Endpoint { } } /** Chain-specific implementation of fetchCodeInfo. */ - protected abstract fetchCodeInfoImpl (options: { ids?: Deploy.CodeId[] }|undefined): + protected abstract fetchCodeInfoImpl (parameters?: { + codeIds?: Deploy.CodeId[] + parallel?: boolean + }): Promise> - ///////////////////////////////////////////////////////////////////////////////////////////////// - - fetchContractInfo (address: Address): - Promise - fetchContractInfo (addresses: Address[]): - Promise> - async fetchContractInfo (...args: unknown[]): Promise { - if (!args[0]) { - throw new Error('fetchContractInfo takes Address or Address[]') - } - if (typeof args[0] === 'string') { - this.log.debug(`Querying info about contract ${args[0]}`) - return timed( - this.fetchContractInfoImpl.bind(this, { addresses: [args[0]] }), - ({ elapsed, result }) => this.log.debug( - `Queried in ${bold(elapsed)}: info about contract ${args[0]}` - )) - } else if (args[0][Symbol.iterator]) { - const addresses = args[0] as Address[] - this.log.debug(`Querying info about ${addresses.length} contracts`) - return timed( - this.fetchContractInfoImpl.bind(this, { addresses }), - ({ elapsed, result }) => this.log.debug( - `Queried in ${bold(elapsed)}: info about ${addresses.length} contracts` - )) - } else { - throw new Error('fetchContractInfo takes Address or Address[]') - } - } - /** Chain-specific implementation of fetchContractInfo. */ - protected abstract fetchContractInfoImpl ({ addresses }: { addresses: Address[] }): - Promise - /** Get a client handle for a specific smart contract, authenticated as as this agent. */ - getContract (options: Address|{ address: Address }): - Contract - getContract ( - options: Address|{ address: Address }, $C: C = Contract as C, - ): InstanceType { - if (typeof options === 'string') { - options = { address: options } - } - return new $C({ - instance: options, - connection: this - }) as InstanceType - } - - ///////////////////////////////////////////////////////////////////////////////////////////////// + /// FETCH CODE INSTANCES /// /** Fetch all instances of a code ID. */ - fetchCodeInstances (id: Deploy.CodeId): - Promise> + fetchCodeInstances ( + codeId: Deploy.CodeId + ): Promise> /** Fetch all instances of a code ID, with custom client class. */ - fetchCodeInstances ($C: C, id: Deploy.CodeId): - Promise>> + fetchCodeInstances ( + Contract: C, + codeId: Deploy.CodeId + ): Promise>> /** Fetch all instances of multple code IDs. */ - fetchCodeInstances (ids: Iterable): - Promise>> + fetchCodeInstances ( + codeIds: Iterable, + options?: { parallel?: boolean } + ): Promise>> /** Fetch all instances of multple code IDs, with custom client class. */ - fetchCodeInstances ($C: C, ids: Iterable): - Promise>>> + fetchCodeInstances ( + Contract: C, + codeIds: Iterable, + options?: { parallel?: boolean } + ): Promise>>> /** Fetch all instances of multple code IDs, with multiple custom client classes. */ - fetchCodeInstances (ids: { [id: Deploy.CodeId]: typeof Contract }): - Promise }>> + fetchCodeInstances ( + codeIds: { [id: Deploy.CodeId]: typeof Contract }, + options?: { parallel?: boolean } + ): Promise<{ + [codeId in keyof typeof codeIds]: Record> + }> async fetchCodeInstances (...args: unknown[]): Promise { let $C = Contract + let custom = false if (typeof args[0] === 'function') { $C = args.shift() as typeof Contract + let custom = true } - if (!args[0]) { throw new Error('Invalid arguments') } - if (!!(args[0][Symbol.iterator])) { + if (args[0][Symbol.iterator]) { const result: Record> = {} - const ids = [...args[0] as Deploy.CodeId[]] - this.log.debug(`Querying contracts with code ids ${ids.join(', ')}...`) - for (const codeId of ids) { - result[codeId] = {} - for (const instance of await this.fetchCodeInstancesImpl(codeId)) { - result[codeId][instance.address] = new $C(instance) - } + const codeIds = {} + for (const codeId of args[0] as Deploy.CodeId[]) { + codeIds[codeId] = $C } - return result + this.log.debug(`Querying contracts with code ids ${Object.keys(codeIds).join(', ')}...`) + return timed( + ()=>this.fetchCodeInstancesImpl({ codeIds }), + ({elapsed})=>this.log.debug(`Queried in ${elapsed}ms`)) } if (typeof args[0] === 'object') { + if (custom) { + throw new Error('Invalid arguments') + } const result: Record> = {} this.log.debug(`Querying contracts with code ids ${Object.keys(args[0]).join(', ')}...`) - for (const [codeId, $C] of Object.entries(args[0])) { - result[codeId] = {} - for (const instance of await this.fetchCodeInstancesImpl(codeId)) { - result[codeId][instance.address] = new $C(instance) - } - } - return result + const codeIds = args[0] as { [id: Deploy.CodeId]: typeof Contract } + return timed( + ()=>this.fetchCodeInstancesImpl({ codeIds }), + ({elapsed})=>this.log.debug(`Queried in ${elapsed}ms`)) } if ((typeof args[0] === 'number')||(typeof args[0] === 'string')) { const id = args[0] this.log.debug(`Querying contracts with code id ${id}...`) const result = {} - for (const instance of await this.fetchCodeInstancesImpl(id as string)) { - result[instance.address] = new $C(instance) - } - return result + return timed( + ()=>this.fetchCodeInstancesImpl({ codeIds: { [id]: $C } }), + ({elapsed})=>this.log.debug(`Queried in ${elapsed}ms`)) } throw new Error('Invalid arguments') } /** Chain-specific implementation of fetchCodeInstances. */ - protected abstract fetchCodeInstancesImpl (id: Deploy.CodeId): - Promise> + protected abstract fetchCodeInstancesImpl (parameters: { + codeIds: { [id: Deploy.CodeId]: typeof Contract }, + parallel?: boolean + }): + Promise<{ + [codeId in keyof typeof parameters["codeIds"]]: + Record> + }> + + /// FETCH CONTRACT INFO /// + + /** Fetch a contract's details wrapped in a `Contract` instance. */ + fetchContractInfo ( + address: Address + ): Promise + /** Fetch a contract's details wrapped in a custom class instance. */ + fetchContractInfo ( + Contract: C, + address: Address + ): Promise + /** Fetch multiple contracts' details wrapped in `Contract` instance. */ + fetchContractInfo ( + addresses: Address[], + options?: { parallel?: boolean } + ): Promise> + /** Fetch multiple contracts' details wrapped in instances of a custom class. */ + fetchContractInfo ( + Contract: C, + addresses: Address[], + options?: { parallel?: boolean } + ): Promise> + /** Fetch multiple contracts' details, specifying a custom class for each. */ + fetchContractInfo ( + contracts: { [address: Address]: typeof Contract }, + options?: { parallel?: boolean } + ): Promise<{ + [address in keyof typeof contracts]: InstanceType + }> - ///////////////////////////////////////////////////////////////////////////////////////////////// + async fetchContractInfo (...args: unknown[]): Promise { + let $C = Contract + let custom = false + if (typeof args[0] === 'function') { + $C = args.shift() as typeof Contract + custom = true + } + if (!args[0]) { + throw new Error('Invalid arguments') + } + const { parallel = false } = (args[1] || {}) as { parallel?: boolean } - abstract authenticate (identity: Identity): Agent + // Fetch single contract + if (typeof args[0] === 'string') { + this.log.debug(`Fetching contract ${args[0]}`) + const contracts = await timed( + ()=>this.fetchContractInfoImpl({ addresses: [args[0] as Address] }), + ({ elapsed }) => this.log.debug( + `Fetched in ${bold(elapsed)}: contract ${args[0]}` + )) + if (custom) { + return new $C(contracts[args[0]]) + } else { + return contracts[args[0]] + } + } - /** Construct a transaction batch. */ - batch (): Batch { - return new Batch({ connection: this }) + // Fetch array of contracts + if (args[0][Symbol.iterator]) { + const addresses = args[0] as Address[] + this.log.debug(`Fetching ${addresses.length} contracts`) + const contracts = await timed( + ()=>this.fetchContractInfoImpl({ addresses, parallel }), + ({ elapsed }) => this.log.debug( + `Fetched in ${bold(elapsed)}: ${addresses.length} contracts` + )) + if (custom) { + return addresses.map(address=>new $C(contracts[address])) + } else { + return addresses.map(address=>contracts[address]) + } + } + + // Fetch map of contracts with different classes + if (typeof args[0] === 'object') { + if (custom) { + // Can't specify class as first argument + throw new Error('Invalid arguments') + } + + const addresses = Object.keys(args[0]) as Address[] + this.log.debug(`Querying info about ${addresses.length} contracts`) + const contracts = await timed( + ()=>this.fetchContractInfoImpl({ addresses, parallel }), + ({ elapsed }) => this.log.debug( + `Queried in ${bold(elapsed)}: info about ${addresses.length} contracts` + )) + const result = {} + for (const address of addresses) { + result[address] = new args[0][address](contracts[address]) + } + return result + } + + throw new Error('Invalid arguments') + } + /** Chain-specific implementation of fetchContractInfo. */ + protected abstract fetchContractInfoImpl (parameters: { + contracts: { [address: Address]: typeof Contract }, + parallel?: boolean + }): + Promise> + + /// QUERY /// + + /** Query a contract by address. */ + query (contract: Address, message: Message): Promise + /** Query a contract object. */ + query (contract: { address: Address }, message: Message): Promise + + query (contract: Address|{ address: Address }, message: Message): + Promise { + return timed( + ()=>this.queryImpl({ + ...(typeof contract === 'string') ? { address: contract } : contract, + message + }), + ({ elapsed, result }) => this.log.debug( + `Queried in ${bold(elapsed)}s: `, JSON.stringify(result) + ) + ) } + + protected abstract queryImpl (parameters: { + address: Address + codeHash?: string + message: Message + }): + Promise + } +/** Enables non-read-only transactions by binding an `Identity` to a `Connection`. */ export abstract class Agent extends Logged { + /** The connection that will broadcast the transactions. */ connection: Connection + /** The identity that will sign the transactions. */ identity: Identity /** Default transaction fees. */ - fees?: { send?: Token.IFee, upload?: Token.IFee, init?: Token.IFee, exec?: Token.IFee } + fees?: Token.FeeMap<'send'|'upload'|'init'|'exec'> constructor (properties: Partial) { super() @@ -539,28 +636,29 @@ export abstract class Agent extends Logged { ` ${amounts.map(x=>x.toString()).join(', ')}` ) return await timed( - ()=>this.sendImpl(recipient as string, amounts.map( - amount=>(amount instanceof Token.Amount)?amount.asCoin():amount - ), options), + ()=>this.sendImpl({ + recipient: recipient as string, + amounts: amounts.map( + amount=>(amount instanceof Token.Amount)?amount.asCoin():amount + ), + options + }), t=>`Sent in ${bold(t)}s` ) } - protected abstract sendImpl ( - recipient: Address, amounts: Token.ICoin[], options?: Parameters[2] - ): Promise - protected abstract sendManyImpl ( - outputs: [Address, Token.ICoin[]][], options?: unknown - ): Promise + + /** Chain-specific implementation of native token transfer. */ + protected abstract sendImpl (parameters: { + outputs: Record>, + sendFee?: Token.IFee, + sendMemo?: string, + parallel?: boolean + }): Promise /** Upload a contract's code, generating a new code id/hash pair. */ async upload ( - code: string|URL|Uint8Array|Partial, - options: { - reupload?: boolean, - uploadStore?: Store.UploadStore, - uploadFee?: Token.ICoin[]|'auto', - uploadMemo?: string - } = {}, + code: string|URL|Uint8Array|Partial, + options?: Omit[0], 'binary'>, ): Promise this.uploadImpl({ ...options, binary: template }), ({elapsed, result}: any) => this.log.debug( `Uploaded in ${bold(elapsed)}:`, `code with hash ${bold(result.codeHash)} as code id ${bold(String(result.codeId))}`, @@ -595,19 +693,20 @@ export abstract class Agent extends Logged { chainId: ChainId, codeId: Deploy.CodeId, } } - protected abstract uploadImpl ( - data: Uint8Array, options: Parameters[1] - ): Promise> - /** Instantiate a new program from a code id, label and init message. - * @example - * await agent.instantiate(template.define({ label, initMsg }) - * @returns - * Deploy.ContractInstance with no `address` populated yet. - * This will be populated after executing the batch. */ + /** Instantiate a new program from a code id, label and init message. */ async instantiate ( contract: Deploy.CodeId|Partial, options: Partial @@ -631,8 +730,11 @@ export abstract class Agent extends Logged { } const { codeId, codeHash } = contract const result = await timed( - () => into(options.initMsg).then(initMsg=>this.instantiateImpl(codeId, { - codeHash, ...options, initMsg + () => into(options.initMsg).then(initMsg=>this.instantiateImpl({ + ...options, + codeId, + codeHash, + initMsg })), ({ elapsed, result }) => this.log.debug( `Instantiated in ${bold(elapsed)}:`, @@ -647,16 +749,16 @@ export abstract class Agent extends Logged { } } - protected abstract instantiateImpl ( - codeId: Deploy.CodeId, options: Partial - ): Promise + /** Chain-specific implementation of contract instantiation. */ + protected abstract instantiateImpl (parameters: Partial): + Promise /** Call a given program's transaction method. */ - async execute ( + async execute ( contract: Address|Partial, message: Message, - options?: { execFee?: Token.IFee, execSend?: Token.ICoin[], execMemo?: string } - ): Promise { + options?: Omit[0], 'address'|'codeHash'|'message'> + ): Promise { if (typeof contract === 'string') { contract = new Deploy.ContractInstance({ address: contract }) } @@ -666,7 +768,11 @@ export abstract class Agent extends Logged { const { address } = contract let method = (typeof message === 'string') ? message : Object.keys(message||{})[0] return timed( - () => this.executeImpl(contract as { address: Address }, message, options), + () => this.executeImpl({ + ...contract as { address, codeHash }, + message, + ...options + }), ({ elapsed }) => this.log.debug( `Executed in ${bold(elapsed)}:`, `tx ${bold(method||'(???)')} of ${bold(address)}` @@ -674,11 +780,15 @@ export abstract class Agent extends Logged { ) } - protected abstract executeImpl ( - contract: { address: Address }, - message: Message, - options: Parameters[2] - ): Promise + /** Chain-specific implementation of contract transaction. */ + protected abstract executeImpl (parameters: { + address: Address + codeHash?: string + message: Message + execFee?: Token.IFee + execSend?: Token.ICoin[] + execMemo?: string + }): Promise } /** The building block of a blockchain, as obtained by @@ -712,17 +822,20 @@ export abstract class Block { * Subclass this to add custom query and transaction methods corresponding * to the contract's API. */ export class Contract extends Logged { - /** Connection to the chain on which this contract is deployed. */ connection?: Connection - - agent?: Agent - - instance?: { address?: Address } - - get address (): Address|undefined { - return this.instance?.address - } + /** Connection to the chain on which this contract is deployed. */ + agent?: Agent + /** Code upload from which this contract is created. */ + codeId?: Deploy.CodeId + /** The code hash uniquely identifies the contents of the contract code. */ + codeHash?: Code.CodeHash + /** The address uniquely identifies the contract instance. */ + address?: Address + /** The label is a human-friendly identifier of the contract. */ + label?: Label + /** The address of the account which instantiated the contract. */ + initBy?: Address constructor (properties: Partial) { super((typeof properties === 'string')?{}:properties) @@ -763,5 +876,4 @@ export class Contract extends Logged { this.instance as Deploy.ContractInstance & { address: Address }, message, options ) } - } diff --git a/packages/agent/token.ts b/packages/agent/token.ts index aca56cdcf76..e1d1847dc2d 100644 --- a/packages/agent/token.ts +++ b/packages/agent/token.ts @@ -19,7 +19,10 @@ export type Decimal256 = string export interface ICoin { amount: Uint128, denom: string } /** A gas fee, payable in native tokens. */ -export interface IFee { amount: readonly ICoin[], gas: Uint128 } +export interface IFee { gas: Uint128, amount: readonly ICoin[] } + +/** A mapping of transaction type to default fee in one or more tokens. */ +export type FeeMap = { [key in T]: IFee } /** A constructable gas fee in native tokens. */ export class Fee implements IFee { diff --git a/packages/cw/cw-bank.ts b/packages/cw/cw-bank.ts index b5baefd104a..ad8bf780f4b 100644 --- a/packages/cw/cw-bank.ts +++ b/packages/cw/cw-bank.ts @@ -3,24 +3,19 @@ import type { Address, Token, Chain } from '@fadroma/agent' import { Core } from '@fadroma/agent' type Connection = { - address?: Address, - log: Core.Console api: CosmWasmClient|Promise } export async function getBalance ( - { api, log, address }: Connection, token: string, queriedAddress: Address|undefined = address + { api }: Connection, + token: string, + address: Address ) { api = await Promise.resolve(api) - if (!queriedAddress) { + if (!address) { throw new Error('getBalance: need address') } - if (queriedAddress === address) { - log.debug('Querying', Core.bold(token), 'balance') - } else { - log.debug('Querying', Core.bold(token), 'balance of', Core.bold(queriedAddress)) - } - const { amount } = await api.getBalance(queriedAddress!, token!) + const { amount } = await api.getBalance(address, token) return amount } @@ -32,9 +27,10 @@ type SigningConnection = { export async function send ( { api, address }: SigningConnection, - recipient: Address, - amounts: Token.ICoin[], - options?: Parameters[2] + { outputs + , sendFee = 'auto' + , sendMemo + , parallel }: Parameters[0] ) { api = await Promise.resolve(api) if (!(api?.sendTokens)) { @@ -44,7 +40,7 @@ export async function send ( address!, recipient as string, amounts, - options?.sendFee || 'auto', - options?.sendMemo + sendFee, + sendMemo ) } diff --git a/packages/cw/cw-batch.ts b/packages/cw/cw-batch.ts index d03098a75e7..2724dcf63d9 100644 --- a/packages/cw/cw-batch.ts +++ b/packages/cw/cw-batch.ts @@ -1,29 +1,28 @@ import { Core, Chain } from '@fadroma/agent' -import type { CWConnection } from './cw-connection' +import type { CWConnection, CWAgent } from './cw-connection' /** Transaction batch for CosmWasm-enabled chains. */ -export class CWBatch extends Chain.Batch { +export class CWBatch extends Chain.Batch { upload ( - code: Parameters["upload"]>[0], - options: Parameters["upload"]>[1] + code: Parameters["upload"]>[0], + options: Parameters["upload"]>[1] ) { throw new Core.Error("CWBatch#upload: not implemented") return this } instantiate ( - code: Parameters["instantiate"]>[0], - options: Parameters["instantiate"]>[1] + code: Parameters["instantiate"]>[0], + options: Parameters["instantiate"]>[1] ) { throw new Core.Error("CWBatch#instantiate: not implemented") return this } execute ( - contract: Parameters["execute"]>[0], - options: Parameters["execute"]>[1] + contract: Parameters["execute"]>[0], + options: Parameters["execute"]>[1] ) { throw new Core.Error("CWBatch#execute: not implemented") return this } async submit () {} } - diff --git a/packages/cw/cw-compute.ts b/packages/cw/cw-compute.ts index 8688303f295..0c31c0a7d64 100644 --- a/packages/cw/cw-compute.ts +++ b/packages/cw/cw-compute.ts @@ -8,6 +8,14 @@ type Connection = { api: CosmWasmClient|Promise } +export type SigningConnection = { + log: Core.Console, + chainId?: string, + address: string, + fees?: any, + api: SigningCosmWasmClient|Promise +} + export async function getCodes ( { chainId, api }: Connection ) { @@ -25,31 +33,46 @@ export async function getCodes ( return codes } -export async function getCodeId ({ api }: Connection, address: Address): Promise { +export async function getCodeId ( + { api }: Connection, + address: Address +): Promise { api = await Promise.resolve(api) const { codeId } = await api.getContract(address) return String(codeId) } -export async function getContractsByCodeId ({ api }: Connection, id: CodeId) { +export async function getContractsByCodeId ( + { api }: Connection, + id: CodeId +) { api = await Promise.resolve(api) const addresses = await api.getContracts(Number(id)) return addresses.map(address=>({address})) } -export async function getCodeHashOfAddress (connection: Connection, address: Address) { +export async function getCodeHashOfAddress ( + connection: Connection, + address: Address +) { const api = await Promise.resolve(connection.api) const {codeId} = await api.getContract(address) return getCodeHashOfCodeId(connection, String(codeId)) } -export async function getCodeHashOfCodeId ({ api }: Connection, codeId: CodeId) { +export async function getCodeHashOfCodeId ( + { api }: Connection, + codeId: CodeId +) { api = await Promise.resolve(api) const {checksum} = await api.getCodeDetails(Number(codeId)) return checksum } -export async function getLabel ({ api }: Connection, address: Address) { +export async function getLabel ( + { api }: Connection, + address: Address +) { if (!address) { throw new Error('chain.getLabel: no address') } @@ -58,39 +81,36 @@ export async function getLabel ({ api }: Connection, address: Address) { return label } -export async function query ( - { api }: Connection, - contract: Address|{ address: Address }, - message: Message -): Promise { +export async function query ( + { api }: Connection, + options: Parameters[0] +): Promise { api = await Promise.resolve(api) - if (typeof contract === 'string') { - contract = { address: contract } - } - if (!contract.address) { + if (!options.address) { throw new Error('no contract address') } - return api.queryContractSmart((contract as { address: Address }).address, message) as U -} - -export type SigningConnection = { - log: Core.Console, - chainId?: string, - address: string, - fees?: any, - api: SigningCosmWasmClient|Promise + return await api.queryContractSmart( + options.address, + options.message, + ) as T } export async function upload ( { address, chainId, fees, api }: SigningConnection, - data: Uint8Array + options: Parameters[0] ) { + if (!address) { + throw new Error("can't upload contract without sender address") + } api = await Promise.resolve(api) if (!(api?.upload)) { throw new Error("can't upload contract with an unauthenticated agent") } const result = await api.upload( - address!, data, fees?.upload || 'auto', "Uploaded by Fadroma" + address!, + options.binary, + fees?.upload || 'auto', + "Uploaded by Fadroma" ) return { chainId: chainId, @@ -104,23 +124,25 @@ export async function upload ( export async function instantiate ( { api, address, chainId }: SigningConnection, - codeId: CodeId, - options: Parameters[1] + options: Parameters[0] ) { + if (!this.address) { + throw new Error("can't instantiate contract without sender address") + } api = await Promise.resolve(api) if (!(api?.instantiate)) { throw new Error("can't instantiate contract without authorizing the agent") } const result = await (api as SigningCosmWasmClient).instantiate( address!, - Number(codeId), + Number(options.codeId), options.initMsg, options.label!, options.initFee as Amino.StdFee || 'auto', { admin: address, funds: options.initSend, memo: options.initMemo } ) return { - codeId, + codeId: options.codeId, codeHash: options.codeHash, label: options.label, initMsg: options.initMsg, @@ -135,27 +157,23 @@ export async function instantiate ( } } -type ExecOptions = - Omit[2]>, 'execFee'> & { - execFee?: Token.IFee | number | 'auto' - } - export async function execute ( { api, address, chainId }: SigningConnection, - contract: { address: Address }, - message: Message, - { execSend, execMemo, execFee }: ExecOptions = {} + options: Parameters[0] ) { + if (!address) { + throw new Error("can't execute transaction without sender address") + } api = await Promise.resolve(api) if (!(api?.execute)) { throw new Error("can't execute transaction without authorizing the agent") } return api.execute( address!, - contract.address, - message, - execFee!, - execMemo, - execSend + options.address, + options.message, + options.execFee!, + options.execMemo, + options.execSend ) } diff --git a/packages/cw/cw-connection.ts b/packages/cw/cw-connection.ts index 0923e1d4c55..dfc54a8ca2a 100644 --- a/packages/cw/cw-connection.ts +++ b/packages/cw/cw-connection.ts @@ -1,39 +1,14 @@ import { Core, Chain, Deploy } from '@fadroma/agent' import type { Address, Message, CodeId, CodeHash, Token } from '@fadroma/agent' -import type { CWMnemonicIdentity, CWSignerIdentity } from './cw-identity' +import type { CWIdentity, CWMnemonicIdentity, CWSignerIdentity } from './cw-identity' import { CWConsole as Console, CWError as Error } from './cw-base' -import { CWBatch } from './cw-batch' - +import { CWBatch } from './cw-batch' +import * as Bank from './cw-bank' +import * as Compute from './cw-compute' +import * as Staking from './cw-staking' import { Amino, Proto, CosmWasmClient, SigningCosmWasmClient } from '@hackbg/cosmjs-esm' import type { Block } from '@hackbg/cosmjs-esm' -import { - getBalance, - send, -} from './cw-bank' - -import { - getCodes, - getCodeId, - getContractsByCodeId, - getCodeHashOfAddress, - getCodeHashOfCodeId, - getLabel, - upload, - instantiate, - execute, - query -} from './cw-compute' - -import type { - SigningConnection -} from './cw-compute' - -import { - Validator, - getValidators, -} from './cw-staking' - export class CWBlock extends Chain.Block { rawTxs: Uint8Array[] constructor ({ hash, height, rawTxs }: Partial = {}) { @@ -50,8 +25,8 @@ export class CWConnection extends Chain.Connection { coinType?: number /** The account index in the HD derivation path */ hdAccountIndex?: number - /** API connects asynchronously, so API handle is a promise of either variant. */ - declare api: Promise + /** API connects asynchronously, so API handle is a promise. */ + declare api: Promise /** A supported method of authentication. */ declare identity: CWMnemonicIdentity|CWSignerIdentity @@ -67,13 +42,24 @@ export class CWConnection extends Chain.Connection { } if (this.identity?.signer) { this.log.debug('Connecting and authenticating via', Core.bold(this.url)) - this.api = SigningCosmWasmClient.connectWithSigner(this.url, this.identity.signer) + this.api = SigningCosmWasmClient.connectWithSigner( + this.url, + this.identity.signer + ) } else { this.log.debug('Connecting anonymously via', Core.bold(this.url)) this.api = CosmWasmClient.connect(this.url) } } + authenticate (identity: CWIdentity): CWAgent { + return new CWAgent({ connection: this, identity }) + } + + batch (): Chain.Batch { + return new CWBatch({ connection: this }) as unknown as Chain.Batch + } + /** Handle to the API's internal query client. */ get queryClient (): Promise> { return Promise.resolve(this.api).then(api=>(api as any)?.queryClient) @@ -92,118 +78,107 @@ export class CWConnection extends Chain.Connection { }) } - async doGetBlockInfo (height?: number): Promise { + protected override async fetchBlockImpl (parameter?: { height: number }|{ hash: string }): + Promise + { const api = await this.api - const { id, header, txs } = await api.getBlock(height) - return new CWBlock({ - hash: id, - height: header.height, - rawTxs: txs as Uint8Array[], - }) + if ((parameter as { height })?.height) { + const { id, header, txs } = await api.getBlock((parameter as { height }).height) + return new CWBlock({ + hash: id, + height: header.height, + rawTxs: txs as Uint8Array[], + }) + } else if ((parameter as { hash })?.hash) { + throw new Error('CWConnection.fetchBlock({ hash }): unimplemented!') + } else { + const { id, header, txs } = await api.getBlock() + return new CWBlock({ + hash: id, + height: header.height, + rawTxs: txs as Uint8Array[], + }) + } } - async doGetHeight () { - const { height } = await this.doGetBlockInfo() + protected override async fetchHeightImpl () { + const { height } = await this.fetchBlockImpl() return height } /** Query native token balance. */ - doGetBalance ( - token: string = this.defaultDenom, - address: Address|undefined = this.address - ): Promise { - return getBalance(this, token, address) + protected override fetchBalanceImpl (parameters) { + return Bank.getBalance(this, parameters) } - doGetCodes () { - return getCodes(this) - } - - doGetCodeId (address: Address): Promise { - return getCodeId(this, address) - } - - doGetContractsByCodeId (id: CodeId): Promise> { - return getContractsByCodeId(this, id) - } - - doGetCodeHashOfAddress (address: Address): Promise { - return getCodeHashOfAddress(this, address) - } - - doGetCodeHashOfCodeId (codeId: CodeId): Promise { - return getCodeHashOfCodeId(this, codeId) - } - - async getLabel (address: Address): Promise { - return getLabel(this, address) - } - - async doSend ( - recipient: Address, - amounts: Token.ICoin[], - options?: Parameters[2] - ) { - return send(this as SigningConnection, recipient, amounts, options) - } - - doSendMany ( - outputs: [Address, Token.ICoin[]][], - options?: Parameters[1] - ): Promise { - throw new Error('doSendMany: not implemented') - } - - async doUpload (data: Uint8Array): Promise> { - if (!this.address) { - throw new Error("can't upload contract without sender address") + protected override fetchCodeInfoImpl (parameters) { + const { ids = [] } = parameters || {} + if (!ids || ids.length === 0) { + return Compute.getCodes(this) + } else if (ids.length === 1) { + return Compute.getCodes(this, ids) + } else { + throw new Error('CWConnection.fetchCodeInfo({ ids: [multiple] }): unimplemented!') } - return upload(this as SigningConnection, data) + //protected override fetchCodesImpl () { + //return Compute.getCodes(this) + //} + //protected override fetchCodeIdImpl (address: Address): Promise { + //return getCodeId(this, address) + //} + //protected override fetchCodeHashOfCodeIdImpl (codeId: CodeId): Promise { + //return Compute.getCodeHashOfCodeId(this, codeId) + //} } - async doInstantiate ( - codeId: CodeId, options: Parameters[1] - ): Promise> { - if (!this.address) { - throw new Error("can't instantiate contract without sender address") - } - return instantiate(this as SigningConnection, codeId, options) - } - - async doExecute ( - contract: { address: Address }, - message: Message, - options: Omit[2]>, 'execFee'> & { - execFee?: Token.IFee | number | 'auto' - } = {} - ): Promise { - if (!this.address) { - throw new Error("can't execute transaction without sender address") - } - options.execFee ??= 'auto' - return execute(this as SigningConnection, contract, message, options) + protected override fetchCodeInstancesImpl (parameters) { + //protected override fetchContractsByCodeIdImpl (id: CodeId): Promise> { + //return Compute.getContractsByCodeId(this, id) + //} } - async doQuery ( - contract: Address|{ address: Address }, message: Message - ): Promise { - return query(this, contract, message) + protected override fetchContractInfoImpl (parameters) { + //return Compute.getCodeId(this, address) + //protected override fetchCodeHashOfAddressImpl (address: Address): Promise { + //return Compute.getCodeHashOfAddress(this, address) + //} + //protected override fetchLabelImpl (address: Address): Promise { + //return Compute.getLabel(this, address) + //} } - batch (): Chain.Batch { - return new CWBatch({ connection: this }) as unknown as Chain.Batch + protected override async queryImpl (parameters) { + return await Compute.query(this, parameters) as T } - getValidators ({ details = false }: { + fetchValidators ({ details = false }: { details?: boolean } = {}) { - return this.tendermintClient.then(()=>getValidators(this, { details })) + return this.tendermintClient.then(()=>Staking.getValidators(this, { details })) } - getValidator (address: Address): Promise { + fetchValidatorInfo (address: Address): Promise { return Promise.all([ this.queryClient, this.tendermintClient - ]).then(()=>new Validator({ address }).fetchDetails(this)) + ]).then(()=>new Staking.Validator({ address }).fetchDetails(this)) + } +} + +export class CWAgent extends Chain.Agent { + /** API connects asynchronously, so API handle is a promise. */ + declare api: Promise + + protected override async sendImpl (parameters) { + return Bank.send(this as Compute.SigningConnection, parameters) + } + protected override async uploadImpl (parameters) { + return Compute.upload(this as Compute.SigningConnection, parameters) + } + protected override async instantiateImpl (parameters) { + return Compute.instantiate(this as Compute.SigningConnection, parameters) + } + protected override async executeImpl (parameters) { + return Compute.execute(this as Compute.SigningConnection, parameters) } } diff --git a/packages/cw/cw-governance.ts b/packages/cw/cw-governance.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/cw/cw.test.ts b/packages/cw/cw.test.ts index caa451b5f0f..fb1676440b1 100644 --- a/packages/cw/cw.test.ts +++ b/packages/cw/cw.test.ts @@ -34,7 +34,7 @@ export async function testCWChain () { Identity: CW.OKP4.MnemonicIdentity, code: fixture('cw-null.wasm') }) - console.log(await (alice as CW.OKP4.Connection).getValidators()) + console.log(await (alice as CW.OKP4.Connection).fetchValidators()) //new CW.OKP4.Connection({ signer: {}, mnemonic: 'x' } as any) //throws(()=>new CW.OKP4.Connection({ address: 'foo', mnemonic: [ //'define abandon palace resource estate elevator', diff --git a/packages/oci/oci.ts b/packages/oci/oci.ts index d5d76ac30e8..4f8ef9eacc8 100644 --- a/packages/oci/oci.ts +++ b/packages/oci/oci.ts @@ -36,64 +36,45 @@ class OCIConnection extends Chain.Connection { declare api: DockerHandle - async doGetHeight () { - throw new Error('doGetHeight: not applicable') + protected override async fetchHeightImpl () { + throw new Error('fetchHeightImpl: not applicable') return + new Date() } - async doGetBlockInfo () { - throw new Error('doGetBlockInfo: not applicable') + + protected override async fetchBlockImpl () { + throw new Error('fetchBlockImpl: not applicable') return {} } - async doGetBalance () { - throw new Error('doGetBalance: not applicable') + + protected override async fetchBalanceImpl () { + throw new Error('fetchBalanceImpl: not applicable') return 0 } - async doSend () { - throw new Error('doSend: not applicable') - } - async doSendMany () { - throw new Error('doSendMany: not applicable') - } - async doGetCodeId (containerId: string): Promise { + protected override async fetchContractInfoImpl (containerId: string): Promise { const container = await this.api.getContainer(containerId) const info = await container.inspect() return info.Image } - async doGetCodeHashOfCodeId (contract) { - return '' - } - async doGetCodeHashOfAddress (contract) { - return '' - } + /** Returns list of container images. */ - async doGetCodes () { + protected override async fetchCodeInfoImpl () { return (await this.api.listImages()) .reduce((images, image)=>Object.assign(images, { [image.Id]: image }), {}) } + /** Returns list of containers from a given image. */ - async doGetContractsByCodeId (imageId) { + protected override async fetchCodeInstancesImpl (imageId) { return (await this.api.listContainers()) .filter(container=>container.Image === imageId) .map(container=>({ address: container.Id, codeId: imageId, container })) } - async doUpload (data: Uint8Array) { - throw new Error('doUpload (load/import image): not implemented') - return {} - } - async doInstantiate (imageId: string) { - throw new Error('doInstantiate (create container): not implemented') - return {} - } - async doExecute () { - throw new Error('doExecute (exec in container): not implemented') - return {} - } - async doQuery (contract, message) { - throw new Error('doQuery (inspect image): not implamented') - return {} + + protected override async queryImpl ({ address, message }) { + throw new Error('doQuery (inspect image): not implemented') + return {} as T } image ( @@ -109,6 +90,26 @@ class OCIConnection extends Chain.Connection { } } +class OCIAgent extends Chain.Agent { + protected override async sendImpl (_) { + throw new Error('send: not applicable') + } + + protected override async uploadImpl (_) { + throw new Error('upload (load/import image): not implemented') + return {} + } + + protected override async instantiateImpl (_) { + throw new Error('instantiate (create container): not implemented') + } + + protected override async executeImpl () { + throw new Error('execute (run in container): not implemented') + return {} as T + } +} + class OCIImage extends Deploy.ContractTemplate { constructor (properties: Partial = {}) { diff --git a/packages/scrt/scrt-bank.ts b/packages/scrt/scrt-bank.ts new file mode 100644 index 00000000000..91a0f535b23 --- /dev/null +++ b/packages/scrt/scrt-bank.ts @@ -0,0 +1,16 @@ +import { withIntoError } from './scrt-core' + +export async function fetchBalance ({ api }, parameters) { + return (await withIntoError(this.api.query.bank.balance({ + address, + denom + }))) + .balance!.amount! +} + +export async function send ({ api }, parameters) { + return withIntoError(this.api.tx.bank.send( + { from_address: this.address!, to_address: recipient, amount: amounts }, + { gasLimit: Number(options?.sendFee?.gas) } + )) +} diff --git a/packages/scrt/scrt-base.ts b/packages/scrt/scrt-base.ts index 7d59a9dd29c..ace97ccb37a 100644 --- a/packages/scrt/scrt-base.ts +++ b/packages/scrt/scrt-base.ts @@ -23,3 +23,12 @@ export const { randomBase64, randomBech32, } = Core + +export const withIntoError = (p: Promise): Promise => + p.catch(intoError) + +const intoError = async (e: object)=>{ + e = await Promise.resolve(e) + console.error(e) + throw Object.assign(new Error(), e) +} diff --git a/packages/scrt/scrt-compute.ts b/packages/scrt/scrt-compute.ts new file mode 100644 index 00000000000..1487d98887f --- /dev/null +++ b/packages/scrt/scrt-compute.ts @@ -0,0 +1,310 @@ +import type { TxResponse } from '@hackbg/secretjs-esm' +import { Deploy, Chain } from '@fadroma/agent' +import { bold, withIntoError } from './scrt-base' +import faucets from './scrt-faucets' +import type { ScrtConnection, ScrtAgent } from './scrt-connection' + +export async function fetchCodeInfo ( + conn: ScrtConnection, + args: Parameters[0] +): + Promise> +{ + const api = await Promise.resolve(conn.api) + const { chainId } = conn + const result = {} + await withIntoError(this.api.query.compute.codes({})).then(({code_infos})=>{ + for (const { code_id, code_hash, creator } of code_infos||[]) { + if (!args.codeIds || args.codeIds.includes(code_id)) { + result[code_id!] = new Deploy.UploadedCode({ + chainId, + codeId: code_id, + codeHash: code_hash, + uploadBy: creator + }) + } + } + }) + return result +} + +export async function fetchCodeInstances ( + conn: ScrtConnection, + args: Parameters[0] +): + Promise>> +{ + const api = await Promise.resolve(conn.api) + const { chainId } = conn + if (args.parallel) { + conn.log.warn('fetchCodeInstances in parallel: not implemented') + } + const result = {} + for (const [codeId, Contract] of Object.entries(args.codeIds)) { + let codeHash + const instances = {} + await withIntoError(this.api.query.compute.codeHashByCodeId({ code_id: codeId })) + .then(({code_hash})=>codeHash = code_hash) + await withIntoError(this.api.query.compute.contractsByCodeId({ code_id: codeId })) + .then(({contract_infos})=>{ + for (const { contract_address, contract_info: { label, creator } } of contract_infos) { + result[contract_address] = new Contract({ + connection: conn, + codeId, + codeHash, + label, + address: contract_address, + initBy: creator + }) + } + }) + result[codeId] = instances + } + return result +} + +export async function fetchContractInfo ( + conn: ScrtConnection, + args: Parameters[0] +): + Promise<{ + [address in keyof typeof args["contracts"]]: InstanceType + }> +{ + const api = await Promise.resolve(conn.api) + const { chainId } = conn + if (args.parallel) { + conn.log.warn('fetchContractInfo in parallel: not implemented') + } + throw new Error('unimplemented!') + //protected override async fetchCodeHashOfAddressImpl (contract_address: Address): Promise { + //return (await withIntoError(this.api.query.compute.codeHashByContractAddress({ + //contract_address + //}))) + //.code_hash! + //} + + //async getLabel (contract_address: Address): Promise { + //return (await withIntoError(this.api.query.compute.contractInfo({ + //contract_address + //}))) + //.ContractInfo!.label! + //} +} + +export async function query ( + conn: ScrtConnection, + args: Parameters[0] +) { + const api = await Promise.resolve(conn.api) + return withIntoError(api.query.compute.queryContract({ + contract_address: args.address, + code_hash: args.codeHash, + query: args.message as Record + })) +} + +export async function upload ( + conn: ScrtAgent, + args: Parameters[0] +) { + + const api = await Promise.resolve(conn.api) + const { gasToken } = conn.connection.constructor as typeof ScrtConnection + + const result: { + code + message + details + rawLog + arrayLog + transactionHash + gasUsed + } = await withIntoError( + this.api!.tx.compute.storeCode({ + sender: this.address!, + wasm_byte_code: args.binary, + source: "", + builder: "" + }, { + gasLimit: Number(this.fees.upload?.amount[0].amount) || undefined + }) + ) + + const { + code, + message, + details = [], + rawLog + } = result + + if (code !== 0) { + this.log.error( + `Upload failed with code ${bold(code)}:`, + bold(message ?? rawLog ?? ''), + ...details + ) + if (message === `account ${this.address} not found`) { + this.log.info(`If this is a new account, send it some ${gasToken} first.`) + if (faucets[this.conn.chainId!]) { + this.log.info(`Available faucets\n `, [...faucets[this.conn.chainId!]].join('\n ')) + } + } + this.log.error(`Upload failed`, { result }) + throw new Error('upload failed') + } + + type Log = { type: string, key: string } + + const codeId = result.arrayLog + ?.find((log: Log) => log.type === "message" && log.key === "code_id") + ?.value + if (!codeId) { + this.log.error(`Code ID not found in result`, { result }) + throw new Error('upload failed') + } + const { codeHash } = await this.conn.fetchCodeInfo(codeId) + return new Deploy.ContractTemplate({ + chainId: this.conn.chainId, + codeId, + codeHash, + uploadBy: this.address, + uploadTx: result.transactionHash, + uploadGas: result.gasUsed + }) +} + +export async function instantiate ( + conn: ScrtAgent, + args: Parameters[0] +) { + if (!this.address) { + throw new Error("agent has no address") + } + + const parameters = { + sender: this.address, + code_id: Number(args.codeId), + code_hash: args.codeHash, + label: args.label!, + init_msg: args.initMsg, + init_funds: args.initSend, + memo: args.initMemo + } + const instantiateOptions = { + gasLimit: Number(this.fees.init?.amount[0].amount) || undefined + } + const result: { code, arrayLog, transactionHash, gasUsed } = await withIntoError( + this.api.tx.compute.instantiateContract(parameters, instantiateOptions) + ) + + if (result.code !== 0) { + this.log.error('Init failed:', { + parameters, + instantiateOptions, + result + }) + throw new Error(`init of code id ${args.codeId} failed`) + } + + type Log = { type: string, key: string } + const address = result.arrayLog! + .find((log: Log) => log.type === "message" && log.key === "contract_address") + ?.value! + + return new Deploy.ContractInstance({ + chainId: this.conn.chainId, + address, + codeHash: args.codeHash, + initBy: this.address, + initTx: result.transactionHash, + initGas: result.gasUsed, + label: args.label, + }) as Deploy.ContractInstance & { address: Chain.Address } +} + +export async function execute ( + conn: ScrtAgent, + args: Parameters[0] & { preSimulate?: boolean } +) { + const api = await Promise.resolve(conn.api) + + const tx = { + sender: conn.address!, + contract_address: args.address, + code_hash: args.codeHash, + msg: args.message as Record, + sentFunds: args?.execSend + } + + const txOpts = { + gasLimit: Number(args?.execFee?.gas) || undefined + } + + if (args?.preSimulate) { + this.log.info('Simulating transaction...') + let simResult + try { + simResult = await api.tx.compute.executeContract.simulate(tx, txOpts) + } catch (e) { + this.log.error(e) + this.log.warn('TX simulation failed:', tx, 'from', this) + } + const gas_used = simResult?.gas_info?.gas_used + if (gas_used) { + this.log.info('Simulation used gas:', gas_used) + const gas = Math.ceil(Number(gas_used) * 1.1) + // Adjust gasLimit up by 10% to account for gas estimation error + this.log.info('Setting gas to 110% of that:', gas) + txOpts.gasLimit = gas + } + } + + const result = await api.tx.compute.executeContract(tx, txOpts) + + // check error code as per https://grpc.github.io/grpc/core/md_doc_statuscodes.html + if (result.code !== 0) { + throw decodeError(result) + } + + return result as TxResponse +} + +export function decodeError (result: TxResponse) { + const error = `ScrtConnection#execute: gRPC error ${result.code}: ${result.rawLog}` + // make the original result available on request + const original = structuredClone(result) + Object.defineProperty(result, "original", { + enumerable: false, get () { return original } + }) + // decode the values in the result + const txBytes = tryDecode(result.tx as Uint8Array) + Object.assign(result, { txBytes }) + for (const i in result.tx.signatures) { + Object.assign(result.tx.signatures, { [i]: tryDecode(result.tx.signatures[i as any]) }) + } + for (const event of result.events) { + for (const attr of event?.attributes ?? []) { + //@ts-ignore + try { attr.key = tryDecode(attr.key) } catch (e) {} + //@ts-ignore + try { attr.value = tryDecode(attr.value) } catch (e) {} + } + } + return Object.assign(new Error(error), result) +} + +/** Used to decode Uint8Array-represented UTF8 strings in TX responses. */ +const decoder = new TextDecoder('utf-8', { fatal: true }) + +/** Marks a response field as non-UTF8 to prevent large binary arrays filling the console. */ +export const nonUtf8 = Symbol('(binary data, see result.original for the raw Uint8Array)') + +/** Decode binary response data or mark it as non-UTF8 */ +const tryDecode = (data: Uint8Array): string|Symbol => { + try { + return decoder.decode(data) + } catch (e) { + return nonUtf8 + } +} diff --git a/packages/scrt/scrt-connection.ts b/packages/scrt/scrt-connection.ts index 53981cee598..ab34269a8b5 100644 --- a/packages/scrt/scrt-connection.ts +++ b/packages/scrt/scrt-connection.ts @@ -7,6 +7,10 @@ import { ScrtError as Error, console, bold, base64 } from './scrt-base' import { ScrtIdentity, ScrtSignerIdentity, ScrtMnemonicIdentity } from './scrt-identity' import faucets from './scrt-faucets' //import * as Mocknet from './scrt-mocknet' +import * as Bank from './scrt-bank' +import * as Compute from './scrt-compute' +import * as Staking from './scrt-staking' +import * as Governance from './scrt-governance' import type { Uint128, Message, Address, TxHash, ChainId, CodeId, CodeHash } from '@fadroma/agent' import { Core, Chain, Token, Deploy, Batch } from '@fadroma/agent' @@ -16,23 +20,11 @@ export type { TxResponse } /** Represents a Secret Network API endpoint. */ export class ScrtConnection extends Chain.Connection { - /** Smallest unit of native token. */ static gasToken = new Token.Native('uscrt') - /** Underlying API client. */ - declare api: SecretNetworkClient - - /** Supports multiple authentication methods. */ - declare identity: ScrtIdentity - - /** Set permissive fees by default. */ - fees = { - upload: ScrtConnection.gasToken.fee(10000000), - init: ScrtConnection.gasToken.fee(10000000), - exec: ScrtConnection.gasToken.fee(1000000), - send: ScrtConnection.gasToken.fee(1000000), - } + declare api: + SecretNetworkClient constructor (properties?: Partial) { super(properties as Partial) @@ -44,27 +36,15 @@ export class ScrtConnection extends Chain.Connection { if (!url) { throw new Error("can't connect without url") } - if (this.identity) { - if (!(this.identity instanceof ScrtIdentity)) { - if (!(typeof this.identity === 'object')) { - throw new Error('identity must be ScrtIdentity instance, { mnemonic }, or { encryptionUtils }') - } else if ((this.identity as { mnemonic: string }).mnemonic) { - this.log.debug('Identifying with mnemonic') - this.identity = new ScrtMnemonicIdentity(this.identity) - } else if ((this.identity as { encryptionUtils: unknown }).encryptionUtils) { - this.log.debug('Identifying with signer (encryptionUtils)') - this.identity = new ScrtSignerIdentity(this.identity) - } else { - throw new Error('identity must be ScrtIdentity instance, { mnemonic }, or { encryptionUtils }') - } - } - this.api = this.identity.getApi({ chainId, url }) - } else { - this.api = new SecretNetworkClient({ chainId, url }) - } + this.api = new SecretNetworkClient({ chainId, url }) } - - protected override async fetchBlockImpl (parameter): Promise { + authenticate (identity: ScrtIdentity): ScrtAgent { + return new ScrtAgent({ connection: this, identity }) + } + batch (): Batch { + return new ScrtBatch({ connection: this }) as unknown as Batch + } + protected override async fetchBlockImpl (parameter?): Promise { if (!parameter) { const { block_id: { hash, part_set_header } = {}, @@ -76,337 +56,115 @@ export class ScrtConnection extends Chain.Connection { }) } } - - protected override async fetchCodeInfoImpl () {} - - protected override async fetchCodeInstancesImpl () {} - - protected override async fetchContractInfoImpl () {} - - authenticate (identity: ScrtIdentity): Promise { - return new ScrtAgent({ connection: this, identity }) + protected override async fetchHeightImpl () { + const { height } = await this.fetchBlockImpl() + return height } - - protected override fetchHeightImpl () { - return this.fetchBlockInfoImpl().then((block: any)=> - Number(block.block?.header?.height)) + protected override async fetchBalanceImpl (parameters) { + return await Bank.fetchBalance(this, parameters) } - - protected override fetchCodesImpl () { - const codes: Record = {} - return withIntoError(this.api.query.compute.codes({})) - .then(({code_infos})=>{ - for (const { code_id, code_hash, creator } of code_infos||[]) { - codes[code_id!] = new Deploy.UploadedCode({ - chainId: this.chainId, - codeId: code_id, - codeHash: code_hash, - uploadBy: creator - }) - } - return codes - }) + protected override async fetchCodeInfoImpl (parameters) { + return await Compute.fetchCodeInfo(this, parameters) } - - protected override async fetchCodeIdImpl (contract_address: Address): Promise { - return (await withIntoError(this.api.query.compute.contractInfo({ - contract_address - }))) - .ContractInfo!.code_id! + protected override async fetchCodeInstancesImpl (parameters) { + return await Compute.fetchCodeInstances(this, parameters) } - - protected override async fetchContractsByCodeIdImpl (code_id: CodeId): Promise> { - return (await withIntoError(this.api.query.compute.contractsByCodeId({ code_id }))) - .contract_infos! - .map(({ contract_address, contract_info: { label, creator } }: any)=>({ - label, - address: contract_address, - initBy: creator - })) + protected override async fetchContractInfoImpl (parameters) { + return await Compute.fetchContractInfo(this, parameters) } - - protected override async fetchCodeHashOfAddressImpl (contract_address: Address): Promise { - return (await withIntoError(this.api.query.compute.codeHashByContractAddress({ - contract_address - }))) - .code_hash! + protected override async queryImpl (parameters): Promise { + return await Compute.query(this, parameters) as T } - - protected override async fetchCodeHashOfCodeIdImpl (code_id: CodeId): Promise { - return (await withIntoError(this.api.query.compute.codeHashByCodeId({ - code_id - }))) - .code_hash! + async fetchLimits (): Promise<{ gas: number }> { + const params = { subspace: "baseapp", key: "BlockParams" } + const { param } = await this.api.query.params.params(params) + let { max_bytes, max_gas } = JSON.parse(param?.value??'{}') + this.log.debug(`Fetched default gas limit: ${max_gas} and code size limit: ${max_bytes}`) + if (max_gas < 0) { + max_gas = 10000000 + this.log.warn(`Chain returned negative max gas limit. Defaulting to: ${max_gas}`) + } + return { gas: max_gas } } +} - protected override async fetchBalanceImpl ( - denom: string = this.defaultDenom, - address: string|undefined = this.address - ) { - return (await withIntoError(this.api.query.bank.balance({ - address, - denom - }))) - .balance!.amount! - } - - async getLabel (contract_address: Address): Promise { - return (await withIntoError(this.api.query.compute.contractInfo({ - contract_address - }))) - .ContractInfo!.label! - } - - /** Query a contract. - * @returns the result of the query */ - protected override queryImpl (contract: { address: Address, codeHash: CodeHash }, message: Message): Promise { - return withIntoError(this.api.query.compute.queryContract({ - contract_address: contract.address, - code_hash: contract.codeHash, - query: message as Record - })) +export class ScrtAgent extends Chain.Agent { + + api: SecretNetworkClient + + declare connection: ScrtConnection + + declare identity: ScrtIdentity + + /** Set permissive fees by default. */ + fees = { + upload: ScrtConnection.gasToken.fee(10000000), + init: ScrtConnection.gasToken.fee(10000000), + exec: ScrtConnection.gasToken.fee(1000000), + send: ScrtConnection.gasToken.fee(1000000), } - get account (): ReturnType { - return this.api.query.auth.account({ address: this.address }) + constructor (properties: Partial = {}) { + super(properties) + if (!(this.identity instanceof ScrtIdentity)) { + if (!(typeof this.identity === 'object')) { + throw new Error('identity must be ScrtIdentity instance, { mnemonic }, or { encryptionUtils }') + } else if ((this.identity as { mnemonic?: string }).mnemonic) { + this.log.debug('Identifying with mnemonic') + this.identity = new ScrtMnemonicIdentity(this.identity) + } else if ((this.identity as { encryptionUtils?: unknown }).encryptionUtils) { + this.log.debug('Identifying with signer (encryptionUtils)') + this.identity = new ScrtSignerIdentity(this.identity) + } else { + throw new Error('identity must be ScrtIdentity instance, { mnemonic }, or { encryptionUtils }') + } + } + const { chainId, url } = this.connection + this.api = this.identity.getApi({ chainId, url }) } async setMaxGas (): Promise { - const max = ScrtConnection.gasToken.fee((await this.fetchLimits()).gas) + const { gas } = await this.connection.fetchLimits() + const max = ScrtConnection.gasToken.fee(gas) this.fees = { upload: max, init: max, exec: max, send: max } return this } - async fetchLimits (): Promise<{ gas: number }> { - const params = { subspace: "baseapp", key: "BlockParams" } - const { param } = await this.api.query.params.params(params) - let { max_bytes, max_gas } = JSON.parse(param?.value??'{}') - this.log.debug(`Fetched default gas limit: ${max_gas} and code size limit: ${max_bytes}`) - if (max_gas < 0) { - max_gas = 10000000 - this.log.warn(`Chain returned negative max gas limit. Defaulting to: ${max_gas}`) - } - return { gas: max_gas } + get account (): ReturnType { + return this.api.query.auth.account({ address: this.address }) } async getNonce (): Promise<{ accountNumber: number, sequence: number }> { - const result: any = await this.api.query.auth.account({ address: this.address }) ?? (() => { + const result: any = await this.account ?? (() => { throw new Error(`Cannot find account "${this.address}", make sure it has a balance.`) - }) + })() const { account_number, sequence } = result.account return { accountNumber: Number(account_number), sequence: Number(sequence) } } - async encrypt (codeHash: CodeHash, msg: Message) { if (!codeHash) { throw new Error("can't encrypt message without code hash") } - const { encryptionUtils } = this.api as any + const { encryptionUtils } = await Promise.resolve(this.api) as any const encrypted = await encryptionUtils.encrypt(codeHash, msg as object) return base64.encode(encrypted) } - batch (): Batch { - return new ScrtBatch({ connection: this }) as unknown as Batch + protected async sendImpl (...args: Parameters) { + return await Bank.send(this, ...args) } - -} - -export class ScrtAgent extends Chain.Agent { - - protected async sendImpl ( - recipient: Address, - amounts: Token.ICoin[], - options?: Parameters[2] - ) { - return withIntoError(this.api.tx.bank.send( - { from_address: this.address!, to_address: recipient, amount: amounts }, - { gasLimit: Number(options?.sendFee?.gas) } - )) - } - - protected sendManyImpl ( - outputs: [Address, Token.ICoin[]][], options?: unknown - ): Promise { - throw new Error('unimplemented') - } - - async sendMany (outputs: never, opts?: any) { - throw new Error('ScrtConnection#sendMany: not implemented') - } - - /** Upload a WASM binary. */ - protected async uploadImpl (data: Uint8Array): Promise> { - const request = { sender: this.address!, wasm_byte_code: data, source: "", builder: "" } - const gasLimit = Number(this.fees.upload?.amount[0].amount) || undefined - const result = await withIntoError(this.api!.tx.compute.storeCode(request, { gasLimit })) - const { code, message, details = [], rawLog } = result as any - if (code !== 0) { - this.log.error( - `Upload failed with code ${bold(code)}:`, - bold(message ?? rawLog ?? ''), - ...details - ) - if (message === `account ${this.address} not found`) { - this.log.info(`If this is a new account, send it some ${ScrtConnection.gasToken} first.`) - if (faucets[this.connection.chainId!]) { - this.log.info(`Available faucets\n `, [...faucets[this.connection.chainId!]].join('\n ')) - } - } - this.log.error(`Upload failed`, { result }) - throw new Error('upload failed') - } - type Log = { type: string, key: string } - const codeId = result.arrayLog - ?.find((log: Log) => log.type === "message" && log.key === "code_id") - ?.value - if (!codeId) { - this.log.error(`Code ID not found in result`, { result }) - throw new Error('upload failed') - } - const codeHash = await this.connection.getCodeHashOfCodeId(codeId) - return { - chainId: this.connection.chainId, - codeId, - codeHash, - uploadBy: this.address, - uploadTx: result.transactionHash, - uploadGas: result.gasUsed - } - } - - protected async instantiateImpl ( - codeId: CodeId, - options: Parameters[1] - ): Promise> { - if (!this.address) throw new Error("agent has no address") - const parameters = { - sender: this.address, - code_id: Number(codeId), - code_hash: options.codeHash, - label: options.label!, - init_msg: options.initMsg, - init_funds: options.initSend, - memo: options.initMemo - } - const instantiateOptions = { gasLimit: Number(this.fees.init?.amount[0].amount) || undefined } - const result = await withIntoError( - this.api.tx.compute.instantiateContract(parameters, instantiateOptions)) - if (result.code !== 0) { - this.log.error('Init failed:', { parameters, instantiateOptions, result }) - throw new Error(`init of code id ${codeId} failed`) - } - - type Log = { type: string, key: string } - const address = result.arrayLog! - .find((log: Log) => log.type === "message" && log.key === "contract_address") - ?.value! - - return { - chainId: this.connection.chainId, - address, - codeHash: options.codeHash, - initBy: this.address, - initTx: result.transactionHash, - initGas: result.gasUsed, - label: options.label, - } + protected async uploadImpl (...args: Parameters) { + return await Compute.upload(this, ...args) } - - protected async executeImpl ( - contract: { address: Address, codeHash: CodeHash }, - message: Message, - options?: Parameters[2] & { - preSimulate?: boolean - } - ): Promise { - const tx = { - sender: this.address!, - contract_address: contract.address, - code_hash: contract.codeHash, - msg: message as Record, - sentFunds: options?.execSend - } - const txOpts = { - gasLimit: Number(options?.execFee?.gas) || undefined - } - if (options?.preSimulate) { - this.log.info('Simulating transaction...') - let simResult - try { - simResult = await this.api.tx.compute.executeContract.simulate(tx, txOpts) - } catch (e) { - this.log.error(e) - this.log.warn('TX simulation failed:', tx, 'from', this) - } - const gas_used = simResult?.gas_info?.gas_used - if (gas_used) { - this.log.info('Simulation used gas:', gas_used) - const gas = Math.ceil(Number(gas_used) * 1.1) - // Adjust gasLimit up by 10% to account for gas estimation error - this.log.info('Setting gas to 110% of that:', gas) - txOpts.gasLimit = gas - } - } - const result = await this.api.tx.compute.executeContract(tx, txOpts) - // check error code as per https://grpc.github.io/grpc/core/md_doc_statuscodes.html - if (result.code !== 0) { - throw decodeError(result) - } - return result as TxResponse + protected async instantiateImpl (...args: Parameters) { + return await Compute.instantiate(this, ...args) } -} - -export class ScrtBlock extends Chain.Block { -} - -export function decodeError (result: TxResponse) { - const error = `ScrtConnection#execute: gRPC error ${result.code}: ${result.rawLog}` - // make the original result available on request - const original = structuredClone(result) - Object.defineProperty(result, "original", { - enumerable: false, get () { return original } - }) - // decode the values in the result - const txBytes = tryDecode(result.tx as Uint8Array) - Object.assign(result, { txBytes }) - for (const i in result.tx.signatures) { - Object.assign(result.tx.signatures, { [i]: tryDecode(result.tx.signatures[i as any]) }) - } - for (const event of result.events) { - for (const attr of event?.attributes ?? []) { - //@ts-ignore - try { attr.key = tryDecode(attr.key) } catch (e) {} - //@ts-ignore - try { attr.value = tryDecode(attr.value) } catch (e) {} - } + protected async executeImpl (...args: Parameters): Promise { + return await Compute.execute(this, ...args) as T } - return Object.assign(new Error(error), result) } -const withIntoError = (p: Promise): Promise => - p.catch(intoError) - -const intoError = async (e: object)=>{ - e = await Promise.resolve(e) - console.error(e) - throw Object.assign(new Error(), e) -} - -/** Used to decode Uint8Array-represented UTF8 strings in TX responses. */ -const decoder = new TextDecoder('utf-8', { fatal: true }) - -/** Marks a response field as non-UTF8 to prevent large binary arrays filling the console. */ -export const nonUtf8 = Symbol('(binary data, see result.original for the raw Uint8Array)') - -/** Decode binary response data or mark it as non-UTF8 */ -const tryDecode = (data: Uint8Array): string|Symbol => { - try { - return decoder.decode(data) - } catch (e) { - return nonUtf8 - } -} +export class ScrtBlock extends Chain.Block {} function removeTrailingSlash (url: string) { while (url.endsWith('/')) { url = url.slice(0, url.length - 1) } diff --git a/packages/scrt/scrt-governance.ts b/packages/scrt/scrt-governance.ts new file mode 100644 index 00000000000..53d9ac782e2 --- /dev/null +++ b/packages/scrt/scrt-governance.ts @@ -0,0 +1,3 @@ +export async function fetchProposals () { + throw new Error('not implemented') +} diff --git a/packages/scrt/scrt-staking.ts b/packages/scrt/scrt-staking.ts new file mode 100644 index 00000000000..37ede78d152 --- /dev/null +++ b/packages/scrt/scrt-staking.ts @@ -0,0 +1,3 @@ +export async function fetchValidators () { + throw new Error('not implemented') +}