diff --git a/packages/cw/cw-connection.ts b/packages/cw/cw-connection.ts index 2f195c37e1..5739a5cf76 100644 --- a/packages/cw/cw-connection.ts +++ b/packages/cw/cw-connection.ts @@ -10,6 +10,16 @@ import { Amino, Proto, CosmWasmClient, SigningCosmWasmClient } from '@hackbg/cos import type { Block } from '@hackbg/cosmjs-esm' export class CWChain extends Chain { + constructor ( + properties: ConstructorParameters[0] + & Pick + ) { + super(properties) + this.coinType = properties.coinType + this.bech32Prefix = properties.bech32Prefix + this.hdAccountIndex = properties.hdAccountIndex + this.connections = properties.connections + } static get Connection () { return CWConnection @@ -47,17 +57,6 @@ export class CWChain extends Chain { return chain } - constructor ( - properties: ConstructorParameters[0] - & Pick - ) { - super(properties) - 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 /** The coin type in the HD derivation path */ @@ -153,17 +152,16 @@ export class CWConnection extends Connection { }) } - async fetchBlockImpl (parameter?: { height: number }|{ hash: string }): + async fetchBlockImpl (parameter?: { height: number|bigint }|{ hash: string }): Promise { const api = await this.api - if ((parameter as { height: number })?.height) { + if ((parameter as { height: number|bigint })?.height) { const { id, header, txs } = await api.getBlock((parameter as { height: number }).height) return new CWBlock({ chain: this.chain, - id, - height: header.height, - timestamp: header.time, + hash: id, + height: BigInt(header.height), transactions: [], rawTransactions: txs as Uint8Array[], }) @@ -173,9 +171,8 @@ export class CWConnection extends Connection { const { id, header, txs } = await api.getBlock() return new CWBlock({ chain: this.chain, - id, - height: header.height, - timestamp: header.time, + hash: id, + height: BigInt(header.height), transactions: [], rawTransactions: txs as Uint8Array[], }) @@ -184,7 +181,7 @@ export class CWConnection extends Connection { async fetchHeightImpl () { const { height } = await this.fetchBlockImpl() - return height + return height! } /** Query native token balance. */ diff --git a/packages/cw/cw-tx.ts b/packages/cw/cw-tx.ts index 171087f1cd..97864945a5 100644 --- a/packages/cw/cw-tx.ts +++ b/packages/cw/cw-tx.ts @@ -1,46 +1,41 @@ import { Block, Batch } from '@hackbg/fadroma' import { CWError as Error } from './cw-base' import { Chain } from '@hackbg/fadroma' +import { Transaction } from '@hackbg/fadroma' import type { CWChain, CWConnection } from './cw-connection' import type { CWAgent } from './cw-identity' +type CWBlockParameters = + ConstructorParameters[0] & Partial> + export class CWBlock extends Block { - constructor ( - properties: ConstructorParameters[0] & Partial> - ) { - super(properties) - if (properties.rawTransactions) { - this.rawTransactions = properties.rawTransactions - } + constructor (props: CWBlockParameters) { + super(props) + this.rawTransactions = props?.rawTransactions } /** Undecoded transactions. */ - rawTransactions?: Uint8Array[] + readonly rawTransactions?: Uint8Array[] } /** Transaction batch for CosmWasm-enabled chains. */ export class CWBatch extends Batch { declare agent: CWAgent - upload ( - code: Parameters[0], - options: Parameters[1], - ) { + upload (...args: Parameters) { throw new Error("CWBatch#upload: not implemented") return this } - instantiate ( - code: Parameters[0], - options: Parameters[1] - ) { + instantiate (...args: Parameters) { throw new Error("CWBatch#instantiate: not implemented") return this } - execute ( - contract: Parameters[0], - options: Parameters[1] - ) { + execute (...args: Parameters) { throw new Error("CWBatch#execute: not implemented") return this } - async submit () {} + async submit () { + throw new Error("CWBatch#submit: not implemented") + } } + +export class CWTransaction extends Transaction {} diff --git a/packages/cw/cw.ts b/packages/cw/cw.ts index 0ad67a69aa..96f12afe45 100644 --- a/packages/cw/cw.ts +++ b/packages/cw/cw.ts @@ -26,8 +26,9 @@ export { CWConnection as Connection, } from './cw-connection' export { - CWBlock as Block, - CWBatch as Batch, + CWBlock as Block, + CWTransaction as Transaction, + CWBatch as Batch, } from './cw-tx' export { CWIdentity as Identity, diff --git a/packages/namada/Namada.ts b/packages/namada/Namada.ts index bca13387f6..0e5e43f448 100644 --- a/packages/namada/Namada.ts +++ b/packages/namada/Namada.ts @@ -1,25 +1,26 @@ -import NamadaConsole from './NamadaConsole' -import NamadaChain from './NamadaChain' -import NamadaConnection from './NamadaConnection' -import NamadaTransaction from './NamadaTransaction' +import Console from './NamadaConsole' +import Chain from './NamadaChain' +import Connection from './NamadaConnection' +import Block, { Transaction } from './NamadaBlock' import { Decode, initDecoder } from './NamadaDecode' import * as Identity from './NamadaIdentity' export { Decode, initDecoder, - NamadaConsole as Console, - NamadaChain as Chain, - NamadaConnection as Connection, - NamadaTransaction as Transaction, + Console, + Chain, + Connection, + Block, + Transaction, Identity } -export const testnetChainId = NamadaChain.testnetChainId -export const testnetURLs = NamadaChain.testnetURLs -export function connect (...args: Parameters) { - return NamadaChain.connect(...args) +export const testnetChainId = Chain.testnetChainId +export const testnetURLs = Chain.testnetURLs +export function connect (...args: Parameters) { + return Chain.connect(...args) } -export function testnet (...args: Parameters) { - return NamadaChain.testnet(...args) +export function testnet (...args: Parameters) { + return Chain.testnet(...args) } export function mainnet (...args: never) { throw new Error( diff --git a/packages/namada/NamadaBlock.ts b/packages/namada/NamadaBlock.ts index aa3ce31971..669ff29655 100644 --- a/packages/namada/NamadaBlock.ts +++ b/packages/namada/NamadaBlock.ts @@ -1,71 +1,39 @@ -import { Block } from '@fadroma/cw' -import type { Chain as Namada } from './Namada' +import type * as Namada from './Namada' import { Decode } from './NamadaDecode' -import NamadaTransaction, { NamadaUndecodedTransaction } from './NamadaTransaction' +import { Block, Transaction } from '@fadroma/cw' -export default class NamadaBlock extends Block { +type NamadaBlockParameters = + ConstructorParameters[0] + & Pick - constructor ({ - chain, hash, header, transactions, responses - }: Omit< - ConstructorParameters[0], 'id' - > & Pick< - NamadaBlock, 'chain'|'hash'|'header'|'transactions'|'responses' - >) { - super({ chain, id: hash, header, transactions }) +class NamadaBlock extends Block { + constructor ({ responses, ...props }: NamadaBlockParameters) { + super(props) this.#responses = responses } - - get hash (): string { - return this.id - } - get chain (): Namada|undefined { - return super.chain as Namada|undefined + get chain (): Namada.Chain|undefined { + return super.chain as Namada.Chain|undefined } - #responses?: { block: { url: string, response: string } results: { url: string, response: string } } - get responses () { return this.#responses } - /** Block header. */ - declare header: { - version: object - chainId: string - height: bigint - time: string - lastBlockId: string - lastCommitHash: string - dataHash: string - validatorsHash: string - nextValidatorsHash: string - consensusHash: string - appHash: string - lastResultsHash: string - evidenceHash: string - proposerAddress: string - } - /** Monotonically incrementing ID of block. */ - get height () { - return Number((this.header as any)?.height) - } + declare header: NamadaBlockHeader /** Timestamp of block */ get time () { return (this.header as any)?.time } - /** Transaction in block. */ declare transactions: NamadaTransaction[] /** Responses from block API endpoints. */ - static async fetchByHeight ( { url, decode = Decode, chain }: { - url: string|URL, decode?: typeof Decode, chain?: Namada + url: string|URL, decode?: typeof Decode, chain?: Namada.Chain }, { height, raw }: { height?: number|string|bigint, @@ -89,7 +57,7 @@ export default class NamadaBlock extends Block { } static async fetchByHash ( - _1: { url: string|URL, decode?: typeof Decode, chain?: Namada }, + _1: { url: string|URL, decode?: typeof Decode, chain?: Namada.Chain }, _2: { hash: string, raw?: boolean }, ): Promise { throw new Error('NamadaBlock.fetchByHash: not implemented') @@ -99,7 +67,7 @@ export default class NamadaBlock extends Block { responses: NonNullable, { decode = Decode, chain, height, raw = false }: { decode?: typeof Decode - chain?: Namada, + chain?: Namada.Chain, height?: string|number|bigint, raw?: boolean }, @@ -110,17 +78,69 @@ export default class NamadaBlock extends Block { ) as { hash: string, header: NamadaBlock["header"] - transactions: Partial[] + transactions: Array & {id: string}> } const block = new NamadaBlock({ - chain, hash, header, transactions: [], responses + chain, hash, header, responses, transactions: [], }) return Object.assign(block, { transactions: transactions.map(tx=>new NamadaTransaction({ - id: tx?.id, + hash: tx?.id, ...tx, block })) }) } } + +type NamadaBlockHeader = { + version: object + chainId: string + height: bigint + time: string + lastBlockId: string + lastCommitHash: string + dataHash: string + validatorsHash: string + nextValidatorsHash: string + consensusHash: string + appHash: string + lastResultsHash: string + evidenceHash: string + proposerAddress: string +} + +type NamadaTransactionParameters = + Pick & NamadaTransaction['data'] + +class NamadaTransaction extends Transaction { + constructor ({ hash, block, ...data }: NamadaTransactionParameters) { + super({ hash, block, data }) + } + get block (): NamadaBlock|undefined { + return super.block as NamadaBlock|undefined + } + declare data: { + expiration?: string|null + timestamp?: string + feeToken?: string + feeAmountPerGasUnit?: string + multiplier?: BigInt + gasLimitMultiplier?: BigInt + atomic?: boolean + txType?: 'Raw'|'Wrapper'|'Decrypted'|'Protocol' + sections?: object[] + content?: object + batch?: Array<{ + hash: string, + codeHash: string, + dataHash: string, + memoHash: string + }> + }|undefined +} + +export { + NamadaBlock as default, + NamadaTransaction as Transaction, +} diff --git a/packages/namada/NamadaConnection.ts b/packages/namada/NamadaConnection.ts index 236a724bd1..ea3daeb7ae 100644 --- a/packages/namada/NamadaConnection.ts +++ b/packages/namada/NamadaConnection.ts @@ -15,7 +15,7 @@ export default class NamadaConnection extends CW.Connection { } override async fetchBlockImpl ( - parameter?: ({ height: number }|{ hash: string }) & { raw?: boolean } + parameter?: ({ height: bigint|number }|{ hash: string }) & { raw?: boolean } ): Promise { if (!this.url) { throw new CW.Error("Can't fetch block: missing connection URL") @@ -56,7 +56,7 @@ export default class NamadaConnection extends CW.Connection { return Gov.fetchProposalCount(this) } fetchProposalInfoImpl (id: number|bigint) { - return Gov.fetchProposalInfo(this, id) + return Gov.Proposal.fetch(this, id) } fetchPGFParametersImpl () { diff --git a/packages/namada/NamadaConsole.ts b/packages/namada/NamadaConsole.ts index f770db1c6e..878b277134 100644 --- a/packages/namada/NamadaConsole.ts +++ b/packages/namada/NamadaConsole.ts @@ -1,5 +1,5 @@ import { Console, bold } from '@hackbg/fadroma' -import type Transaction from './NamadaTransaction' +import type { Transaction } from './NamadaBlock' import type { Proposal } from './NamadaGov' import type { Validator } from './NamadaPoS' diff --git a/packages/namada/NamadaDecode.ts b/packages/namada/NamadaDecode.ts index 6661f6f777..8214f3142c 100644 --- a/packages/namada/NamadaDecode.ts +++ b/packages/namada/NamadaDecode.ts @@ -27,6 +27,10 @@ export interface NamadaDecoder { gas_cost_table (_: Uint8Array): Record + gov_proposal (_: Uint8Array): unknown + + gov_votes (_: Uint8Array): unknown + gov_parameters (_: Uint8Array): Partial<{ minProposalFund: bigint maxProposalCodeSize: bigint diff --git a/packages/namada/NamadaGov.ts b/packages/namada/NamadaGov.ts index 27aee5540f..0eeb5055f5 100644 --- a/packages/namada/NamadaGov.ts +++ b/packages/namada/NamadaGov.ts @@ -16,44 +16,55 @@ export async function fetchProposalCount (connection: Pick> +export const INTERNAL_ADDRESS = + "tnam1q5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrw33g6" -export async function fetchProposalInfo ( - connection: Pick, id: number|bigint -) { - const proposalResponse = await connection.abciQuery(`/vp/governance/proposal/${id}`) - if (proposalResponse[0] === 0) { - return null +class NamadaGovernanceProposal { + readonly id: bigint + readonly proposal: ReturnType + readonly votes: ReturnType + readonly result: NamadaGovernanceProposalResult|null + constructor (props: Pick) { + this.id = props?.id + this.proposal = props?.proposal + this.votes = props?.votes + this.result = props?.result ? new NamadaGovernanceProposalResult(props.result) : null + } + static async fetch ( + connection: Pick, id: number|bigint + ) { + const proposalResponse = await connection.abciQuery(`/vp/governance/proposal/${id}`) + if (proposalResponse[0] === 0) return null + const [ votesResponse, resultResponse ] = await Promise.all([ + `/vp/governance/proposal/${id}/votes`, + `/vp/governance/stored_proposal_result/${id}`, + ].map(x=>connection.abciQuery(x))) + return new this({ + id: + BigInt(id), + proposal: + connection.decode.gov_proposal(proposalResponse.slice(1)) as + ReturnType, + votes: + connection.decode.gov_votes(votesResponse) as + ReturnType, + result: (resultResponse[0] === 0) + ? null + : new NamadaGovernanceProposalResult( + connection.decode.gov_result(resultResponse.slice(1)) + ) + }) } - const [ - votesResponse, resultResponse - ] = await Promise.all([ - `/vp/governance/proposal/${id}/votes`, - `/vp/governance/stored_proposal_result/${id}`, - ].map( - x=>connection.abciQuery(x) - )) - const proposal = - connection.decode.gov_proposal(proposalResponse.slice(1)) - const votes = - connection.decode.gov_votes(votesResponse) - const result: GovernanceProposalResult|null = - (resultResponse[0] === 0) - ? null - : new GovernanceProposalResult( - connection.decode.gov_result(resultResponse.slice(1)) - ) - return { proposal, votes, result } } -class GovernanceProposalResult implements ReturnType { +class NamadaGovernanceProposalResult implements ReturnType { result!: "Passed"|"Rejected" tallyType!: "TwoThirds"|"OneHalfOverOneThird"|"LessOneHalfOverOneThirdNay" totalVotingPower!: bigint totalYayPower!: bigint totalNayPower!: bigint totalAbstainPower!: bigint - constructor (properties: Partial = {}) { + constructor (properties: Partial = {}) { assign(this, properties, [ 'result', 'tallyType', @@ -84,7 +95,6 @@ const percent = (a: bigint, b: bigint) => ((Number(a * 1000000n / b) / 10000).toFixed(2) + '%').padStart(7) export { - GovernanceProposalResult as ProposalResult, + NamadaGovernanceProposal as Proposal, + NamadaGovernanceProposalResult as ProposalResult, } - -export const INTERNAL_ADDRESS = "tnam1q5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrw33g6" diff --git a/packages/namada/NamadaPoS.ts b/packages/namada/NamadaPoS.ts index 910c725d1f..adff74915c 100644 --- a/packages/namada/NamadaPoS.ts +++ b/packages/namada/NamadaPoS.ts @@ -80,7 +80,7 @@ export async function fetchValidators ( return [base16.encode(binary.slice(2)), address] }))) for (const [publicKey, namadaAddress] of Object.entries(publicKeyToNamadaAddress)) { - validators[publicKey] ??= new NamadaValidator({ chain, publicKey }) + validators[publicKey] ??= new NamadaValidator({ chain, publicKey, address: '' }) validators[publicKey].namadaAddress = namadaAddress } if (options?.details ?? true) { diff --git a/packages/namada/NamadaTransaction.ts b/packages/namada/NamadaTransaction.ts deleted file mode 100644 index e1a155f7ba..0000000000 --- a/packages/namada/NamadaTransaction.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { assign, Transaction } from '@hackbg/fadroma' -import { Block } from '@fadroma/cw' -import { Decode } from './NamadaDecode' -import type { Chain as Namada } from './Namada' -import type NamadaBlock from './NamadaBlock' - -export default class NamadaTransaction extends Transaction { - constructor ({ - id, block, ...data - }: Pick & NamadaTransaction['data']) { - super({ id, block, data }) - } - - get block (): NamadaBlock|undefined { - return super.block as NamadaBlock|undefined - } - - declare data: { - expiration?: string|null - timestamp?: string - feeToken?: string - feeAmountPerGasUnit?: string - multiplier?: BigInt - gasLimitMultiplier?: BigInt - atomic?: boolean - txType?: 'Raw'|'Wrapper'|'Decrypted'|'Protocol' - sections?: object[] - content?: object - batch?: Array<{ - hash: string, - codeHash: string, - dataHash: string, - memoHash: string - }> - }|undefined - - static fromDecoded ( - data: Pick & NamadaTransaction['data'], - block?: NamadaBlock - ) { - try { - return new this(data) - } catch (error: any) { - console.error(error) - const context = ` in block ${block?.height??'??'}.` - console.warn(`Failed to decode transaction${context}`) - return new NamadaUndecodedTransaction({ error, data }) - } - } -} - -export class NamadaUndecodedTransaction extends NamadaTransaction { - declare data: any - error: Error - constructor (properties: Partial = {}) { - super(properties) - assign(this, properties, [ "data", "error" ]) - } -} diff --git a/src/Block.ts b/src/Block.ts index 8a7e5d60b1..85e73429d4 100644 --- a/src/Block.ts +++ b/src/Block.ts @@ -9,64 +9,55 @@ import type { Chain } from '../index' * * Contains zero or more transactions. */ export abstract class Block { - constructor ( - properties: Pick - ) { - this.hash = properties.hash - this.#chain = properties.chain - this.header = properties.header + constructor (properties: Pick) { + const height = properties?.height ?? properties?.header?.height + if (!height) { + throw new Error("Can't construct Block without at least specifying height") + } + this.#chain = properties?.chain + this.hash = properties?.hash + this.header = properties?.header + this.height = BigInt(height) this.transactions = properties?.transactions || [] } - /** Unique ID of block. */ - hash?: string - /** Height of block. */ - height?: number - /** Contents of block header. */ - header?: unknown /** Private reference to chain to which this block belongs. */ - #chain?: Chain - /** Transactions in block */ - transactions?: Transaction[] = [] + readonly #chain?: Chain /** Chain to which this block belongs. */ - get chain () { - return this.#chain - } + get chain () { return this.#chain } /** ID of chain to which this block belongs. */ - get chainId () { - return this.chain?.id - } + get chainId () { return this.chain?.id } + /** Unique ID of block. */ + readonly hash?: string + /** Unique identifying hash of block. */ + readonly height?: bigint + /** Contents of block header. */ + readonly header?: { height?: string|number|bigint } + /** Transactions in block */ + readonly transactions?: Transaction[] = [] } /** A transaction in a block on a chain. */ export class Transaction { - constructor (properties: Pick) { + constructor (properties: Pick) { this.#block = properties?.block this.hash = properties?.hash this.data = properties?.data } - hash?: string - data?: unknown - #block?: Block + readonly #block?: Block /** Block to which this transaction belongs. */ - get block () { - return this.#block - } + get block () { return this.#block } /** Hash of block to which this transaction belongs. */ - get blockHash () { - return this.block?.hash - } + get blockHash () { return this.block?.hash } /** Height of block to which this transaction belongs. */ - get blockHeight () { - return this.block?.height - } + get blockHeight () { return this.block?.height } /** Chain to which this transaction belongs. */ - get chain () { - return this.block?.chain - } + get chain () { return this.block?.chain } /** ID of chain to which this transaction belongs. */ - get chainId () { - return this.block?.chain?.id - } + get chainId () { return this.block?.chain?.id } + /** Unique identifying hash of transaction. */ + readonly hash?: string + /** Any custom data attached to the transaction. */ + readonly data?: unknown } /** Implementation of Connection#fetchBlock -> Connection#fetchBlockImpl */ @@ -76,13 +67,13 @@ export async function fetchBlock (chain: Chain, ...args: Parameters Connection#fetchNextBlockImpl */ export async function fetchNextBlock (chain: Chain): - Promise + Promise { return chain.fetchHeight().then(async startingHeight=>{ - startingHeight = Number(startingHeight) - if (isNaN(startingHeight)) { - chain.log.warn('Current block height undetermined. Not waiting for next block') - return Promise.resolve(NaN) - } + startingHeight = BigInt(startingHeight) chain.log.log( `Waiting for block > ${bold(String(startingHeight))}`, `(polling every ${chain.blockInterval}ms)` @@ -122,7 +109,7 @@ export async function fetchNextBlock (chain: Chain): const height = await chain.fetchHeight() if (height > startingHeight) { chain.log.log(`Block height incremented to ${bold(String(height))}, proceeding`) - return resolve(height as number) + return resolve(BigInt(height as unknown as number)) } } throw new Error('endpoint dead, not waiting for next block') diff --git a/src/Chain.ts b/src/Chain.ts index 55dcdef9d8..0ea51af768 100644 --- a/src/Chain.ts +++ b/src/Chain.ts @@ -50,13 +50,13 @@ export abstract class Chain extends Logged { abstract authenticate (properties?: { mnemonic: string }|Identity): Promise /** Get the current block height. */ - fetchHeight (): Promise { + fetchHeight (): Promise { this.log.debug('Querying block height') return this.getConnection().fetchHeightImpl() } /** Wait until the block height increments, or until `this.alive` is set to false. */ - fetchNextBlock (): Promise { + fetchNextBlock (): Promise { this.log.debug('Querying block height') return fetchNextBlock(this) } @@ -65,7 +65,7 @@ export abstract class Chain extends Logged { fetchBlock (): Promise /** Get info about the block with a specific height. */ - fetchBlock ({ height }: { height: number, raw?: boolean }): + fetchBlock ({ height }: { height: number|bigint, raw?: boolean }): Promise /** Get info about the block with a specific hash. */ fetchBlock ({ hash }: { hash: string, raw?: boolean }): diff --git a/src/Connection.ts b/src/Connection.ts index b959719331..0f52974141 100644 --- a/src/Connection.ts +++ b/src/Connection.ts @@ -57,11 +57,11 @@ export abstract class Connection extends Logged { /** Chain-specific implementation of fetchBlock. */ abstract fetchBlockImpl (parameters?: - { raw?: boolean } & ({ height: number }|{ hash: string }) + { raw?: boolean } & ({ height: number|bigint }|{ hash: string }) ): Promise /** Chain-specific implementation of fetchHeight. */ abstract fetchHeightImpl (): - Promise + Promise /** Chain-specific implementation of fetchBalance. */ abstract fetchBalanceImpl (parameters: { addresses: Record,