diff --git a/packages/agent/src/Block.ts b/packages/agent/src/Block.ts index 9cd2ad0eee..e73383b9e3 100644 --- a/packages/agent/src/Block.ts +++ b/packages/agent/src/Block.ts @@ -1,7 +1,7 @@ /** Fadroma. Copyright (C) 2023 Hack.bg. License: GNU AGPLv3 or custom. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . **/ -import { bold } from './Util' +import { assign, bold } from './Util' import type { Chain, Transaction } from '../index' /** The building block of a blockchain, as obtained by @@ -9,22 +9,24 @@ import type { Chain, Transaction } from '../index' * * Contains zero or more transactions. */ export abstract class Block { - constructor (properties: Pick) { - this.#chain = properties.chain - this.height = properties.height - this.id = properties.id - this.timestamp = properties.timestamp + constructor ( + properties: Pick + ) { + this.#chain = properties.chain + assign(this, properties, [ "id", "height", "timestamp", "transactions" ]) } #chain: Chain get chain () { return this.#chain } /** Unique ID of block. */ - id: string + id!: string /** Monotonically incrementing ID of block. */ - height: number + height!: number /** Timestamp of block */ - timestamp?: string + timestamp?: string + /** Transactions in block */ + transactions!: unknown[] } export async function fetchBlock (chain: Chain, ...args: Parameters): diff --git a/packages/agent/src/Chain.ts b/packages/agent/src/Chain.ts index 1c06fd1846..097c87f6a8 100644 --- a/packages/agent/src/Chain.ts +++ b/packages/agent/src/Chain.ts @@ -1,32 +1,17 @@ /** Fadroma. Copyright (C) 2023 Hack.bg. License: GNU AGPLv3 or custom. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . **/ -import { - Logged, assign, bold, timed -} from './Util' -import { - fetchBalance -} from './dlt/Bank' -import { - Contract, - fetchCodeInstances, - query -} from './compute/Contract' -import { - UploadedCode, - fetchCodeInfo, -} from './compute/Upload' -import { - Block, - fetchBlock, - nextBlock -} from './Block' +import { Logged, assign, bold, timed } from './Util' +import { fetchBalance } from './dlt/Bank' +import { Contract, fetchCodeInstances, query } from './compute/Contract' +import { UploadedCode, fetchCodeInfo, } from './compute/Upload' +import { Block, fetchBlock, nextBlock } from './Block' +import { Connection } from './Connection' import type { Address, Agent, ChainId, CodeId, - Connection, Identity, Message, Token, @@ -34,6 +19,11 @@ import type { } from '../index' export abstract class Chain extends Logged { + + static get Connection () { + return Connection + } + constructor ( properties: ConstructorParameters[0] & Pick @@ -50,15 +40,11 @@ export abstract class Chain extends Logged { /** Time to ping for next block. */ blockInterval = 250 - /** Get a connection to the API endpoint. */ + /** Get a read-only connection to the API endpoint. */ abstract getConnection (): Connection - /** Authenticate with a random identity. */ - abstract authenticate (): Promise - /** Authenticate with a mnemonic. */ - abstract authenticate (mnemonic: string): Promise - /** Authenticate with the provided identity. */ - abstract authenticate (identity: Identity): Promise + /** Authenticate to the chain, obtaining an Agent instance that can send transactions. */ + abstract authenticate (properties?: { mnemonic: string }|Identity): Promise /** Get the current block height. */ get height (): Promise { diff --git a/packages/agent/src/Connection.ts b/packages/agent/src/Connection.ts index 282f094673..70c92fa712 100644 --- a/packages/agent/src/Connection.ts +++ b/packages/agent/src/Connection.ts @@ -6,7 +6,7 @@ import { } from './Util' import type { Address, Block, Chain, ChainId, CodeId, Message, Token, Uint128, - UploadStore, UploadedCode, Contract, Into + UploadStore, UploadedCode, Contract, Into, Identity } from '../index' /** Represents a remote API endpoint. @@ -17,14 +17,12 @@ import type { export abstract class Connection extends Logged { constructor ( properties: ConstructorParameters[0] - & Pick + & Pick & Partial> ) { super(properties) - this.#chain = properties.chain - this.url = properties.url - this.alive = properties.alive || true - this.api = properties.api + this.#chain = properties.chain + assign(this, properties, [ "url", "alive" ]) this.log.label = [ this.constructor.name, '(', this[Symbol.toStringTag] ? `(${bold(this[Symbol.toStringTag])})` : null, ')' @@ -33,7 +31,12 @@ export abstract class Connection extends Logged { const chainColor = randomColor({ luminosity: 'dark', seed: this.url }) this.log.label = colors.bgHex(chainColor).whiteBright(` ${this.url} `) } - + get [Symbol.toStringTag] () { + if (this.url) { + const color = randomColor({ luminosity: 'dark', seed: this.url }) + return colors.bgHex(color).whiteBright(this.url) + } + } #chain: Chain /** Chain to which this connection points. */ get chain (): Chain { @@ -43,31 +46,14 @@ export abstract class Connection extends Logged { get chainId (): ChainId { return this.chain.chainId } - /** Connection URL. * * The same chain may be accessible via different endpoints, so * this property contains the URL to which requests are sent. */ - url: string - /** Instance of platform SDK. This must be provided in a subclass. - * - * Since most chain SDKs initialize asynchronously, this is usually a `Promise` - * that resolves to an instance of the underlying client class (e.g. `CosmWasmClient` or `SecretNetworkClient`). - * - * Since transaction and query methods are always asynchronous as well, well-behaved - * implementations of Fadroma Agent begin each method that talks to the chain with - * e.g. `const { api } = await this.api`, making sure an initialized platform SDK instance - * is available. */ - api: unknown + url!: string /** Setting this to false stops retries. */ alive: boolean = true - get [Symbol.toStringTag] () { - if (this.url) { - const color = randomColor({ luminosity: 'dark', seed: this.url }) - return colors.bgHex(color).whiteBright(this.url) - } - } /** Chain-specific implementation of fetchBlock. */ abstract fetchBlockImpl (parameters?: { height: number }|{ hash: string } @@ -77,9 +63,9 @@ export abstract class Connection extends Logged { Promise /** Chain-specific implementation of fetchBalance. */ abstract fetchBalanceImpl (parameters: { - token?: string, - address?: string - }): Promise + addresses: Record, + parallel?: boolean + }): Promise>> /** Chain-specific implementation of fetchCodeInfo. */ abstract fetchCodeInfoImpl (parameters?: { codeIds?: CodeId[] @@ -108,6 +94,25 @@ export abstract class Connection extends Logged { /** Extend this class and implement the abstract methods to add support for a new kind of chain. */ export abstract class SigningConnection { + constructor (properties: { chain: Chain, identity: Identity }) { + this.#chain = properties.chain + this.#identity = properties.identity + } + #chain: Chain + /** Chain to which this connection points. */ + get chain (): Chain { + return this.#chain + } + get chainId (): ChainId { + return this.chain.chainId + } + #identity: Identity + get identity (): Identity { + return this.#identity + } + get address (): Address { + return this.identity.address! + } /** Chain-specific implementation of native token transfer. */ abstract sendImpl (parameters: { outputs: Record>, @@ -120,14 +125,19 @@ export abstract class SigningConnection { binary: Uint8Array, reupload?: boolean, uploadStore?: UploadStore, - uploadFee?: Token.ICoin[]|'auto', + uploadFee?: Token.IFee uploadMemo?: string }): Promise> /** Chain-specific implementation of contract instantiation. */ - abstract instantiateImpl (parameters: Partial & { initMsg: Into }): + abstract instantiateImpl (parameters: Partial & { + initMsg: Into + initFee?: Token.IFee + initSend?: Token.ICoin[] + initMemo?: string + }): Promise /** Chain-specific implementation of contract transaction. */ abstract executeImpl (parameters: { diff --git a/packages/agent/src/Identity.ts b/packages/agent/src/Identity.ts index 3d80a22759..cb1c2f71a0 100644 --- a/packages/agent/src/Identity.ts +++ b/packages/agent/src/Identity.ts @@ -6,13 +6,15 @@ import type { Address } from './Types' /** A cryptographic identity. */ export class Identity extends Logged { - constructor (properties?: Partial) { + constructor ( + properties: ConstructorParameters[0] & Pick = {} + ) { super(properties) assign(this, properties, ['name', 'address']) } /** Display name. */ - name?: Address - /** Unique identifier. */ + name?: Address + /** Address of account. */ address?: Address /** Sign some data with the identity's private key. */ sign (doc: any): unknown { diff --git a/packages/agent/src/Util.ts b/packages/agent/src/Util.ts index 72cb0e8d5f..0c2076868b 100644 --- a/packages/agent/src/Util.ts +++ b/packages/agent/src/Util.ts @@ -26,3 +26,14 @@ export async function timed ( }) return result } + +export async function optionallyParallel (parallel: boolean|undefined, thunks: Array<()=>Promise>) { + if (parallel) { + return await Promise.all(thunks.map(thunk=>thunk())) + } + const results = [] + for (const thunk of thunks) { + results.push(await thunk()) + } + return results +} diff --git a/packages/agent/src/dlt/Staking.ts b/packages/agent/src/dlt/Staking.ts index 85a6641928..385d73126d 100644 --- a/packages/agent/src/dlt/Staking.ts +++ b/packages/agent/src/dlt/Staking.ts @@ -2,13 +2,16 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . **/ import { assign } from '../Util' -import type { Connection, Address } from '../../index' +import type { Chain, Address } from '../../index' export class Validator { - chain?: Connection - address: Address - constructor (properties: Pick & Partial>) { - this.chain = properties.chain - this.address = properties.address + constructor (properties: Pick) { + this.#chain = properties.chain + assign(this, properties, [ "address" ]) } + #chain: Chain + get chain () { + return this.#chain + } + address!: Address } diff --git a/packages/agent/stub/StubChain.ts b/packages/agent/stub/StubChain.ts index 5354a9a2df..9a881a29af 100644 --- a/packages/agent/stub/StubChain.ts +++ b/packages/agent/stub/StubChain.ts @@ -51,11 +51,7 @@ export class StubChain extends Chain { } getConnection (): StubConnection { - return new StubConnection({ - chain: this, - url: 'stub', - api: {}, - }) + return new StubConnection({ chain: this, url: 'stub', }) } } @@ -80,21 +76,22 @@ export class StubConnection extends Connection { override fetchBlockImpl (): Promise { const timestamp = new Date() return Promise.resolve(new StubBlock({ - chain: this.chain, - id: `stub${+timestamp}`, - height: +timestamp, - timestamp: timestamp.toISOString() + chain: this.chain, + id: `stub${+timestamp}`, + height: +timestamp, + timestamp: timestamp.toISOString(), + transactions: [] })) } override fetchBalanceImpl ( ...args: Parameters - ): Promise { + ) { throw new Error('unimplemented!') //token ??= this.defaultDenom //const balance = (this.backend.balances.get(address!)||{})[token] ?? 0 //return Promise.resolve(String(balance)) - return Promise.resolve('') + return Promise.resolve({}) } override fetchCodeInfoImpl ( diff --git a/packages/agent/stub/StubIdentity.ts b/packages/agent/stub/StubIdentity.ts index a81b5651a5..2ef746ad1c 100644 --- a/packages/agent/stub/StubIdentity.ts +++ b/packages/agent/stub/StubIdentity.ts @@ -24,8 +24,8 @@ export class StubAgent extends Agent { getConnection (): StubSigningConnection { return new StubSigningConnection({ - address: this.identity.address!, - backend: this.chain.backend + chain: this.chain, + identity: this.identity, }) } @@ -35,15 +35,12 @@ export class StubAgent extends Agent { } export class StubSigningConnection extends SigningConnection { - constructor (properties: Pick) { - super() - this.backend = properties.backend - this.address = properties.address + get chain (): StubChain { + return super.chain as unknown as StubChain + } + get backend (): StubBackend { + return this.chain.backend } - - backend: StubBackend - - address: Address async sendImpl (...args: Parameters): Promise { const { backend } = this diff --git a/packages/cw/cw-bank.ts b/packages/cw/cw-bank.ts index 540fecdbe6..3cea773e8f 100644 --- a/packages/cw/cw-bank.ts +++ b/packages/cw/cw-bank.ts @@ -1,30 +1,58 @@ +import { optionallyParallel } from '@fadroma/agent' import type { CosmWasmClient, SigningCosmWasmClient } from '@hackbg/cosmjs-esm' import type { Address, Token, Chain, Connection, SigningConnection } from '@fadroma/agent' import type { CWChain, CWConnection } from './cw-connection' import type { CWAgent, CWSigningConnection } from './cw-identity' -export async function fetchBalance ( - connection: CWConnection, balances: Parameters[0] -) { - if (!address) { - throw new Error('getBalance: need address') +export async function fetchBalance (chain: CWConnection, { + parallel = false, + addresses, +}: Parameters[0]) { + const queries = [] + for (const [address, tokens] of Object.entries(addresses)) { + for (const token of tokens) { + queries.push(()=>chain.api.getBalance(address, token).then(balance=>({ + address, token, balance + }))) + } } - const { amount } = await connection.api.getBalance(address, token) - return amount + const result: Record> = {} + const responses = await optionallyParallel(parallel, queries) + for (const { address, token, balance } of responses) { + result[address] ??= {} + result[address][token] = balance.amount + } + return result } -export async function send ( - { api, address }: CWSigningConnection, - { outputs - , sendFee = 'auto' - , sendMemo - , parallel }: Parameters[0] -) { - return api.sendTokens( - address!, - recipient as string, - amounts, - sendFee, - sendMemo - ) +export async function send (agent: CWSigningConnection, { + parallel = false, + outputs, + sendFee, + sendMemo, +}: Parameters[0]) { + const sender = agent.address + const transactions = [] + for (const [recipient, amounts] of Object.entries(outputs)) { + transactions.push(()=>agent.api.sendTokens( + sender, + recipient, + Object.entries(amounts).map(([denom, amount])=>({amount, denom})), + sendFee || 'auto', + sendMemo + ).then(transaction=>({ + sender, recipient, amounts, transaction + }))) + } + const result: Record + transaction: unknown + }> = {} + const responses = await optionallyParallel(parallel, transactions) + for (const response of responses) { + result[response.recipient] = response + } + return result } diff --git a/packages/cw/cw-compute.ts b/packages/cw/cw-compute.ts index 54db6cc746..f4bbd032ae 100644 --- a/packages/cw/cw-compute.ts +++ b/packages/cw/cw-compute.ts @@ -68,7 +68,7 @@ export async function getCodes ( const results = await chain.api.getCodes() for (const { id, checksum, creator } of results||[]) { codes[id!] = new UploadedCode({ - chainId: chainId, + chainId: chain.chainId, codeId: String(id), codeHash: checksum, uploadBy: creator @@ -130,7 +130,7 @@ export async function query ( } export async function upload ( - { api, address }: CWSigningConnection, + { chainId, address, api }: CWSigningConnection, options: Parameters[0] ) { if (!address) { @@ -139,7 +139,7 @@ export async function upload ( const result = await api.upload( address!, options.binary, - fees?.upload || 'auto', + options.uploadFee as Amino.StdFee || 'auto', "Uploaded by Fadroma" ) return { @@ -153,7 +153,7 @@ export async function upload ( } export async function instantiate ( - { address, api }: CWSigningConnection, + { chain, address, api }: CWSigningConnection, options: Parameters[0] ) { const result = await (api as SigningCosmWasmClient).instantiate( @@ -165,18 +165,17 @@ export async function instantiate ( { admin: address, funds: options.initSend, memo: options.initMemo } ) return new Contract({ + chain, codeId: options.codeId, codeHash: options.codeHash, label: options.label, - initMsg: options.initMsg, - chainId: chainId, address: result.contractAddress, - initTx: result.transactionHash, - initGas: result.gasUsed, + //initTx: result.transactionHash, + //initGas: result.gasUsed, initBy: address, - initFee: options.initFee || 'auto', - initSend: options.initSend, - initMemo: options.initMemo + //initFee: options.initFee || 'auto', + //initSend: options.initSend, + //initMemo: options.initMemo }) as Contract & { address: Address } } diff --git a/packages/cw/cw-connection.ts b/packages/cw/cw-connection.ts index 8d0e900086..febd26d863 100644 --- a/packages/cw/cw-connection.ts +++ b/packages/cw/cw-connection.ts @@ -1,7 +1,6 @@ import { bold, assign, Chain, Connection } from '@fadroma/agent' -import type { Address, Message, CodeId, CodeHash, Token } from '@fadroma/agent' -import { CWAgent } from './cw-identity' -import type { CWIdentity, CWMnemonicIdentity, CWSignerIdentity } from './cw-identity' +import type { Address, Message, CodeId, CodeHash, Token, ChainId } from '@fadroma/agent' +import { CWAgent, CWSigningConnection, CWIdentity, CWMnemonicIdentity, CWSignerIdentity } from './cw-identity' import { CWConsole as Console, CWError as Error } from './cw-base' import { CWBlock, CWBatch } from './cw-tx' import * as CWBank from './cw-bank' @@ -11,42 +10,92 @@ import { Amino, Proto, CosmWasmClient, SigningCosmWasmClient } from '@hackbg/cos import type { Block } from '@hackbg/cosmjs-esm' export class CWChain extends Chain { - constructor (properties: Partial = {}) { + + static get Connection () { + return CWConnection + } + + static async connect ( + properties: { chainId?: ChainId }&({ url: string|URL }|{ urls: Iterable }) + ): Promise { + const { chainId, url, urls = [ url ] } = properties as any + const chain = new this({ + chainId, + connections: [], + bech32Prefix: 'tnam' + }) + const connections: CWConnection[] = urls.map(async (url: string|URL)=>new this.Connection({ + api: await CosmWasmClient.connect(String(url)), + chain, + url: String(url) + })) + chain.connections = await Promise.all(connections) + return chain + } + + constructor ( + properties: ConstructorParameters[0] + & Pick + ) { super(properties) - assign(this, properties, [ - 'coinType', - 'bech32Prefix', - 'hdAccountIndex' - ]) + this.coinType = properties.coinType + this.bech32Prefix = properties.bech32Prefix + this.hdAccountIndex = properties.hdAccountIndex + this.connections = properties.connections } /** The bech32 prefix for the account's address */ - bech32Prefix?: string + bech32Prefix: string /** The coin type in the HD derivation path */ coinType?: number /** The account index in the HD derivation path */ hdAccountIndex?: number - #connection: CWConnection + connections: CWConnection[] getConnection (): CWConnection { - return this.#connection - } - - async authenticate (...args): Promise { - return new CWAgent({ chain: this }) + return this.connections[0] + } + + async authenticate ( + ...args: Parameters + ): Promise { + let identity: CWIdentity + if (!args[0]) { + identity = new CWMnemonicIdentity({}) + } else if (typeof (args[0] as any).mnemonic === 'string') { + identity = new CWMnemonicIdentity({ + mnemonic: (args[0] as any).mnemonic + }) + } else if (args[0] instanceof CWIdentity) { + identity = args[0] + } else if (typeof args[0] === 'object') { + identity = new CWIdentity(args[0] as Partial) + } else { + throw Object.assign(new Error('Invalid arguments'), { args }) + } + return new CWAgent({ + chain: this, + identity, + connection: new CWSigningConnection({ + chain: this, + identity, + api: await SigningCosmWasmClient.connectWithSigner( + this.getConnection().url, + identity.signer, + ) + }) + }) } } -/** Generic agent for CosmWasm-enabled chains. */ +/** Read-only client for CosmWasm-enabled chains. */ export class CWConnection extends Connection { /** API connects asynchronously, so API handle is a promise. */ declare api: CosmWasmClient - constructor (properties: Partial) { + constructor (properties: ConstructorParameters[0] & { api: CosmWasmClient }) { super(properties) - assign(this, properties, [ - 'api', - ]) + this.api = properties.api //if (!this.url) { //throw new Error('No connection URL.') //} @@ -62,10 +111,6 @@ export class CWConnection extends Connection { //} } - override authenticate (identity: CWIdentity): CWAgent { - return new CWAgent({ connection: this, identity }) - } - /** Handle to the API's internal query client. */ get queryClient (): Promise> { return Promise.resolve(this.api).then(api=>(api as any)?.queryClient) @@ -76,7 +121,7 @@ export class CWConnection extends Connection { return Promise.resolve(this.api).then(api=>(api as any)?.tmClient) } - abciQuery (path, params = new Uint8Array()) { + abciQuery (path: string, params = new Uint8Array()) { return this.queryClient.then(async client=>{ this.log.debug('ABCI query:', path) const { value } = await client!.queryAbci(path, params) @@ -88,20 +133,26 @@ export class CWConnection extends Connection { Promise { const api = await this.api - if ((parameter as { height })?.height) { - const { id, header, txs } = await api.getBlock((parameter as { height }).height) + if ((parameter as { height: number })?.height) { + const { id, header, txs } = await api.getBlock((parameter as { height: number }).height) return new CWBlock({ - hash: id, + chain: this.chain, + id, height: header.height, + timestamp: header.time, + transactions: [], rawTxs: txs as Uint8Array[], }) - } else if ((parameter as { hash })?.hash) { + } else if ((parameter as { hash: string })?.hash) { throw new Error('CWConnection.fetchBlock({ hash }): unimplemented!') } else { const { id, header, txs } = await api.getBlock() return new CWBlock({ - hash: id, + chain: this.chain, + id, height: header.height, + timestamp: header.time, + transactions: [], rawTxs: txs as Uint8Array[], }) } @@ -146,6 +197,9 @@ export class CWConnection extends Connection { return Promise.all([ this.queryClient, this.tendermintClient - ]).then(()=>new CWStaking.Validator({ address }).fetchDetails(this)) + ]).then(()=>new CWStaking.Validator({ + chain: this.chain, + address + }).fetchDetails()) } } diff --git a/packages/cw/cw-identity.ts b/packages/cw/cw-identity.ts index 2586302885..0947d41cba 100644 --- a/packages/cw/cw-identity.ts +++ b/packages/cw/cw-identity.ts @@ -27,10 +27,15 @@ import * as CWCompute from './cw-compute' import * as CWStaking from './cw-staking' export class CWAgent extends Agent { + constructor (properties: ConstructorParameters[0] & { + connection: CWSigningConnection + }) { + super(properties) + this.#connection = properties.connection + } override batch (): CWBatch { return new CWBatch({ agent: this }) } - #connection: CWSigningConnection getConnection (): CWSigningConnection { return this.#connection @@ -38,11 +43,17 @@ export class CWAgent extends Agent { } export class CWSigningConnection extends SigningConnection { + constructor ( + properties: ConstructorParameters[0] + & Pick + ) { + super(properties) + this.api = properties.api + } + /** API connects asynchronously, so API handle is a promise. */ declare api: SigningCosmWasmClient - address: Address - async sendImpl (...args: Parameters) { return await CWBank.send(this, ...args) } diff --git a/packages/cw/cw-staking.ts b/packages/cw/cw-staking.ts index e199781850..105b47b301 100644 --- a/packages/cw/cw-staking.ts +++ b/packages/cw/cw-staking.ts @@ -1,13 +1,10 @@ -import { base16, SHA256 } from '@fadroma/agent' +import { base16, SHA256, Validator } from '@fadroma/agent' import type { Address } from '@fadroma/agent' import { Amino, Proto } from '@hackbg/cosmjs-esm' -import type { CWConnection } from './cw-connection' +import type { CWChain, CWConnection } from './cw-connection' export async function getValidators ( - connection: { - tendermintClient, - abciQuery - }, + connection: CWConnection, { pagination, details, Validator = CWValidator as V }: { pagination?: [number, number], details?: boolean, @@ -35,36 +32,33 @@ export async function getValidators ( const result: Array> = [] for (const { address, pubkey, votingPower, proposerPriority } of validators) { const info = new Validator({ - address: base16.encode(address), - publicKey: pubkey.data, + chain: connection.chain, + address: base16.encode(address), + publicKey: pubkey?.data, votingPower, proposerPriority, }) as InstanceType result.push(info) if (details) { - await info.fetchDetails(connection) + await info.fetchDetails() } } return result } -class CWValidator { - address: string - publicKey: string - votingPower?: bigint - proposerPriority?: bigint - - constructor ({ address, publicKey, votingPower, proposerPriority }: { - address?: Address +class CWValidator extends Validator { + constructor ({ + publicKey, votingPower, proposerPriority, ...properties + }: ConstructorParameters[0] & { publicKey?: string|Uint8Array|Array votingPower?: string|number|bigint proposerPriority?: string|number|bigint - } = {}) { + }) { + super(properties) if ((publicKey instanceof Uint8Array)||(publicKey instanceof Array)) { publicKey = base16.encode(new Uint8Array(publicKey)) } this.publicKey = publicKey! - this.address = address! if (votingPower) { this.votingPower = BigInt(votingPower) } @@ -72,19 +66,24 @@ class CWValidator { this.proposerPriority = BigInt(proposerPriority) } } - + publicKey: string + votingPower?: bigint + proposerPriority?: bigint get publicKeyBytes () { return base16.decode(this.publicKey) } get publicKeyHash () { return base16.encode(SHA256(this.publicKeyBytes).slice(0, 20)) } + get chain (): CWChain { + return super.chain as unknown as CWChain + } - async fetchDetails (connection: { abciQuery }): Promise { + async fetchDetails (): Promise { const request = Proto.Cosmos.Staking.v1beta1.Query.QueryValidatorRequest.encode({ validatorAddr: this.address }).finish() - const value = await connection.abciQuery( + const value = await this.chain.getConnection().abciQuery( '/cosmos.staking.v1beta1.Query/Validator', request ) diff --git a/packages/cw/okp4/okp4.ts b/packages/cw/okp4/okp4.ts index 4fa03869aa..9acecde3bf 100644 --- a/packages/cw/okp4/okp4.ts +++ b/packages/cw/okp4/okp4.ts @@ -16,8 +16,18 @@ export * from './okp4-law-stone' class OKP4CLI extends CLI {} -class OKP4Chain extends CWChain { +export default class OKP4Chain extends CWChain { + + /** Connect to OKP4 in testnet mode. */ + static testnet (options: Partial = {}): Promise { + return OKP4Chain.connect({ + chainId: chainIds.testnet, + urls: [...testnets], ...options||{} + }) + } + declare connections: OKP4Connection[] + } /** Connection for OKP4. */ @@ -26,16 +36,17 @@ class OKP4Connection extends CWConnection { /** Default denomination of gas token. */ static gasToken = new Token.Native('uknow') - /** Transaction fees for this agent. */ - fees = { - upload: OKP4Connection.gasToken.fee(10000000), - init: OKP4Connection.gasToken.fee(1000000), - exec: OKP4Connection.gasToken.fee(1000000), - send: OKP4Connection.gasToken.fee(1000000), - } - - constructor (options: Partial) { - super({ ...defaults, ...options } as Partial) + constructor (properties: ConstructorParameters[0]) { + super({ + ...defaults, + //fees: { + //upload: OKP4Connection.gasToken.fee(10000000), + //init: OKP4Connection.gasToken.fee(1000000), + //exec: OKP4Connection.gasToken.fee(1000000), + //send: OKP4Connection.gasToken.fee(1000000), + //}, + ...properties + }) } /** Get clients for all Cognitarium instances, keyed by address. */ @@ -86,11 +97,18 @@ class OKP4Connection extends CWConnection { class OKP4MnemonicIdentity extends CWMnemonicIdentity { constructor (properties?: { mnemonic?: string } & Partial) { - super({ ...defaults, ...properties||{} }) + super({ + ...defaults, + ...properties||{} + }) } } -const defaults = { coinType: 118, bech32Prefix: 'okp4', hdAccountIndex: 0, } +const defaults = { + coinType: 118, + bech32Prefix: 'okp4', + hdAccountIndex: 0, +} export { OKP4CLI as CLI, @@ -101,11 +119,3 @@ export { export const chainIds = { testnet: 'okp4-nemeton-1', } export const testnets = new Set([ 'https://okp4-testnet-rpc.polkachu.com/' ]) - -/** Connect to OKP4 in testnet mode. */ -export const testnet = (options: Partial = {}): OKP4Connection => { - return OKP4Chain.connect({ - chainId: chainIds.testnet, - urls: [...testnets], ...options||{} - }) -} diff --git a/packages/cw/tsconfig.json b/packages/cw/tsconfig.json index ddb8e91514..48785e6e8f 100644 --- a/packages/cw/tsconfig.json +++ b/packages/cw/tsconfig.json @@ -10,15 +10,20 @@ "okp4/okp4-objectarium.ts" ], "include": [], - "exclude": [ ".ubik", "*.dist.*", "node_modules" ], + "exclude": [ + ".ubik", + "*.dist.*", + "node_modules" + ], "compilerOptions": { - "target": "esnext", - "module": "esnext", - "moduleResolution": "node", - "esModuleInterop": true, + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "esModuleInterop": true, "allowSyntheticDefaultImports": true, - "noEmitOnError": true, - "skipLibCheck": true, - "experimentalDecorators": true + "noEmitOnError": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "strict": true } } diff --git a/packages/namada/namada-cli.ts b/packages/namada/namada-cli.ts index 6593da5ca5..e1f1089429 100644 --- a/packages/namada/namada-cli.ts +++ b/packages/namada/namada-cli.ts @@ -6,7 +6,7 @@ import { } from './namada-console' import { NamadaMnemonicIdentity -} from './namada-connection' +} from './namada-identity' import Namada from './namada' /** Namada CLI commands. */ @@ -277,7 +277,12 @@ export default class NamadaCLI extends CLI { process.exit(1) } const namada = await Namada.connect({ url }) - const {proposal, votes, result} = await namada.getProposalInfo(Number(number)) + const proposalInfo = await namada.getProposalInfo(Number(number)) + if (!proposalInfo) { + this.log.error(`No proposal #${number}`) + process.exit(1) + } + const {proposal, votes, result} = proposalInfo this.log .log() .log('Proposal: ', bold(number)) @@ -336,7 +341,12 @@ export default class NamadaCLI extends CLI { process.exit(1) } const namada = await Namada.connect({ url }) - const {proposal, votes, result} = await namada.getProposalInfo(Number(number)) + const proposalInfo = await namada.getProposalInfo(Number(number)) + if (!proposalInfo) { + this.log.error(`No proposal #${number}`) + process.exit(1) + } + const {proposal, votes, result} = proposalInfo this.log .log() .log('Proposal: ', bold(number)) @@ -386,7 +396,7 @@ export default class NamadaCLI extends CLI { height = Number(height) } const namada = await Namada.connect({ url }) - const block = await namada.fetchBlock({ height }) + const block = await namada.fetchBlock({ height: height! }) this.log.log() .log('Block:', bold(block.height)) .log('ID: ', bold(block.id)) @@ -410,13 +420,13 @@ export default class NamadaCLI extends CLI { let block do { block = await namada.fetchBlock({ height: Number(height) }) - height = block.header.height + height = block.height this.log.log() - .log('Block:', bold(block.header.height)) + .log('Block:', bold(block.height)) .log('ID: ', bold(block.id)) - .log('Time: ', bold(block.header.time)) + .log('Time: ', bold(block.timestamp)) .log(bold('Transactions:')) - for (const tx of block.txsDecoded) { + for (const tx of block.transactions) { this.log.log(tx) } height-- diff --git a/packages/namada/namada-connection.ts b/packages/namada/namada-connection.ts index 0830262796..cb88208713 100644 --- a/packages/namada/namada-connection.ts +++ b/packages/namada/namada-connection.ts @@ -83,6 +83,10 @@ export class Namada extends CW.Chain { return getValidatorAddresses(this.getConnection()) } + getValidator (address: string) { + return getValidator(this, address) + } + getValidators (options?: { details?: boolean, pagination?: [number, number] @@ -91,7 +95,7 @@ export class Namada extends CW.Chain { parallel?: boolean, parallelDetails?: boolean, }) { - return getValidators(this.getConnection(), options) + return getValidators(this, options) } getValidatorsConsensus () { @@ -102,10 +106,6 @@ export class Namada extends CW.Chain { return getValidatorsBelowCapacity(this.getConnection()) } - getValidator (address: string) { - return getValidator(this.getConnection(), address) - } - getDelegations (address: string) { return getDelegations(this.getConnection(), address) } diff --git a/packages/namada/namada-decode.ts b/packages/namada/namada-decode.ts index 8b99c51e80..b358c2113b 100644 --- a/packages/namada/namada-decode.ts +++ b/packages/namada/namada-decode.ts @@ -1,8 +1,8 @@ import * as TX from './namada-tx' import init, { Decode } from './pkg/fadroma_namada.js' -export function decodeTxs (txs, height): TX.Transaction[] { - const txsDecoded = [] +export function decodeTxs (txs: unknown[], height: number|bigint): TX.Transaction[] { + const txsDecoded: TX.Transaction[] = [] for (const i in txs) { try { txsDecoded[i] = TX.Transaction.fromDecoded(txs[i] as any) diff --git a/packages/namada/namada-pos.ts b/packages/namada/namada-pos.ts index 1a023a6680..ad1a24a558 100644 --- a/packages/namada/namada-pos.ts +++ b/packages/namada/namada-pos.ts @@ -2,6 +2,8 @@ import type { Address } from '@fadroma/agent' import { Console, assign, base16, optionallyParallel} from '@fadroma/agent' import { Staking } from '@fadroma/cw' import { decode, u8, u64, u256, array, set } from '@hackbg/borshest' +import { NamadaConnection } from './namada-connection' +import type { Namada } from './namada-connection' class NamadaPoSParameters { maxProposalPeriod!: bigint @@ -45,30 +47,37 @@ class NamadaPoSParameters { } class NamadaValidatorMetadata { + constructor ( + properties: Pick + ) { + assign(this, properties, [ 'email', 'description', 'website', 'discordHandle', 'avatar', ]) + } email!: string description!: string|null website!: string|null discordHandle!: string|null avatar!: string|null - constructor (properties: Partial) { - assign(this, properties, [ - 'email', - 'description', - 'website', - 'discordHandle', - 'avatar', - ]) - } } class NamadaValidator extends Staking.Validator { - static fromNamadaAddress = (namadaAddress: string) => Object.assign(new this({}), { namadaAddress }) + constructor (properties: Omit[0], 'chain'> & { + chain: Namada, + namadaAddress: string + }) { + super(properties) + this.namadaAddress = properties.namadaAddress + } + get chain (): Namada { + return super.chain as unknown as Namada + } namadaAddress!: Address metadata!: NamadaValidatorMetadata commission!: NamadaCommissionPair state!: unknown stake!: bigint - async fetchDetails (connection: Connection, options?: { parallel?: boolean }) { + async fetchDetails (options?: { parallel?: boolean }) { + + const connection = this.chain.getConnection() if (!this.namadaAddress) { const addressBinary = await connection.abciQuery(`/vp/pos/validator_by_tm_addr/${this.address}`) @@ -76,15 +85,20 @@ class NamadaValidator extends Staking.Validator { } const requests: Array<()=>Promise> = [ + () => connection.abciQuery(`/vp/pos/validator/metadata/${this.namadaAddress}`) .then(binary => { if (binary[0] === 1) { - this.metadata = new NamadaValidatorMetadata(connection.decode.pos_validator_metadata(binary.slice(1))) + this.metadata = new NamadaValidatorMetadata( + connection.decode.pos_validator_metadata(binary.slice(1)) as + ConstructorParameters[0] + ) } }) .catch(e => connection.log.warn( `Failed to decode validator metadata for ${this.namadaAddress}` )), + () => connection.abciQuery(`/vp/pos/validator/commission/${this.namadaAddress}`) .then(binary => { if (binary[0] === 1) { @@ -94,6 +108,7 @@ class NamadaValidator extends Staking.Validator { .catch(e => connection.log.warn( `Failed to decode validator commission pair for ${this.namadaAddress}` )), + () => connection.abciQuery(`/vp/pos/validator/state/${this.namadaAddress}`) .then(binary => { if (binary[0] === 1) { @@ -103,6 +118,7 @@ class NamadaValidator extends Staking.Validator { .catch(e => connection.log.warn( `Failed to decode validator state for ${this.namadaAddress}` )), + () => connection.abciQuery(`/vp/pos/validator/stake/${this.namadaAddress}`) .then(binary => { if (binary[0] === 1) { @@ -112,6 +128,7 @@ class NamadaValidator extends Staking.Validator { .catch(e => connection.log.warn( `Failed to decode validator stake for ${this.namadaAddress}` )), + ] if (this.namadaAddress && !this.publicKey) { @@ -159,37 +176,18 @@ export { NamadaCommissionPair as CommissionPair, } -type Connection = { - log: Console, - abciQuery: (path: string)=>Promise - tendermintClient - decode: { - address (binary: Uint8Array): string - addresses (binary: Uint8Array): string[] - address_to_amount (binary: Uint8Array): object - pos_parameters (binary: Uint8Array): Partial - pos_validator_metadata (binary: Uint8Array): Partial - pos_commission_pair (binary: Uint8Array): Partial - pos_validator_state (binary: Uint8Array): string - pos_validator_set (binary: Uint8Array): Array<{ - address: string, - bondedStake: bigint, - }> - } -} - -export async function getStakingParameters (connection: Connection) { +export async function getStakingParameters (connection: NamadaConnection) { const binary = await connection.abciQuery("/vp/pos/pos_params") return new NamadaPoSParameters(connection.decode.pos_parameters(binary)) } -export async function getTotalStaked (connection: Connection) { +export async function getTotalStaked (connection: NamadaConnection) { const binary = await connection.abciQuery("/vp/pos/total_stake") return decode(u64, binary) } export async function getValidators ( - connection: Connection, + chain: Namada, options: Partial[1]> & { addresses?: string[], allStates?: boolean, @@ -198,6 +196,7 @@ export async function getValidators ( interval?: number, } = {} ) { + const connection = chain.getConnection() if (options.allStates) { let { addresses } = options addresses ??= await getValidatorAddresses(connection) @@ -208,12 +207,16 @@ export async function getValidators ( const [page, perPage] = options.pagination addresses = addresses.slice((page - 1)*perPage, page*perPage) } - const validators = addresses.map(address=>NamadaValidator.fromNamadaAddress(address)) + const validators = addresses.map(namadaAddress=>new NamadaValidator({ + chain, + address: '', + namadaAddress + })) if (options.details) { if (options.parallel && !options.pagination) { - throw new Error("set parallel=false or pagination, so as not to bombard the node") + throw new Error("set pagination or parallel=false, so as not to bombard the node") } - const thunks = validators.map(validator=>()=>validator.fetchDetails(connection, { + const thunks = validators.map(validator=>()=>validator.fetchDetails({ parallel: options?.parallelDetails })) if (options?.parallel) { @@ -234,41 +237,48 @@ export async function getValidators ( } } -export async function getValidatorAddresses (connection: Connection): Promise { +export async function getValidatorAddresses (connection: NamadaConnection): Promise { const binary = await connection.abciQuery("/vp/pos/validator/addresses") return connection.decode.addresses(binary) } -export async function getValidatorsConsensus (connection: Connection) { +export async function getValidatorsConsensus (connection: NamadaConnection) { const binary = await connection.abciQuery("/vp/pos/validator_set/consensus") return connection.decode.pos_validator_set(binary).sort(byBondedStake) } -export async function getValidatorsBelowCapacity (connection: Connection) { +export async function getValidatorsBelowCapacity (connection: NamadaConnection) { const binary = await connection.abciQuery("/vp/pos/validator_set/below_capacity") return connection.decode.pos_validator_set(binary).sort(byBondedStake) } -const byBondedStake = (a, b)=> (a.bondedStake > b.bondedStake) ? -1 +const byBondedStake = ( + a: { bondedStake: number|bigint }, + b: { bondedStake: number|bigint }, +)=> (a.bondedStake > b.bondedStake) ? -1 : (a.bondedStake < b.bondedStake) ? 1 : 0 -export async function getValidator (connection: Connection, address: Address) { - return await NamadaValidator.fromNamadaAddress(address).fetchDetails(connection) +export async function getValidator (chain: Namada, namadaAddress: Address) { + return await new NamadaValidator({ + chain, + address: '', + namadaAddress + }).fetchDetails() } -export async function getValidatorStake (connection: Connection, address: Address) { +export async function getValidatorStake (connection: NamadaConnection, address: Address) { const totalStake = await connection.abciQuery(`/vp/pos/validator/stake/${address}`) return decode(u256, totalStake) } -export async function getDelegations (connection: Connection, address: Address) { +export async function getDelegations (connection: NamadaConnection, address: Address) { const binary = await connection.abciQuery(`/vp/pos/delegations/${address}`) return connection.decode.addresses(binary) } export async function getDelegationsAt ( - connection: Connection, address: Address, epoch?: number + connection: NamadaConnection, address: Address, epoch?: number ): Promise> { let query = `/vp/pos/delegations_at/${address}` epoch = Number(epoch) diff --git a/packages/namada/namada-tx-base.ts b/packages/namada/namada-tx-base.ts index d11fd1d992..30cf06b84b 100644 --- a/packages/namada/namada-tx-base.ts +++ b/packages/namada/namada-tx-base.ts @@ -3,7 +3,19 @@ import * as Sections from './namada-tx-section' class NamadaTransaction { - static fromDecoded = ({ sections, ...header }) => new this({ + static fromDecoded = ({ sections, ...header }: { + sections: Array< + | Partial + | Partial + | Partial + | Partial + | Partial + | Partial + | Partial + | Partial + | Partial + > + }) => new this({ ...header, sections: sections.map(section=>{ switch (section.type) { diff --git a/packages/namada/namada-tx.ts b/packages/namada/namada-tx.ts index 80791bc558..149c107888 100644 --- a/packages/namada/namada-tx.ts +++ b/packages/namada/namada-tx.ts @@ -10,14 +10,13 @@ import { Transaction } from './namada-tx-base' export class NamadaBlock extends Block { constructor ({ - transactions, rawTransactions, ...properties + rawTransactions, ...properties }: ConstructorParameters[0] - & Pick + & Pick ) { super(properties) - this.transactions = [...transactions||[]] this.rawTransactions = rawTransactions } - transactions: Transaction[] + declare transactions: Transaction[] rawTransactions?: unknown[] } diff --git a/packages/namada/tsconfig.json b/packages/namada/tsconfig.json index b1ddb9df87..18a8b04e94 100644 --- a/packages/namada/tsconfig.json +++ b/packages/namada/tsconfig.json @@ -1,14 +1,21 @@ { - "include": [ "*.ts" ], - "exclude": [ ".ubik", "*.dist.*", "node_modules" ], + "include": [ + "*.ts" + ], + "exclude": [ + ".ubik", + "*.dist.*", + "node_modules" + ], "compilerOptions": { - "target": "esnext", - "module": "esnext", - "moduleResolution": "node", - "esModuleInterop": true, + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "esModuleInterop": true, "allowSyntheticDefaultImports": true, - "noEmitOnError": true, - "skipLibCheck": true, - "experimentalDecorators": true + "noEmitOnError": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "strict": true } } diff --git a/toolbox b/toolbox index d1ea1bbed7..a803b6d908 160000 --- a/toolbox +++ b/toolbox @@ -1 +1 @@ -Subproject commit d1ea1bbed78fda8c6d52220b7c484fddebfff125 +Subproject commit a803b6d908e4a8a3934ec6bd691257e7ceeff64c