From 44d81e2e9a92abe6754bf9a703ec27feef4b6080 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Tue, 11 Jun 2024 14:35:54 -0400 Subject: [PATCH 1/9] Refactor block header --- src/client/v2/algod/models/types.ts | 16 +- src/client/v2/untypedmodel.ts | 2 +- src/encoding/encoding.ts | 2 +- src/encoding/schema/index.ts | 7 +- src/encoding/schema/map.ts | 96 +++++ src/logicsig.ts | 4 +- src/signedTransaction.ts | 2 +- src/transaction.ts | 2 +- src/types/blockHeader.ts | 613 +++++++++++++++++++++------- tests/2.Encoding.ts | 2 +- tests/cucumber/steps/steps.js | 7 +- 11 files changed, 595 insertions(+), 158 deletions(-) diff --git a/src/client/v2/algod/models/types.ts b/src/client/v2/algod/models/types.ts index 29e8289b4..f3238f765 100644 --- a/src/client/v2/algod/models/types.ts +++ b/src/client/v2/algod/models/types.ts @@ -15,11 +15,7 @@ import { OptionalSchema, } from '../../../../encoding/schema/index.js'; import { base64ToBytes } from '../../../../encoding/binarydata.js'; -import BlockHeader, { - blockHeaderFromEncodingData, - blockHeaderToEncodingData, - BLOCK_HEADER_SCHEMA, -} from '../../../../types/blockHeader.js'; +import { Block } from '../../../../types/blockHeader.js'; import { SignedTransaction } from '../../../../signedTransaction.js'; import { Address } from '../../../../encoding/address.js'; import { UntypedValue } from '../../untypedmodel.js'; @@ -2972,7 +2968,7 @@ export class BlockResponse implements Encodable { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); (this.encodingSchemaValue as NamedMapSchema).entries.push( - { key: 'block', valueSchema: BLOCK_HEADER_SCHEMA, omitEmpty: true }, + { key: 'block', valueSchema: Block.encodingSchema, omitEmpty: true }, { key: 'cert', valueSchema: new OptionalSchema(UntypedValue.encodingSchema), @@ -2986,7 +2982,7 @@ export class BlockResponse implements Encodable { /** * Block header data. */ - public block: BlockHeader; + public block: Block; /** * Optional certificate object. This is only included when the format is set to @@ -3000,7 +2996,7 @@ export class BlockResponse implements Encodable { * @param cert - Optional certificate object. This is only included when the format is set to * message pack. */ - constructor({ block, cert }: { block: BlockHeader; cert?: UntypedValue }) { + constructor({ block, cert }: { block: Block; cert?: UntypedValue }) { this.block = block; this.cert = cert; } @@ -3012,7 +3008,7 @@ export class BlockResponse implements Encodable { toEncodingData(): Map { return new Map([ - ['block', blockHeaderToEncodingData(this.block)], + ['block', this.block.toEncodingData()], [ 'cert', typeof this.cert !== 'undefined' @@ -3027,7 +3023,7 @@ export class BlockResponse implements Encodable { throw new Error(`Invalid decoded logic sig account: ${data}`); } return new BlockResponse({ - block: blockHeaderFromEncodingData(data.get('block') ?? new Map()), + block: Block.fromEncodingData(data.get('block') ?? new Map()), cert: typeof data.get('cert') !== 'undefined' ? UntypedValue.fromEncodingData(data.get('cert')) diff --git a/src/client/v2/untypedmodel.ts b/src/client/v2/untypedmodel.ts index a37516dd0..e7b19b5e7 100644 --- a/src/client/v2/untypedmodel.ts +++ b/src/client/v2/untypedmodel.ts @@ -2,7 +2,7 @@ import { Encodable, MsgpackEncodingData } from '../../encoding/encoding.js'; import { UntypedSchema } from '../../encoding/schema/index.js'; export class UntypedValue implements Encodable { - static encodingSchema = new UntypedSchema(); + static readonly encodingSchema = new UntypedSchema(); public readonly data: MsgpackEncodingData; diff --git a/src/encoding/encoding.ts b/src/encoding/encoding.ts index fc757fb3e..5e6a59466 100644 --- a/src/encoding/encoding.ts +++ b/src/encoding/encoding.ts @@ -233,7 +233,7 @@ export interface EncodableClass { /** * The encoding Schema for this class, used to prepare encoding data from msgpack and JSON. */ - encodingSchema: Schema; + readonly encodingSchema: Schema; } /** diff --git a/src/encoding/schema/index.ts b/src/encoding/schema/index.ts index 17f8775d0..848b5429f 100644 --- a/src/encoding/schema/index.ts +++ b/src/encoding/schema/index.ts @@ -6,7 +6,12 @@ export { AddressSchema } from './address.js'; export { ByteArraySchema, FixedLengthByteArraySchema } from './bytearray.js'; export { ArraySchema } from './array.js'; -export { NamedMapSchema, NamedMapEntry, allOmitEmpty } from './map.js'; +export { + NamedMapSchema, + NamedMapEntry, + allOmitEmpty, + Uint64MapSchema, +} from './map.js'; export { OptionalSchema } from './optional.js'; export { UntypedSchema } from './untyped.js'; diff --git a/src/encoding/schema/map.ts b/src/encoding/schema/map.ts index ecb778a29..391857ba8 100644 --- a/src/encoding/schema/map.ts +++ b/src/encoding/schema/map.ts @@ -1,4 +1,5 @@ import { Schema, MsgpackEncodingData, JSONEncodingData } from '../encoding.js'; +import { ensureUint64 } from '../../utils/utils.js'; /* eslint-disable class-methods-use-this */ @@ -14,6 +15,9 @@ export function allOmitEmpty( return entries.map((entry) => ({ ...entry, omitEmpty: true })); } +/** + * Schema for a map/struct with a fixed set of string fields. + */ export class NamedMapSchema extends Schema { constructor(public readonly entries: NamedMapEntry[]) { super(); @@ -115,3 +119,95 @@ export class NamedMapSchema extends Schema { return map; } } + +/** + * Schema for a map with a variable number of uint64 keys. + */ +export class Uint64MapSchema extends Schema { + constructor(public readonly valueSchema: Schema) { + super(); + } + + public defaultValue(): Map { + return new Map(); + } + + public isDefaultValue(data: unknown): boolean { + return data instanceof Map && data.size === 0; + } + + public prepareMsgpack(data: unknown): MsgpackEncodingData { + if (!(data instanceof Map)) { + throw new Error( + `Uint64MapSchema data must be a Map. Got (${typeof data}) ${data}` + ); + } + const prepared = new Map(); + for (const [key, value] of data) { + const bigintKey = ensureUint64(key); + if (prepared.has(bigintKey)) { + throw new Error(`Duplicate key: ${bigintKey}`); + } + prepared.set(key, this.valueSchema.prepareMsgpack(value)); + } + return prepared; + } + + public fromPreparedMsgpack( + encoded: MsgpackEncodingData + ): Map { + if (!(encoded instanceof Map)) { + throw new Error('Uint64MapSchema data must be a Map'); + } + const map = new Map(); + for (const [key, value] of encoded) { + const bigintKey = ensureUint64(key); + if (map.has(bigintKey)) { + throw new Error(`Duplicate key: ${bigintKey}`); + } + map.set(bigintKey, this.valueSchema.fromPreparedMsgpack(value)); + } + return map; + } + + public prepareJSON(data: unknown): JSONEncodingData { + if (!(data instanceof Map)) { + throw new Error( + `Uint64MapSchema data must be a Map. Got (${typeof data}) ${data}` + ); + } + const prepared = new Map(); + for (const [key, value] of data) { + const bigintKey = ensureUint64(key); + if (prepared.has(bigintKey)) { + throw new Error(`Duplicate key: ${bigintKey}`); + } + prepared.set(key, this.valueSchema.prepareJSON(value)); + } + // Convert map to object + const obj: { [key: string]: JSONEncodingData } = {}; + for (const [key, value] of prepared) { + obj[key.toString()] = value; + } + return obj; + } + + public fromPreparedJSON(encoded: JSONEncodingData): Map { + if ( + encoded == null || + typeof encoded !== 'object' || + Array.isArray(encoded) + ) { + throw new Error('NamedMapSchema data must be an object'); + } + const map = new Map(); + for (const [key, value] of Object.entries(encoded)) { + const bigintKey = BigInt(key); + if (map.has(bigintKey)) { + throw new Error(`Duplicate key: ${bigintKey}`); + } + map.set(bigintKey, this.valueSchema.fromPreparedJSON(value)); + } + return map; + } +} diff --git a/src/logicsig.ts b/src/logicsig.ts index e1ec6f22b..3267c24d9 100644 --- a/src/logicsig.ts +++ b/src/logicsig.ts @@ -67,7 +67,7 @@ const programTag = new TextEncoder().encode('Program'); LogicSig cannot sign transactions in all cases. Instead, use LogicSigAccount as a safe, general purpose signing mechanism. Since LogicSig does not track the provided signature's public key, LogicSig cannot sign transactions when delegated to a non-multisig account _and_ the sender is not the delegating account. */ export class LogicSig implements encoding.Encodable { - static encodingSchema = new NamedMapSchema( + static readonly encodingSchema = new NamedMapSchema( allOmitEmpty([ { key: 'l', @@ -255,7 +255,7 @@ export class LogicSig implements encoding.Encodable { * Represents an account that can sign with a LogicSig program. */ export class LogicSigAccount implements encoding.Encodable { - static encodingSchema = new NamedMapSchema( + static readonly encodingSchema = new NamedMapSchema( allOmitEmpty([ { key: 'lsig', diff --git a/src/signedTransaction.ts b/src/signedTransaction.ts index 7d783049d..03bf4d44f 100644 --- a/src/signedTransaction.ts +++ b/src/signedTransaction.ts @@ -21,7 +21,7 @@ import { } from './encoding/schema/index.js'; export class SignedTransaction implements Encodable { - static encodingSchema = new NamedMapSchema( + static readonly encodingSchema = new NamedMapSchema( allOmitEmpty([ { key: 'txn', diff --git a/src/transaction.ts b/src/transaction.ts index 0cd66736b..e1584333b 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -251,7 +251,7 @@ export interface StateProofTransactionFields { * Transaction enables construction of Algorand transactions * */ export class Transaction implements encoding.Encodable { - static encodingSchema = new NamedMapSchema( + static readonly encodingSchema = new NamedMapSchema( allOmitEmpty([ // Common { key: 'type', valueSchema: new StringSchema() }, diff --git a/src/types/blockHeader.ts b/src/types/blockHeader.ts index 259f9b511..d1440271a 100644 --- a/src/types/blockHeader.ts +++ b/src/types/blockHeader.ts @@ -1,201 +1,538 @@ +import { Encodable, Schema } from '../encoding/encoding.js'; import { NamedMapSchema, + Uint64MapSchema, + ArraySchema, StringSchema, Uint64Schema, AddressSchema, + ByteArraySchema, FixedLengthByteArraySchema, UntypedSchema, allOmitEmpty, } from '../encoding/schema/index.js'; +import { Address } from '../encoding/address.js'; /** - * Represents the metadata and state of a block. - * - * For more information, refer to: https://github.com/algorand/go-algorand/blob/master/data/bookkeeping/block.go + * StateProofTrackingData tracks the status of state proofs. */ -export default interface BlockHeader { +export class StateProofTrackingData implements Encodable { + public static readonly encodingSchema = new NamedMapSchema( + allOmitEmpty([ + { + key: 'v', // stateProofVotersCommitment + valueSchema: new ByteArraySchema(), + }, + { + key: 't', // stateProofOnlineTotalWeight + valueSchema: new Uint64Schema(), + }, + { + key: 'n', // stateProofNextRound + valueSchema: new Uint64Schema(), + }, + ]) + ); + /** - * Transaction fees + * StateProofVotersCommitment is the root of a vector commitment containing the online accounts + * that will help sign a state proof. The VC root, and the state proof, happen on blocks that are + * a multiple of ConsensusParams.StateProofRounds. For blocks that are not a multiple of + * ConsensusParams.StateProofRounds, this value is zero. */ - fees: string; + public stateProofVotersCommitment: Uint8Array; /** - * The number of leftover MicroAlgos after rewards distribution + * StateProofOnlineTotalWeight is the total number of microalgos held by the online accounts during + * the StateProof round (or zero, if the merkle root is zero - no commitment for StateProof voters). + * This is intended for computing the threshold of votes to expect from StateProofVotersCommitment. */ - frac: number; + public stateProofOnlineTotalWeight: bigint; /** - * Genesis ID to which this block belongs + * StateProofNextRound is the next round for which we will accept a StateProof transaction. */ - gen: string; + public stateProofNextRound: bigint; + + public constructor(params: { + stateProofVotersCommitment: Uint8Array; + stateProofOnlineTotalWeight: bigint; + stateProofNextRound: bigint; + }) { + this.stateProofVotersCommitment = params.stateProofVotersCommitment; + this.stateProofOnlineTotalWeight = params.stateProofOnlineTotalWeight; + this.stateProofNextRound = params.stateProofNextRound; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return StateProofTrackingData.encodingSchema; + } + + public toEncodingData(): unknown { + return new Map([ + ['v', this.stateProofVotersCommitment], + ['t', this.stateProofOnlineTotalWeight], + ['n', this.stateProofNextRound], + ]); + } + + public static fromEncodingData(data: unknown): StateProofTrackingData { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded StateProofTrackingData: ${data}`); + } + return new StateProofTrackingData({ + stateProofVotersCommitment: data.get('v'), + stateProofOnlineTotalWeight: data.get('t'), + stateProofNextRound: data.get('n'), + }); + } +} +/** + * TxnCommitments represents the commitments computed from the transactions in the block. + * It contains multiple commitments based on different algorithms and hash functions, to support + * different use cases. + */ +export interface TxnCommitments { /** - * Genesis hash to which this block belongs. + * Root of transaction merkle tree using SHA512_256 hash function. This commitment is computed + * based on the PaysetCommit type specified in the block's consensus protocol. */ - gh: string; + nativeSha512_256Commitment: Uint8Array; /** - * The hash of the previous block + * Root of transaction vector commitment merkle tree using SHA256 hash function */ - prev: string; + sha256Commitment: Uint8Array; +} +/** + * RewardsState represents the global parameters controlling the rate at which accounts accrue rewards. + */ +export interface RewardState { /** - * Current protocol + * The FeeSink address. */ - proto: string; + feeSink: Address; /** - * Rewards rate + * The RewardsPool address. */ - rate: number; + rewardsPool: Address; /** - * Round number + * RewardsLevel specifies how many rewards, in MicroAlgos, have been distributed to each + * config.Protocol.RewardUnit of MicroAlgos since genesis. */ - rnd: number; + rewaredsLevel: bigint; /** - * Rewards recalculation round + * The number of new MicroAlgos added to the participation stake from rewards at the next round. */ - rwcalr: number; + rewardsRate: bigint; /** - * Rewards pool + * The number of leftover MicroAlgos after the distribution of RewardsRate/rewardUnits MicroAlgos for + * every reward unit in the next round. */ - rwd: string; + rewardsResidue: bigint; + + /** + * The round at which the RewardsRate will be recalculated. + */ + rewardsRecalculationRound: bigint; +} + +/** + * UpgradeState tracks the protocol upgrade state machine. It is, strictly speaking, computable from + * the history of all UpgradeVotes but we keep it in the block for explicitness and convenience + * (instead of materializing it separately, like balances). + */ +export interface UpgradeState { + currentProtocol: string; + + nextProtocol: string; + + nextProtocolApprovals: bigint; + + /** + * NextProtocolVoteBefore specify the last voting round for the next protocol proposal. If there + * is no voting for an upgrade taking place, this would be zero. + */ + nextProtocolVoteBefore: bigint; + + /** + * NextProtocolSwitchOn specify the round number at which the next protocol would be adopted. If + * there is no upgrade taking place, nor a wait for the next protocol, this would be zero. + */ + nextProtocolSwitchOn: bigint; +} + +/** + * UpgradeVote represents the vote of the block proposer with respect to protocol upgrades. + */ +export interface UpgradeVote { + /** + * UpgradePropose indicates a proposed upgrade + */ + upgradePropose: string; + + /** + * UpgradeDelay indicates the time between acceptance and execution + */ + upgradeDelay: bigint; + + /** + * UpgradeApprove indicates a yes vote for the current proposal + */ + upgradeApprove: boolean; +} + +/** + * ParticipationUpdates represents participation account data that needs to be checked/acted on by + * the network + */ +export interface ParticipationUpdates { + /** + * ExpiredParticipationAccounts contains a list of online accounts that needs to be converted to + * offline since their participation key expired. + */ + expiredParticipationAccounts: Address[]; + + /** + * AbsentParticipationAccounts contains a list of online accounts that needs to be converted to + * offline since they are not proposing. + */ + absentParticipationAccounts: Address[]; +} + +/** + * Represents the metadata and state of a block. + * + * For more information, refer to: https://github.com/algorand/go-algorand/blob/master/data/bookkeeping/block.go + */ +export interface BlockHeader { + /** + * Round number + */ + round: bigint; + + /** + * Previous block hash + */ + branch: Uint8Array; /** * Sortition seed */ - seed: string; + seed: Uint8Array; + + txnCommitments: TxnCommitments; /** * Timestamp in seconds since epoch */ - ts: number; + timestamp: bigint; + + /** + * Genesis ID to which this block belongs. + */ + genesisID: string; + + /** + * Genesis hash to which this block belongs. + */ + genesisHash: Uint8Array; + + /** + * Proposer is the proposer of this block. Like the Seed, agreement adds this after the block is + * assembled by the transaction pool, so that the same block can be prepared for multiple + * participating accounts in the same node. Populated if proto.Payouts.Enabled + */ + proposer: Address; + + /** + * FeesCollected is the sum of all fees paid by transactions in this block. Populated if + * proto.EnableMining. + */ + feesCollected: bigint; /** - * Transaction root SHA512_256 + * Bonus is the bonus incentive to be paid for proposing this block. It begins as a consensus + * parameter value, and decays periodically. */ - txn: string; + bonus: bigint; + + /** + * ProposerPayout is the amount that should be moved from the FeeSink to the Proposer at the start + * of the next block. It is basically the bonus + the payouts percent of FeesCollected, but may + * be zero'd by proposer ineligibility. + */ + proposerPayout: bigint; + + rewardState: RewardState; + + upgradeState: UpgradeState; + + upgradeVote: UpgradeVote; /** - * Transaction root SHA256 + * TxnCounter is the number of the next transaction that will be committed after this block. Genesis + * blocks can start at either 0 or 1000, depending on a consensus parameter (AppForbidLowResources). */ - txn256: string; + txnCounter: bigint; /** - * StateProofTracking map of type to tracking data + * StateProofTracking tracks the status of the state proofs, potentially for multiple types of + * ASPs (Algorand's State Proofs). */ - spt: Map; + stateproofTracking: Map; + + participationUpdates: ParticipationUpdates; } -export const BLOCK_HEADER_SCHEMA = new NamedMapSchema( - allOmitEmpty([ - { - key: 'fees', - valueSchema: new AddressSchema(), - }, - { - key: 'frac', - valueSchema: new Uint64Schema(), - }, - { - key: 'gen', - valueSchema: new StringSchema(), - }, - { - key: 'gh', - valueSchema: new FixedLengthByteArraySchema(32), - }, - { - key: 'prev', - valueSchema: new FixedLengthByteArraySchema(32), - }, - { - key: 'proto', - valueSchema: new StringSchema(), - }, - { - key: 'rate', - valueSchema: new Uint64Schema(), - }, - { - key: 'rnd', - valueSchema: new Uint64Schema(), - }, - { - key: 'rwcalr', - valueSchema: new Uint64Schema(), - }, - { - key: 'rwd', - valueSchema: new AddressSchema(), - }, - { - key: 'seed', - valueSchema: new FixedLengthByteArraySchema(32), - }, - { - key: 'ts', - valueSchema: new Uint64Schema(), - }, - { - key: 'txn', - valueSchema: new FixedLengthByteArraySchema(32), - }, - { - key: 'txn256', - valueSchema: new FixedLengthByteArraySchema(32), - }, - { - key: 'spt', - valueSchema: new UntypedSchema(), // TODO: fix - }, - ]) -); - -export function blockHeaderFromEncodingData(data: unknown): BlockHeader { - if (!(data instanceof Map)) { - throw new Error(`Invalid decoded BlockHeader: ${data}`); +/** + * A Block contains the Payset and metadata corresponding to a given Round. + */ +export class Block implements Encodable { + public static readonly encodingSchema = new NamedMapSchema( + allOmitEmpty([ + { + key: 'rnd', // header.round + valueSchema: new Uint64Schema(), + }, + { + key: 'prev', // header.branch + valueSchema: new ByteArraySchema(), + }, + { + key: 'seed', // header.seed + valueSchema: new ByteArraySchema(), + }, + { + key: 'txn', // header.txnCommitments.nativeSha512_256Commitment + valueSchema: new FixedLengthByteArraySchema(32), + }, + { + key: 'txn256', // header.txnCommitments.sha256Commitment + valueSchema: new FixedLengthByteArraySchema(32), + }, + { + key: 'ts', // header.timestamp + valueSchema: new Uint64Schema(), + }, + { + key: 'gen', // header.genesisID + valueSchema: new StringSchema(), + }, + { + key: 'gh', // header.genesisHash + valueSchema: new FixedLengthByteArraySchema(32), + }, + { + key: 'prp', // header.proposer + valueSchema: new AddressSchema(), + }, + { + key: 'fc', // header.feesCollected + valueSchema: new Uint64Schema(), + }, + { + key: 'bi', // header.bonus + valueSchema: new Uint64Schema(), + }, + { + key: 'pp', // header.proposerPayout + valueSchema: new Uint64Schema(), + }, + { + key: 'fees', // header.rewardState.feeSink + valueSchema: new AddressSchema(), + }, + { + key: 'rwd', // header.rewardState.rewardsPool + valueSchema: new AddressSchema(), + }, + { + key: 'earn', // header.rewardState.rewaredsLevel + valueSchema: new Uint64Schema(), + }, + { + key: 'rate', // header.rewardState.rewardsRate + valueSchema: new Uint64Schema(), + }, + { + key: 'frac', // header.rewardState.rewardsResidue + valueSchema: new Uint64Schema(), + }, + { + key: 'rwcalr', // header.rewardState.rewardsRecalculationRound + valueSchema: new Uint64Schema(), + }, + { + key: 'proto', // header.upgradeState.currentProtocol + valueSchema: new StringSchema(), + }, + { + key: 'nextproto', // header.upgradeState.nextProtocol + valueSchema: new StringSchema(), + }, + { + key: 'nextyes', // header.upgradeState.nextProtocolApprovals + valueSchema: new Uint64Schema(), + }, + { + key: 'nextbefore', // header.upgradeState.nextProtocolVoteBefore + valueSchema: new Uint64Schema(), + }, + { + key: 'nextswitch', // header.upgradeState.nextProtocolSwitchOn + valueSchema: new Uint64Schema(), + }, + { + key: 'upgradeprop', // header.upgradeVote.upgradePropose + valueSchema: new StringSchema(), + }, + { + key: 'upgradedelay', // header.upgradeVote.upgradeDelay + valueSchema: new Uint64Schema(), + }, + { + key: 'upgradeyes', // header.upgradeVote.upgradeApprove + valueSchema: new Uint64Schema(), + }, + { + key: 'tc', // header.txnCounter + valueSchema: new Uint64Schema(), + }, + { + key: 'spt', // header.stateproofTracking + valueSchema: new Uint64MapSchema(StateProofTrackingData.encodingSchema), + }, + { + key: 'partupdrmv', // header.participationUpdates.expiredParticipationAccounts + valueSchema: new ArraySchema(new AddressSchema()), + }, + { + key: 'partupdabs', // header.participationUpdates.absentParticipationAccounts + valueSchema: new ArraySchema(new AddressSchema()), + }, + { + key: 'txns', // payset + valueSchema: new UntypedSchema(), // TODO: fix + }, + ]) + ); + + public header: BlockHeader; + + public payset: unknown; // TODO: fix + + public constructor(params: { header: BlockHeader; payset: unknown }) { + this.header = params.header; + this.payset = params.payset; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return Block.encodingSchema; } - return { - fees: data.get('fees'), - frac: data.get('frac'), - gen: data.get('gen'), - gh: data.get('gh'), - prev: data.get('prev'), - proto: data.get('proto'), - rate: data.get('rate'), - rnd: data.get('rnd'), - rwcalr: data.get('rwcalr'), - rwd: data.get('rwd'), - seed: data.get('seed'), - ts: data.get('ts'), - txn: data.get('txn'), - txn256: data.get('txn256'), - spt: data.get('spt'), - }; -} -export function blockHeaderToEncodingData( - header: BlockHeader -): Map { - return new Map([ - ['fees', header.fees], - ['frac', header.frac], - ['gen', header.gen], - ['gh', header.gh], - ['prev', header.prev], - ['proto', header.proto], - ['rate', header.rate], - ['rnd', header.rnd], - ['rwcalr', header.rwcalr], - ['rwd', header.rwd], - ['seed', header.seed], - ['ts', header.ts], - ['txn', header.txn], - ['txn256', header.txn256], - ['spt', header.spt], - ]); + public toEncodingData(): unknown { + return new Map([ + ['rnd', this.header.round], + ['prev', this.header.branch], + ['seed', this.header.seed], + ['txn', this.header.txnCommitments.nativeSha512_256Commitment], + ['txn256', this.header.txnCommitments.sha256Commitment], + ['ts', this.header.timestamp], + ['gen', this.header.genesisID], + ['gh', this.header.genesisHash], + ['prp', this.header.proposer], + ['fc', this.header.feesCollected], + ['bi', this.header.bonus], + ['pp', this.header.proposerPayout], + ['fees', this.header.rewardState.feeSink], + ['rwd', this.header.rewardState.rewardsPool], + ['earn', this.header.rewardState.rewaredsLevel], + ['rate', this.header.rewardState.rewardsRate], + ['frac', this.header.rewardState.rewardsResidue], + ['rwcalr', this.header.rewardState.rewardsRecalculationRound], + ['proto', this.header.upgradeState.currentProtocol], + ['nextproto', this.header.upgradeState.nextProtocol], + ['nextyes', this.header.upgradeState.nextProtocolApprovals], + ['nextbefore', this.header.upgradeState.nextProtocolVoteBefore], + ['nextswitch', this.header.upgradeState.nextProtocolSwitchOn], + ['upgradeprop', this.header.upgradeVote.upgradePropose], + ['upgradedelay', this.header.upgradeVote.upgradeDelay], + ['upgradeyes', this.header.upgradeVote.upgradeApprove], + ['tc', this.header.txnCounter], + ['spt', this.header.stateproofTracking], + [ + 'partupdrmv', + this.header.participationUpdates.expiredParticipationAccounts, + ], + [ + 'partupdabs', + this.header.participationUpdates.absentParticipationAccounts, + ], + ['txns', this.payset], // TODO: fix + ]); + } + + public static fromEncodingData(data: unknown): Block { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded BlockHeader: ${data}`); + } + return new Block({ + header: { + round: data.get('rnd'), + branch: data.get('prev'), + seed: data.get('seed'), + txnCommitments: { + nativeSha512_256Commitment: data.get('txn'), + sha256Commitment: data.get('txn256'), + }, + timestamp: data.get('ts'), + genesisHash: data.get('gen'), + genesisID: data.get('gh'), + proposer: data.get('prp'), + feesCollected: data.get('fc'), + bonus: data.get('bi'), + proposerPayout: data.get('pp'), + rewardState: { + feeSink: data.get('fees'), + rewardsPool: data.get('rwd'), + rewaredsLevel: data.get('earn'), + rewardsRate: data.get('rate'), + rewardsResidue: data.get('frac'), + rewardsRecalculationRound: data.get('rwcalr'), + }, + upgradeState: { + currentProtocol: data.get('proto'), + nextProtocol: data.get('nextproto'), + nextProtocolApprovals: data.get('nextyes'), + nextProtocolVoteBefore: data.get('nextbefore'), + nextProtocolSwitchOn: data.get('nextswitch'), + }, + upgradeVote: { + upgradePropose: data.get('upgradeprop'), + upgradeDelay: data.get('upgradedelay'), + upgradeApprove: data.get('upgradeyes'), + }, + txnCounter: data.get('tc'), + stateproofTracking: new Map( + Array.from( + (data.get('spt') as Map).entries() + ).map(([k, v]) => [Number(k), v]) + ), + participationUpdates: { + expiredParticipationAccounts: data.get('partupdrmv'), + absentParticipationAccounts: data.get('partupdabs'), + }, + }, + payset: data.get('txn'), // TODO: fix + }); + } } diff --git a/tests/2.Encoding.ts b/tests/2.Encoding.ts index c605c225d..e9dab8cc0 100644 --- a/tests/2.Encoding.ts +++ b/tests/2.Encoding.ts @@ -26,7 +26,7 @@ const ERROR_CONTAINS_EMPTY_STRING = describe('encoding', () => { class ExampleEncodable implements algosdk.Encodable { - static encodingSchema = new NamedMapSchema( + static readonly encodingSchema = new NamedMapSchema( allOmitEmpty([ { key: 'a', valueSchema: new Uint64Schema() }, { key: 'b', valueSchema: new StringSchema() }, diff --git a/tests/cucumber/steps/steps.js b/tests/cucumber/steps/steps.js index 1cc828b1f..bba3a830b 100644 --- a/tests/cucumber/steps/steps.js +++ b/tests/cucumber/steps/steps.js @@ -1915,9 +1915,12 @@ module.exports = function getSteps(options) { Then( 'the parsed Get Block response should have rewards pool {string}', (rewardsPoolAddress) => { - assert.ok(anyBlockResponse.block.rwd instanceof algosdk.Address); + assert.ok( + anyBlockResponse.block.header.rewardState.rewardsPool instanceof + algosdk.Address + ); const rewardsPoolB64String = algosdk.bytesToBase64( - anyBlockResponse.block.rwd.publicKey + anyBlockResponse.block.header.rewardState.rewardsPool.publicKey ); assert.strictEqual(rewardsPoolAddress, rewardsPoolB64String); } From aca0748f91d922c29eda2932241c73e07ca630df Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 12 Jun 2024 12:52:32 -0400 Subject: [PATCH 2/9] Add map embedding capability --- src/client/v2/algod/models/types.ts | 142 ++++++++++---------- src/client/v2/indexer/models/types.ts | 122 ++++++++--------- src/encoding/schema/map.ts | 89 +++++++++++- tests/2.Encoding.ts | 186 ++++++++++++++++++++++++++ 4 files changed, 400 insertions(+), 139 deletions(-) diff --git a/src/client/v2/algod/models/types.ts b/src/client/v2/algod/models/types.ts index f3238f765..55c611fb1 100644 --- a/src/client/v2/algod/models/types.ts +++ b/src/client/v2/algod/models/types.ts @@ -31,7 +31,7 @@ export class Account implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'address', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'amount', valueSchema: new Uint64Schema(), omitEmpty: true }, { @@ -632,7 +632,7 @@ export class AccountApplicationResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'round', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'app-local-state', @@ -741,7 +741,7 @@ export class AccountAssetHolding implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'asset-holding', valueSchema: AssetHolding.encodingSchema, @@ -831,7 +831,7 @@ export class AccountAssetResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'round', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'asset-holding', @@ -937,7 +937,7 @@ export class AccountAssetsInformationResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'round', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'asset-holdings', @@ -1035,7 +1035,7 @@ export class AccountParticipation implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'selection-participation-key', valueSchema: new ByteArraySchema(), @@ -1184,7 +1184,7 @@ export class AccountStateDelta implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'address', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'delta', @@ -1254,7 +1254,7 @@ export class AppCallLogs implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'application-index', valueSchema: new Uint64Schema(), @@ -1340,7 +1340,7 @@ export class Application implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'id', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'params', @@ -1413,7 +1413,7 @@ export class ApplicationInitialStates implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'id', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'app-boxes', @@ -1543,7 +1543,7 @@ export class ApplicationKVStorage implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'kvs', valueSchema: new ArraySchema(AvmKeyValue.encodingSchema), @@ -1625,7 +1625,7 @@ export class ApplicationLocalReference implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'account', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'app', valueSchema: new Uint64Schema(), omitEmpty: true } ); @@ -1692,7 +1692,7 @@ export class ApplicationLocalState implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'id', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'schema', @@ -1792,7 +1792,7 @@ export class ApplicationParams implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'approval-program', valueSchema: new ByteArraySchema(), @@ -1990,7 +1990,7 @@ export class ApplicationStateOperation implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'app-state-type', valueSchema: new StringSchema(), @@ -2122,7 +2122,7 @@ export class ApplicationStateSchema implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'num-byte-slice', valueSchema: new Uint64Schema(), @@ -2192,7 +2192,7 @@ export class Asset implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'index', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'params', @@ -2270,7 +2270,7 @@ export class AssetHolding implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'amount', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'asset-id', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'is-frozen', valueSchema: new BooleanSchema(), omitEmpty: true } @@ -2348,7 +2348,7 @@ export class AssetHoldingReference implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'account', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'asset', valueSchema: new Uint64Schema(), omitEmpty: true } ); @@ -2418,7 +2418,7 @@ export class AssetParams implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'creator', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'decimals', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'total', valueSchema: new Uint64Schema(), omitEmpty: true }, @@ -2717,7 +2717,7 @@ export class AvmKeyValue implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'key', valueSchema: new ByteArraySchema(), omitEmpty: true }, { key: 'value', valueSchema: AvmValue.encodingSchema, omitEmpty: true } ); @@ -2774,7 +2774,7 @@ export class AvmValue implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'type', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'bytes', @@ -2860,7 +2860,7 @@ export class BlockHashResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push({ + (this.encodingSchemaValue as NamedMapSchema).pushEntries({ key: 'blockHash', valueSchema: new StringSchema(), omitEmpty: true, @@ -2916,7 +2916,7 @@ export class BlockLogsResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push({ + (this.encodingSchemaValue as NamedMapSchema).pushEntries({ key: 'logs', valueSchema: new ArraySchema(AppCallLogs.encodingSchema), omitEmpty: true, @@ -2967,7 +2967,7 @@ export class BlockResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'block', valueSchema: Block.encodingSchema, omitEmpty: true }, { key: 'cert', @@ -3041,7 +3041,7 @@ export class BlockTxidsResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push({ + (this.encodingSchemaValue as NamedMapSchema).pushEntries({ key: 'blockTxids', valueSchema: new ArraySchema(new StringSchema()), omitEmpty: true, @@ -3091,7 +3091,7 @@ export class Box implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'name', valueSchema: new ByteArraySchema(), omitEmpty: true }, { key: 'round', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'value', valueSchema: new ByteArraySchema(), omitEmpty: true } @@ -3169,7 +3169,7 @@ export class BoxDescriptor implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push({ + (this.encodingSchemaValue as NamedMapSchema).pushEntries({ key: 'name', valueSchema: new ByteArraySchema(), omitEmpty: true, @@ -3219,7 +3219,7 @@ export class BoxReference implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'app', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'name', valueSchema: new ByteArraySchema(), omitEmpty: true } ); @@ -3285,7 +3285,7 @@ export class BoxesResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push({ + (this.encodingSchemaValue as NamedMapSchema).pushEntries({ key: 'boxes', valueSchema: new ArraySchema(BoxDescriptor.encodingSchema), omitEmpty: true, @@ -3333,7 +3333,7 @@ export class BuildVersion implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'branch', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'build_number', @@ -3437,7 +3437,7 @@ export class CompileResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'hash', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'result', valueSchema: new StringSchema(), omitEmpty: true }, { @@ -3527,7 +3527,7 @@ export class DisassembleResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push({ + (this.encodingSchemaValue as NamedMapSchema).pushEntries({ key: 'result', valueSchema: new StringSchema(), omitEmpty: true, @@ -3578,7 +3578,7 @@ export class DryrunRequest implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'accounts', valueSchema: new ArraySchema(Account.encodingSchema), @@ -3730,7 +3730,7 @@ export class DryrunResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'error', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'protocol-version', @@ -3813,7 +3813,7 @@ export class DryrunSource implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'app-index', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'field-name', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'source', valueSchema: new StringSchema(), omitEmpty: true }, @@ -3898,7 +3898,7 @@ export class DryrunState implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'line', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'pc', valueSchema: new Uint64Schema(), omitEmpty: true }, { @@ -4021,7 +4021,7 @@ export class DryrunTxnResult implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'disassembly', valueSchema: new ArraySchema(new StringSchema()), @@ -4277,7 +4277,7 @@ export class ErrorResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'message', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'data', @@ -4343,7 +4343,7 @@ export class EvalDelta implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'action', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'bytes', @@ -4429,7 +4429,7 @@ export class EvalDeltaKeyValue implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'key', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'value', valueSchema: EvalDelta.encodingSchema, omitEmpty: true } ); @@ -4486,7 +4486,7 @@ export class GetBlockTimeStampOffsetResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push({ + (this.encodingSchemaValue as NamedMapSchema).pushEntries({ key: 'offset', valueSchema: new Uint64Schema(), omitEmpty: true, @@ -4536,7 +4536,7 @@ export class GetSyncRoundResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push({ + (this.encodingSchemaValue as NamedMapSchema).pushEntries({ key: 'round', valueSchema: new Uint64Schema(), omitEmpty: true, @@ -4587,7 +4587,7 @@ export class KvDelta implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'key', valueSchema: new OptionalSchema(new ByteArraySchema()), @@ -4661,7 +4661,7 @@ export class LedgerStateDeltaForTransactionGroup implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'Delta', valueSchema: UntypedValue.encodingSchema, @@ -4726,7 +4726,7 @@ export class LightBlockHeaderProof implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'index', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'proof', valueSchema: new ByteArraySchema(), omitEmpty: true }, { key: 'treedepth', valueSchema: new Uint64Schema(), omitEmpty: true } @@ -4806,7 +4806,7 @@ export class NodeStatusResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'catchup-time', valueSchema: new Uint64Schema(), @@ -5324,7 +5324,7 @@ export class PendingTransactionResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'pool-error', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'txn', @@ -5660,7 +5660,7 @@ export class PendingTransactionsResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'top-transactions', valueSchema: new ArraySchema(SignedTransaction.encodingSchema), @@ -5736,7 +5736,7 @@ export class PostTransactionsResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push({ + (this.encodingSchemaValue as NamedMapSchema).pushEntries({ key: 'txId', valueSchema: new StringSchema(), omitEmpty: true, @@ -5786,7 +5786,7 @@ export class ScratchChange implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'new-value', valueSchema: AvmValue.encodingSchema, @@ -5856,7 +5856,7 @@ export class SimulateInitialStates implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push({ + (this.encodingSchemaValue as NamedMapSchema).pushEntries({ key: 'app-initial-states', valueSchema: new OptionalSchema( new ArraySchema(ApplicationInitialStates.encodingSchema) @@ -5926,7 +5926,7 @@ export class SimulateRequest implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'txn-groups', valueSchema: new ArraySchema( @@ -6103,7 +6103,7 @@ export class SimulateRequestTransactionGroup implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push({ + (this.encodingSchemaValue as NamedMapSchema).pushEntries({ key: 'txns', valueSchema: new ArraySchema(SignedTransaction.encodingSchema), omitEmpty: true, @@ -6157,7 +6157,7 @@ export class SimulateResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'last-round', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'txn-groups', @@ -6323,7 +6323,7 @@ export class SimulateTraceConfig implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'enable', valueSchema: new OptionalSchema(new BooleanSchema()), @@ -6435,7 +6435,7 @@ export class SimulateTransactionGroupResult implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'txn-results', valueSchema: new ArraySchema( @@ -6622,7 +6622,7 @@ export class SimulateTransactionResult implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'txn-result', valueSchema: PendingTransactionResponse.encodingSchema, @@ -6806,7 +6806,7 @@ export class SimulateUnnamedResourcesAccessed implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'accounts', valueSchema: new OptionalSchema(new ArraySchema(new StringSchema())), @@ -7025,7 +7025,7 @@ export class SimulationEvalOverrides implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'allow-empty-signatures', valueSchema: new OptionalSchema(new BooleanSchema()), @@ -7158,7 +7158,7 @@ export class SimulationOpcodeTraceUnit implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'pc', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'scratch-changes', @@ -7337,7 +7337,7 @@ export class SimulationTransactionExecTrace implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'approval-program-hash', valueSchema: new OptionalSchema(new ByteArraySchema()), @@ -7596,7 +7596,7 @@ export class StateProof implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'Message', valueSchema: StateProofMessage.encodingSchema, @@ -7673,7 +7673,7 @@ export class StateProofMessage implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'BlockHeadersCommitment', valueSchema: new ByteArraySchema(), @@ -7805,7 +7805,7 @@ export class SupplyResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'current_round', valueSchema: new Uint64Schema(), @@ -7891,7 +7891,7 @@ export class TealKeyValue implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'key', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'value', valueSchema: TealValue.encodingSchema, omitEmpty: true } ); @@ -7948,7 +7948,7 @@ export class TealValue implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'bytes', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'type', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'uint', valueSchema: new Uint64Schema(), omitEmpty: true } @@ -8029,7 +8029,7 @@ export class TransactionGroupLedgerStateDeltasForRoundResponse static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push({ + (this.encodingSchemaValue as NamedMapSchema).pushEntries({ key: 'Deltas', valueSchema: new ArraySchema( LedgerStateDeltaForTransactionGroup.encodingSchema @@ -8085,7 +8085,7 @@ export class TransactionParametersResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'consensus-version', valueSchema: new StringSchema(), @@ -8220,7 +8220,7 @@ export class TransactionProofResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'idx', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'proof', valueSchema: new ByteArraySchema(), omitEmpty: true }, { @@ -8337,7 +8337,7 @@ export class Version implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'build', valueSchema: BuildVersion.encodingSchema, diff --git a/src/client/v2/indexer/models/types.ts b/src/client/v2/indexer/models/types.ts index 1c4f3e384..6e7c46726 100644 --- a/src/client/v2/indexer/models/types.ts +++ b/src/client/v2/indexer/models/types.ts @@ -29,7 +29,7 @@ export class Account implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'address', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'amount', valueSchema: new Uint64Schema(), omitEmpty: true }, { @@ -674,7 +674,7 @@ export class AccountParticipation implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'selection-participation-key', valueSchema: new ByteArraySchema(), @@ -821,7 +821,7 @@ export class AccountResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'account', valueSchema: Account.encodingSchema, @@ -899,7 +899,7 @@ export class AccountStateDelta implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'address', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'delta', @@ -968,7 +968,7 @@ export class AccountsResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'accounts', valueSchema: new ArraySchema(Account.encodingSchema), @@ -1059,7 +1059,7 @@ export class Application implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'id', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'params', @@ -1185,7 +1185,7 @@ export class ApplicationLocalState implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'id', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'schema', @@ -1341,7 +1341,7 @@ export class ApplicationLocalStatesResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'apps-local-states', valueSchema: new ArraySchema(ApplicationLocalState.encodingSchema), @@ -1435,7 +1435,7 @@ export class ApplicationLogData implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'logs', valueSchema: new ArraySchema(new ByteArraySchema()), @@ -1499,7 +1499,7 @@ export class ApplicationLogsResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'application-id', valueSchema: new Uint64Schema(), @@ -1616,7 +1616,7 @@ export class ApplicationParams implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'approval-program', valueSchema: new ByteArraySchema(), @@ -1823,7 +1823,7 @@ export class ApplicationResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'current-round', valueSchema: new Uint64Schema(), @@ -1905,7 +1905,7 @@ export class ApplicationStateSchema implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'num-byte-slice', valueSchema: new Uint64Schema(), @@ -1975,7 +1975,7 @@ export class ApplicationsResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'applications', valueSchema: new ArraySchema(Application.encodingSchema), @@ -2066,7 +2066,7 @@ export class Asset implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'index', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'params', @@ -2196,7 +2196,7 @@ export class AssetBalancesResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'balances', valueSchema: new ArraySchema(MiniAssetHolding.encodingSchema), @@ -2289,7 +2289,7 @@ export class AssetHolding implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'amount', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'asset-id', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'is-frozen', valueSchema: new BooleanSchema(), omitEmpty: true }, @@ -2421,7 +2421,7 @@ export class AssetHoldingsResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'assets', valueSchema: new ArraySchema(AssetHolding.encodingSchema), @@ -2515,7 +2515,7 @@ export class AssetParams implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'creator', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'decimals', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'total', valueSchema: new Uint64Schema(), omitEmpty: true }, @@ -2814,7 +2814,7 @@ export class AssetResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'asset', valueSchema: Asset.encodingSchema, omitEmpty: true }, { key: 'current-round', @@ -2884,7 +2884,7 @@ export class AssetsResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'assets', valueSchema: new ArraySchema(Asset.encodingSchema), @@ -2977,7 +2977,7 @@ export class Block implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'genesis-hash', valueSchema: new ByteArraySchema(), @@ -3417,7 +3417,7 @@ export class BlockRewards implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'fee-sink', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'rewards-calculation-round', @@ -3561,7 +3561,7 @@ export class BlockUpgradeState implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'current-protocol', valueSchema: new StringSchema(), @@ -3694,7 +3694,7 @@ export class BlockUpgradeVote implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'upgrade-approve', valueSchema: new OptionalSchema(new BooleanSchema()), @@ -3787,7 +3787,7 @@ export class Box implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'name', valueSchema: new ByteArraySchema(), omitEmpty: true }, { key: 'round', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'value', valueSchema: new ByteArraySchema(), omitEmpty: true } @@ -3865,7 +3865,7 @@ export class BoxDescriptor implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push({ + (this.encodingSchemaValue as NamedMapSchema).pushEntries({ key: 'name', valueSchema: new ByteArraySchema(), omitEmpty: true, @@ -3915,7 +3915,7 @@ export class BoxesResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'application-id', valueSchema: new Uint64Schema(), @@ -4006,7 +4006,7 @@ export class ErrorResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'message', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'data', @@ -4072,7 +4072,7 @@ export class EvalDelta implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'action', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'bytes', @@ -4158,7 +4158,7 @@ export class EvalDeltaKeyValue implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'key', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'value', valueSchema: EvalDelta.encodingSchema, omitEmpty: true } ); @@ -4212,7 +4212,7 @@ export class HashFactory implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push({ + (this.encodingSchemaValue as NamedMapSchema).pushEntries({ key: 'hash-type', valueSchema: new OptionalSchema(new Uint64Schema()), omitEmpty: true, @@ -4263,7 +4263,7 @@ export class HealthCheck implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'db-available', valueSchema: new BooleanSchema(), @@ -4392,7 +4392,7 @@ export class IndexerStateProofMessage implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'block-headers-commitment', valueSchema: new OptionalSchema(new ByteArraySchema()), @@ -4526,7 +4526,7 @@ export class MerkleArrayProof implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'hash-factory', valueSchema: new OptionalSchema(HashFactory.encodingSchema), @@ -4626,7 +4626,7 @@ export class MiniAssetHolding implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'address', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'amount', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'is-frozen', valueSchema: new BooleanSchema(), omitEmpty: true }, @@ -4749,7 +4749,7 @@ export class ParticipationUpdates implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'absent-participation-accounts', valueSchema: new OptionalSchema(new ArraySchema(new StringSchema())), @@ -4827,7 +4827,7 @@ export class StateProofFields implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'part-proofs', valueSchema: new OptionalSchema(MerkleArrayProof.encodingSchema), @@ -5018,7 +5018,7 @@ export class StateProofParticipant implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'verifier', valueSchema: new OptionalSchema(StateProofVerifier.encodingSchema), @@ -5098,7 +5098,7 @@ export class StateProofReveal implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'participant', valueSchema: new OptionalSchema(StateProofParticipant.encodingSchema), @@ -5204,7 +5204,7 @@ export class StateProofSigSlot implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'lower-sig-weight', valueSchema: new OptionalSchema(new Uint64Schema()), @@ -5283,7 +5283,7 @@ export class StateProofSignature implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'falcon-signature', valueSchema: new OptionalSchema(new ByteArraySchema()), @@ -5394,7 +5394,7 @@ export class StateProofTracking implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'next-round', valueSchema: new OptionalSchema(new Uint64Schema()), @@ -5509,7 +5509,7 @@ export class StateProofVerifier implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'commitment', valueSchema: new OptionalSchema(new ByteArraySchema()), @@ -5590,7 +5590,7 @@ export class StateSchema implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'num-byte-slice', valueSchema: new Uint64Schema(), @@ -5660,7 +5660,7 @@ export class TealKeyValue implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'key', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'value', valueSchema: TealValue.encodingSchema, omitEmpty: true } ); @@ -5717,7 +5717,7 @@ export class TealValue implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'bytes', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'type', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'uint', valueSchema: new Uint64Schema(), omitEmpty: true } @@ -5799,7 +5799,7 @@ export class Transaction implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'fee', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'first-valid', @@ -6615,7 +6615,7 @@ export class TransactionApplication implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'application-id', valueSchema: new Uint64Schema(), @@ -6930,7 +6930,7 @@ export class TransactionAssetConfig implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'asset-id', valueSchema: new OptionalSchema(new Uint64Schema()), @@ -7021,7 +7021,7 @@ export class TransactionAssetFreeze implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'address', valueSchema: new StringSchema(), omitEmpty: true }, { key: 'asset-id', valueSchema: new Uint64Schema(), omitEmpty: true }, { @@ -7105,7 +7105,7 @@ export class TransactionAssetTransfer implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'amount', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'asset-id', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'receiver', valueSchema: new StringSchema(), omitEmpty: true }, @@ -7246,7 +7246,7 @@ export class TransactionKeyreg implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'non-participation', valueSchema: new OptionalSchema(new BooleanSchema()), @@ -7422,7 +7422,7 @@ export class TransactionPayment implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'amount', valueSchema: new Uint64Schema(), omitEmpty: true }, { key: 'receiver', valueSchema: new StringSchema(), omitEmpty: true }, { @@ -7527,7 +7527,7 @@ export class TransactionResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'current-round', valueSchema: new Uint64Schema(), @@ -7612,7 +7612,7 @@ export class TransactionSignature implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'logicsig', valueSchema: new OptionalSchema( @@ -7732,7 +7732,7 @@ export class TransactionSignatureLogicsig implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'logic', valueSchema: new ByteArraySchema(), omitEmpty: true }, { key: 'args', @@ -7857,7 +7857,7 @@ export class TransactionSignatureMultisig implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'subsignature', valueSchema: new OptionalSchema( @@ -7964,7 +7964,7 @@ export class TransactionSignatureMultisigSubsignature implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'public-key', valueSchema: new OptionalSchema(new ByteArraySchema()), @@ -8044,7 +8044,7 @@ export class TransactionStateProof implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'message', valueSchema: new OptionalSchema( @@ -8161,7 +8161,7 @@ export class TransactionsResponse implements Encodable { static get encodingSchema(): Schema { if (!this.encodingSchemaValue) { this.encodingSchemaValue = new NamedMapSchema([]); - (this.encodingSchemaValue as NamedMapSchema).entries.push( + (this.encodingSchemaValue as NamedMapSchema).pushEntries( { key: 'current-round', valueSchema: new Uint64Schema(), diff --git a/src/encoding/schema/map.ts b/src/encoding/schema/map.ts index 391857ba8..bdfa84bfe 100644 --- a/src/encoding/schema/map.ts +++ b/src/encoding/schema/map.ts @@ -3,10 +3,30 @@ import { ensureUint64 } from '../../utils/utils.js'; /* eslint-disable class-methods-use-this */ +/** + * Describes a key-value entry in a NamedMapSchema. + */ export interface NamedMapEntry { + /** + * Key of the entry. Must be unique for this map. + */ key: string; + /** + * The Schema for the entry's value. + */ valueSchema: Schema; + /** + * If true, the entry will be omitted from the encoding if the value is the default value. + */ omitEmpty: boolean; + /** + * If true, valueSchema must be a NamedMapSchema and key must be the empty string. The fields of + * valueSchema will be embedded directly in the parent map. + * + * omitEmpty is ignored for embedded entries. Instead, the individual omitEmpty values of the + * embedded fields are used. + */ + embedded?: boolean; } export function allOmitEmpty( @@ -19,13 +39,68 @@ export function allOmitEmpty( * Schema for a map/struct with a fixed set of string fields. */ export class NamedMapSchema extends Schema { - constructor(public readonly entries: NamedMapEntry[]) { + private readonly entries: NamedMapEntry[]; + + constructor(entries: NamedMapEntry[]) { super(); + this.entries = entries; + this.checkEntries(); + } + + /** + * Adds new entries to the map schema. WARNING: this is a mutable operation, and you should be very + * careful when using it. Any error that happens here is non-recoverable and will corrupt the + * NamedMapSchema object; + * @param entries - The entries to add. + */ + public pushEntries(...entries: NamedMapEntry[]) { + this.entries.push(...entries); + this.checkEntries(); + } + + private checkEntries() { + for (const entry of this.entries) { + if (entry.embedded) { + if (entry.key !== '') { + throw new Error('Embedded entries must have an empty key'); + } + if (!(entry.valueSchema instanceof NamedMapSchema)) { + throw new Error( + 'Embedded entry valueSchema must be a NamedMapSchema' + ); + } + } + } + + const keys = new Set(); + for (const entry of this.getEntries()) { + if (keys.has(entry.key)) { + throw new Error(`Duplicate key: ${entry.key}`); + } + keys.add(entry.key); + } + } + + /** + * Returns all top-level entries, properly accounting for fields from embedded entries. + * @returns An array of all top-level entries for this map. + */ + public getEntries(): NamedMapEntry[] { + const entries: NamedMapEntry[] = []; + for (const entry of this.entries) { + if (entry.embedded) { + const embeddedMapSchema = entry.valueSchema as NamedMapSchema; + entries.push(...embeddedMapSchema.getEntries()); + } else { + entries.push(entry); + } + } + return entries; } public defaultValue(): Map { const map = new Map(); - for (const entry of this.entries) { + for (const entry of this.getEntries()) { map.set(entry.key, entry.valueSchema.defaultValue()); } return map; @@ -33,7 +108,7 @@ export class NamedMapSchema extends Schema { public isDefaultValue(data: unknown): boolean { if (!(data instanceof Map)) return false; - for (const entry of this.entries) { + for (const entry of this.getEntries()) { if (!entry.valueSchema.isDefaultValue(data.get(entry.key))) { return false; } @@ -48,7 +123,7 @@ export class NamedMapSchema extends Schema { ); } const map = new Map(); - for (const entry of this.entries) { + for (const entry of this.getEntries()) { const value = data.get(entry.key); if (entry.omitEmpty && entry.valueSchema.isDefaultValue(value)) { continue; @@ -65,7 +140,7 @@ export class NamedMapSchema extends Schema { throw new Error('NamedMapSchema data must be a Map'); } const map = new Map(); - for (const entry of this.entries) { + for (const entry of this.getEntries()) { if (encoded.has(entry.key)) { map.set( entry.key, @@ -85,7 +160,7 @@ export class NamedMapSchema extends Schema { throw new Error('NamedMapSchema data must be a Map'); } const obj: { [key: string]: JSONEncodingData } = {}; - for (const entry of this.entries) { + for (const entry of this.getEntries()) { const value = data.get(entry.key); if (entry.omitEmpty && entry.valueSchema.isDefaultValue(value)) { continue; @@ -104,7 +179,7 @@ export class NamedMapSchema extends Schema { throw new Error('NamedMapSchema data must be an object'); } const map = new Map(); - for (const entry of this.entries) { + for (const entry of this.getEntries()) { if (Object.prototype.hasOwnProperty.call(encoded, entry.key)) { map.set( entry.key, diff --git a/tests/2.Encoding.ts b/tests/2.Encoding.ts index e9dab8cc0..0b7dd0fc2 100644 --- a/tests/2.Encoding.ts +++ b/tests/2.Encoding.ts @@ -16,6 +16,7 @@ import { FixedLengthByteArraySchema, ArraySchema, NamedMapSchema, + NamedMapEntry, UntypedSchema, OptionalSchema, allOmitEmpty, @@ -1495,6 +1496,191 @@ describe('encoding', () => { } ); }); + + it('correctly embeds other maps', () => { + const bSchema = new NamedMapSchema([ + { + key: 'b', + omitEmpty: true, + valueSchema: new Uint64Schema(), + }, + ]); + + const abSchema = new NamedMapSchema([ + { + key: 'a', + omitEmpty: true, + valueSchema: new StringSchema(), + }, + { + key: '', + omitEmpty: true, + valueSchema: bSchema, + embedded: true, + }, + ]); + + const emptySchema = new NamedMapSchema([]); + + const abcdSchema = new NamedMapSchema([ + { + key: '', + omitEmpty: true, + valueSchema: abSchema, + embedded: true, + }, + { + key: 'c', + omitEmpty: true, + valueSchema: new BooleanSchema(), + }, + { + key: 'd', + omitEmpty: true, + valueSchema: new ArraySchema(new StringSchema()), + }, + { + key: '', + omitEmpty: true, + valueSchema: emptySchema, + embedded: true, + }, + ]); + + const actualEntries = abcdSchema.getEntries(); + const expectedEntries: NamedMapEntry[] = [ + { key: 'a', omitEmpty: true, valueSchema: new StringSchema() }, + { key: 'b', omitEmpty: true, valueSchema: new Uint64Schema() }, + { key: 'c', omitEmpty: true, valueSchema: new BooleanSchema() }, + { + key: 'd', + omitEmpty: true, + valueSchema: new ArraySchema(new StringSchema()), + }, + ]; + assert.deepStrictEqual(actualEntries, expectedEntries); + + const acutalDefaultValue = abcdSchema.defaultValue(); + const expectedDefaultValue = new Map([ + ['a', ''], + ['b', BigInt(0)], + ['c', false], + ['d', []], + ]); + assert.deepStrictEqual(acutalDefaultValue, expectedDefaultValue); + }); + + it('correctly pushes new entries', () => { + const schema = new NamedMapSchema([ + { + key: 'a', + omitEmpty: true, + valueSchema: new StringSchema(), + }, + ]); + + schema.pushEntries({ + key: 'b', + omitEmpty: true, + valueSchema: new Uint64Schema(), + }); + + const actualEntries = schema.getEntries(); + const expectedEntries: NamedMapEntry[] = [ + { key: 'a', omitEmpty: true, valueSchema: new StringSchema() }, + { key: 'b', omitEmpty: true, valueSchema: new Uint64Schema() }, + ]; + assert.deepStrictEqual(actualEntries, expectedEntries); + + assert.throws( + () => + schema.pushEntries({ + key: 'a', + omitEmpty: true, + valueSchema: new StringSchema(), + }), + new Error('Duplicate key: a') + ); + }); + + it('errors on invalid constructor args', () => { + assert.throws( + () => + new NamedMapSchema([ + { + key: 'a', + omitEmpty: true, + valueSchema: new StringSchema(), + }, + { + key: 'a', + omitEmpty: true, + valueSchema: new StringSchema(), + }, + ]), + new Error('Duplicate key: a') + ); + + assert.throws( + () => + new NamedMapSchema([ + { + key: 'a', + omitEmpty: true, + valueSchema: new StringSchema(), + }, + { + key: '', + omitEmpty: true, + valueSchema: new NamedMapSchema([ + { + key: 'a', + omitEmpty: true, + valueSchema: new StringSchema(), + }, + ]), + embedded: true, + }, + ]), + new Error('Duplicate key: a') + ); + + assert.throws( + () => + new NamedMapSchema([ + { + key: 'a', + omitEmpty: true, + valueSchema: new StringSchema(), + }, + { + key: 'x', + omitEmpty: true, + valueSchema: new NamedMapSchema([]), + embedded: true, + }, + ]), + new Error('Embedded entries must have an empty key') + ); + + assert.throws( + () => + new NamedMapSchema([ + { + key: 'a', + omitEmpty: true, + valueSchema: new StringSchema(), + }, + { + key: '', + omitEmpty: true, + valueSchema: new StringSchema(), + embedded: true, + }, + ]), + new Error('Embedded entry valueSchema must be a NamedMapSchema') + ); + }); }); }); }); From 830e7b4624446bd8690cf91e3e15b3cf328d9331 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 12 Jun 2024 13:01:22 -0400 Subject: [PATCH 3/9] Test Uint64MapSchema --- tests/2.Encoding.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/2.Encoding.ts b/tests/2.Encoding.ts index 0b7dd0fc2..6c208ce79 100644 --- a/tests/2.Encoding.ts +++ b/tests/2.Encoding.ts @@ -17,6 +17,7 @@ import { ArraySchema, NamedMapSchema, NamedMapEntry, + Uint64MapSchema, UntypedSchema, OptionalSchema, allOmitEmpty, @@ -1053,6 +1054,37 @@ describe('encoding', () => { preparedMsgpackValues: [undefined, true, false], preparedJsonValues: [null, true, false], }, + { + name: 'Uint64MapSchema of BooleanSchema', + schema: new Uint64MapSchema(new BooleanSchema()), + values: [ + new Map(), + new Map([ + [0n, true], + [1n, false], + [2n, true], + [BigInt('18446744073709551615'), true], + ]), + ], + preparedMsgpackValues: [ + new Map(), + new Map([ + [0n, true], + [1n, false], + [2n, true], + [BigInt('18446744073709551615'), true], + ]), + ], + preparedJsonValues: [ + {}, + { + 0: true, + 1: false, + 2: true, + '18446744073709551615': true, + }, + ], + }, ]; const primitiveTestcases = testcases.slice(); @@ -1260,6 +1292,16 @@ describe('encoding', () => { emptyValueRestored: new Map([['key', undefined]]), nonemptyValue: new Map([['key', true]]), }, + { + schema: new Uint64MapSchema(new BooleanSchema()), + emptyValue: new Map(), + nonemptyValue: new Map([ + [0n, true], + [1n, false], + [2n, true], + [BigInt('18446744073709551615'), true], + ]), + }, ]; for (const testValue of testValues.slice()) { From 5b76a45a3df2895c76cfa75688dc33b8607533ad Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Wed, 12 Jun 2024 13:35:44 -0400 Subject: [PATCH 4/9] Use embedded maps in Block types --- src/client/v2/algod/models/types.ts | 2 +- src/encoding/schema/index.ts | 1 + src/encoding/schema/map.ts | 13 + src/main.ts | 1 + src/types/block.ts | 798 ++++++++++++++++++++++++++++ src/types/blockHeader.ts | 538 ------------------- 6 files changed, 814 insertions(+), 539 deletions(-) create mode 100644 src/types/block.ts delete mode 100644 src/types/blockHeader.ts diff --git a/src/client/v2/algod/models/types.ts b/src/client/v2/algod/models/types.ts index 55c611fb1..82c99f3e0 100644 --- a/src/client/v2/algod/models/types.ts +++ b/src/client/v2/algod/models/types.ts @@ -15,7 +15,7 @@ import { OptionalSchema, } from '../../../../encoding/schema/index.js'; import { base64ToBytes } from '../../../../encoding/binarydata.js'; -import { Block } from '../../../../types/blockHeader.js'; +import { Block } from '../../../../types/block.js'; import { SignedTransaction } from '../../../../signedTransaction.js'; import { Address } from '../../../../encoding/address.js'; import { UntypedValue } from '../../untypedmodel.js'; diff --git a/src/encoding/schema/index.ts b/src/encoding/schema/index.ts index 848b5429f..88c3173e3 100644 --- a/src/encoding/schema/index.ts +++ b/src/encoding/schema/index.ts @@ -10,6 +10,7 @@ export { NamedMapSchema, NamedMapEntry, allOmitEmpty, + combineMaps, Uint64MapSchema, } from './map.js'; export { OptionalSchema } from './optional.js'; diff --git a/src/encoding/schema/map.ts b/src/encoding/schema/map.ts index bdfa84bfe..ee1145987 100644 --- a/src/encoding/schema/map.ts +++ b/src/encoding/schema/map.ts @@ -195,6 +195,19 @@ export class NamedMapSchema extends Schema { } } +export function combineMaps(...maps: Array>): Map { + const combined = new Map(); + for (const map of maps) { + for (const [key, value] of map) { + if (combined.has(key)) { + throw new Error(`Duplicate key: ${key}`); + } + combined.set(key, value); + } + } + return combined; +} + /** * Schema for a map with a variable number of uint64 keys. */ diff --git a/src/main.ts b/src/main.ts index 46547c9d7..c13e053b1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -137,6 +137,7 @@ export { export { encodeUint64, decodeUint64 } from './encoding/uint64.js'; export { parseJSON, ParseJSONOptions, stringifyJSON } from './utils/utils.js'; export { default as generateAccount } from './account.js'; +export * from './types/block.js'; export * as modelsv2 from './client/v2/algod/models/types.js'; export * as indexerModels from './client/v2/indexer/models/types.js'; export { diff --git a/src/types/block.ts b/src/types/block.ts new file mode 100644 index 000000000..380d812de --- /dev/null +++ b/src/types/block.ts @@ -0,0 +1,798 @@ +import { Encodable, Schema } from '../encoding/encoding.js'; +import { + NamedMapSchema, + Uint64MapSchema, + ArraySchema, + StringSchema, + Uint64Schema, + AddressSchema, + ByteArraySchema, + FixedLengthByteArraySchema, + UntypedSchema, + allOmitEmpty, + combineMaps, +} from '../encoding/schema/index.js'; +import { Address } from '../encoding/address.js'; + +/** + * StateProofTrackingData tracks the status of state proofs. + */ +export class StateProofTrackingData implements Encodable { + public static readonly encodingSchema = new NamedMapSchema( + allOmitEmpty([ + { + key: 'v', // stateProofVotersCommitment + valueSchema: new ByteArraySchema(), + }, + { + key: 't', // stateProofOnlineTotalWeight + valueSchema: new Uint64Schema(), + }, + { + key: 'n', // stateProofNextRound + valueSchema: new Uint64Schema(), + }, + ]) + ); + + /** + * StateProofVotersCommitment is the root of a vector commitment containing the online accounts + * that will help sign a state proof. The VC root, and the state proof, happen on blocks that are + * a multiple of ConsensusParams.StateProofRounds. For blocks that are not a multiple of + * ConsensusParams.StateProofRounds, this value is zero. + */ + public stateProofVotersCommitment: Uint8Array; + + /** + * StateProofOnlineTotalWeight is the total number of microalgos held by the online accounts during + * the StateProof round (or zero, if the merkle root is zero - no commitment for StateProof voters). + * This is intended for computing the threshold of votes to expect from StateProofVotersCommitment. + */ + public stateProofOnlineTotalWeight: bigint; + + /** + * StateProofNextRound is the next round for which we will accept a StateProof transaction. + */ + public stateProofNextRound: bigint; + + public constructor(params: { + stateProofVotersCommitment: Uint8Array; + stateProofOnlineTotalWeight: bigint; + stateProofNextRound: bigint; + }) { + this.stateProofVotersCommitment = params.stateProofVotersCommitment; + this.stateProofOnlineTotalWeight = params.stateProofOnlineTotalWeight; + this.stateProofNextRound = params.stateProofNextRound; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return StateProofTrackingData.encodingSchema; + } + + public toEncodingData(): Map { + return new Map([ + ['v', this.stateProofVotersCommitment], + ['t', this.stateProofOnlineTotalWeight], + ['n', this.stateProofNextRound], + ]); + } + + public static fromEncodingData(data: unknown): StateProofTrackingData { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded StateProofTrackingData: ${data}`); + } + return new StateProofTrackingData({ + stateProofVotersCommitment: data.get('v'), + stateProofOnlineTotalWeight: data.get('t'), + stateProofNextRound: data.get('n'), + }); + } +} + +/** + * TxnCommitments represents the commitments computed from the transactions in the block. + * It contains multiple commitments based on different algorithms and hash functions, to support + * different use cases. + */ +export class TxnCommitments implements Encodable { + public static readonly encodingSchema = new NamedMapSchema( + allOmitEmpty([ + { + key: 'txn', // nativeSha512_256Commitment + valueSchema: new FixedLengthByteArraySchema(32), + }, + { + key: 'txn256', // sha256Commitment + valueSchema: new FixedLengthByteArraySchema(32), + }, + ]) + ); + + /** + * Root of transaction merkle tree using SHA512_256 hash function. This commitment is computed + * based on the PaysetCommit type specified in the block's consensus protocol. + */ + public nativeSha512_256Commitment: Uint8Array; + + /** + * Root of transaction vector commitment merkle tree using SHA256 hash function + */ + public sha256Commitment: Uint8Array; + + constructor(params: { + nativeSha512_256Commitment: Uint8Array; + sha256Commitment: Uint8Array; + }) { + this.nativeSha512_256Commitment = params.nativeSha512_256Commitment; + this.sha256Commitment = params.sha256Commitment; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return TxnCommitments.encodingSchema; + } + + public toEncodingData(): Map { + return new Map([ + ['txn', this.nativeSha512_256Commitment], + ['txn256', this.sha256Commitment], + ]); + } + + public static fromEncodingData(data: unknown): TxnCommitments { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded TxnCommitments: ${data}`); + } + return new TxnCommitments({ + nativeSha512_256Commitment: data.get('txn'), + sha256Commitment: data.get('txn256'), + }); + } +} + +/** + * RewardsState represents the global parameters controlling the rate at which accounts accrue rewards. + */ +export class RewardState implements Encodable { + public static readonly encodingSchema = new NamedMapSchema( + allOmitEmpty([ + { + key: 'fees', // feeSink + valueSchema: new AddressSchema(), + }, + { + key: 'rwd', // rewardsPool + valueSchema: new AddressSchema(), + }, + { + key: 'earn', // rewardsLevel + valueSchema: new Uint64Schema(), + }, + { + key: 'rate', // rewardsRate + valueSchema: new Uint64Schema(), + }, + { + key: 'frac', // rewardsResidue + valueSchema: new Uint64Schema(), + }, + { + key: 'rwcalr', // rewardsRecalculationRound + valueSchema: new Uint64Schema(), + }, + ]) + ); + + /** + * The FeeSink address. + */ + public feeSink: Address; + + /** + * The RewardsPool address. + */ + public rewardsPool: Address; + + /** + * RewardsLevel specifies how many rewards, in MicroAlgos, have been distributed to each + * config.Protocol.RewardUnit of MicroAlgos since genesis. + */ + public rewardsLevel: bigint; + + /** + * The number of new MicroAlgos added to the participation stake from rewards at the next round. + */ + public rewardsRate: bigint; + + /** + * The number of leftover MicroAlgos after the distribution of RewardsRate/rewardUnits MicroAlgos for + * every reward unit in the next round. + */ + public rewardsResidue: bigint; + + /** + * The round at which the RewardsRate will be recalculated. + */ + public rewardsRecalculationRound: bigint; + + constructor(params: { + feeSink: Address; + rewardsPool: Address; + rewardsLevel: bigint; + rewardsRate: bigint; + rewardsResidue: bigint; + rewardsRecalculationRound: bigint; + }) { + this.feeSink = params.feeSink; + this.rewardsPool = params.rewardsPool; + this.rewardsLevel = params.rewardsLevel; + this.rewardsRate = params.rewardsRate; + this.rewardsResidue = params.rewardsResidue; + this.rewardsRecalculationRound = params.rewardsRecalculationRound; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return RewardState.encodingSchema; + } + + public toEncodingData(): Map { + return new Map([ + ['fees', this.feeSink], + ['rwd', this.rewardsPool], + ['earn', this.rewardsLevel], + ['rate', this.rewardsRate], + ['frac', this.rewardsResidue], + ['rwcalr', this.rewardsRecalculationRound], + ]); + } + + public static fromEncodingData(data: unknown): RewardState { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded RewardState: ${data}`); + } + return new RewardState({ + feeSink: data.get('fees'), + rewardsPool: data.get('rwd'), + rewardsLevel: data.get('earn'), + rewardsRate: data.get('rate'), + rewardsResidue: data.get('frac'), + rewardsRecalculationRound: data.get('rwcalr'), + }); + } +} + +/** + * UpgradeState tracks the protocol upgrade state machine. It is, strictly speaking, computable from + * the history of all UpgradeVotes but we keep it in the block for explicitness and convenience + * (instead of materializing it separately, like balances). + */ +export class UpgradeState implements Encodable { + public static readonly encodingSchema = new NamedMapSchema( + allOmitEmpty([ + { + key: 'proto', // currentProtocol + valueSchema: new StringSchema(), + }, + { + key: 'nextproto', // nextProtocol + valueSchema: new StringSchema(), + }, + { + key: 'nextyes', // nextProtocolApprovals + valueSchema: new Uint64Schema(), + }, + { + key: 'nextbefore', // nextProtocolVoteBefore + valueSchema: new Uint64Schema(), + }, + { + key: 'nextswitch', // nextProtocolSwitchOn + valueSchema: new Uint64Schema(), + }, + ]) + ); + + public currentProtocol: string; + + public nextProtocol: string; + + public nextProtocolApprovals: bigint; + + /** + * NextProtocolVoteBefore specify the last voting round for the next protocol proposal. If there + * is no voting for an upgrade taking place, this would be zero. + */ + public nextProtocolVoteBefore: bigint; + + /** + * NextProtocolSwitchOn specify the round number at which the next protocol would be adopted. If + * there is no upgrade taking place, nor a wait for the next protocol, this would be zero. + */ + public nextProtocolSwitchOn: bigint; + + public constructor(params: { + currentProtocol: string; + nextProtocol: string; + nextProtocolApprovals: bigint; + nextProtocolVoteBefore: bigint; + nextProtocolSwitchOn: bigint; + }) { + this.currentProtocol = params.currentProtocol; + this.nextProtocol = params.nextProtocol; + this.nextProtocolApprovals = params.nextProtocolApprovals; + this.nextProtocolVoteBefore = params.nextProtocolVoteBefore; + this.nextProtocolSwitchOn = params.nextProtocolSwitchOn; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return UpgradeState.encodingSchema; + } + + public toEncodingData(): Map { + return new Map([ + ['proto', this.currentProtocol], + ['nextproto', this.nextProtocol], + ['nextyes', this.nextProtocolApprovals], + ['nextbefore', this.nextProtocolVoteBefore], + ['nextswitch', this.nextProtocolSwitchOn], + ]); + } + + public static fromEncodingData(data: unknown): UpgradeState { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded UpgradeState: ${data}`); + } + return new UpgradeState({ + currentProtocol: data.get('proto'), + nextProtocol: data.get('nextproto'), + nextProtocolApprovals: data.get('nextyes'), + nextProtocolVoteBefore: data.get('nextbefore'), + nextProtocolSwitchOn: data.get('nextswitch'), + }); + } +} + +/** + * UpgradeVote represents the vote of the block proposer with respect to protocol upgrades. + */ +export class UpgradeVote implements Encodable { + public static readonly encodingSchema = new NamedMapSchema( + allOmitEmpty([ + { + key: 'upgradeprop', // upgradePropose + valueSchema: new StringSchema(), + }, + { + key: 'upgradedelay', // upgradeDelay + valueSchema: new Uint64Schema(), + }, + { + key: 'upgradeyes', // upgradeApprove + valueSchema: new Uint64Schema(), + }, + ]) + ); + + /** + * UpgradePropose indicates a proposed upgrade + */ + public upgradePropose: string; + + /** + * UpgradeDelay indicates the time between acceptance and execution + */ + public upgradeDelay: bigint; + + /** + * UpgradeApprove indicates a yes vote for the current proposal + */ + public upgradeApprove: boolean; + + public constructor(params: { + upgradePropose: string; + upgradeDelay: bigint; + upgradeApprove: boolean; + }) { + this.upgradePropose = params.upgradePropose; + this.upgradeDelay = params.upgradeDelay; + this.upgradeApprove = params.upgradeApprove; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return UpgradeVote.encodingSchema; + } + + public toEncodingData(): Map { + return new Map([ + ['upgradeprop', this.upgradePropose], + ['upgradedelay', this.upgradeDelay], + ['upgradeyes', this.upgradeApprove], + ]); + } + + public static fromEncodingData(data: unknown): UpgradeVote { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded UpgradeVote: ${data}`); + } + return new UpgradeVote({ + upgradePropose: data.get('upgradeprop'), + upgradeDelay: data.get('upgradedelay'), + upgradeApprove: data.get('upgradeyes'), + }); + } +} + +/** + * ParticipationUpdates represents participation account data that needs to be checked/acted on by + * the network + */ +export class ParticipationUpdates implements Encodable { + public static readonly encodingSchema = new NamedMapSchema( + allOmitEmpty([ + { + key: 'partupdrmv', // expiredParticipationAccounts + valueSchema: new ArraySchema(new AddressSchema()), + }, + { + key: 'partupdabs', // absentParticipationAccounts + valueSchema: new ArraySchema(new AddressSchema()), + }, + ]) + ); + + /** + * ExpiredParticipationAccounts contains a list of online accounts that needs to be converted to + * offline since their participation key expired. + */ + public expiredParticipationAccounts: Address[]; + + /** + * AbsentParticipationAccounts contains a list of online accounts that needs to be converted to + * offline since they are not proposing. + */ + public absentParticipationAccounts: Address[]; + + public constructor(params: { + expiredParticipationAccounts: Address[]; + absentParticipationAccounts: Address[]; + }) { + this.expiredParticipationAccounts = params.expiredParticipationAccounts; + this.absentParticipationAccounts = params.absentParticipationAccounts; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return ParticipationUpdates.encodingSchema; + } + + public toEncodingData(): Map { + return new Map([ + ['partupdrmv', this.expiredParticipationAccounts], + ['partupdabs', this.absentParticipationAccounts], + ]); + } + + public static fromEncodingData(data: unknown): ParticipationUpdates { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded ParticipationUpdates: ${data}`); + } + return new ParticipationUpdates({ + expiredParticipationAccounts: data.get('partupdrmv'), + absentParticipationAccounts: data.get('partupdabs'), + }); + } +} + +/** + * Represents the metadata and state of a block. + * + * For more information, refer to: https://github.com/algorand/go-algorand/blob/master/data/bookkeeping/block.go + */ +export class BlockHeader implements Encodable { + public static readonly encodingSchema = new NamedMapSchema( + allOmitEmpty([ + { + key: 'rnd', // round + valueSchema: new Uint64Schema(), + }, + { + key: 'prev', // branch + valueSchema: new ByteArraySchema(), + }, + { + key: 'seed', // seed + valueSchema: new ByteArraySchema(), + }, + { + key: '', + valueSchema: TxnCommitments.encodingSchema, + embedded: true, + }, + { + key: 'ts', // timestamp + valueSchema: new Uint64Schema(), + }, + { + key: 'gen', // genesisID + valueSchema: new StringSchema(), + }, + { + key: 'gh', // genesisHash + valueSchema: new FixedLengthByteArraySchema(32), + }, + { + key: 'prp', // proposer + valueSchema: new AddressSchema(), + }, + { + key: 'fc', // feesCollected + valueSchema: new Uint64Schema(), + }, + { + key: 'bi', // bonus + valueSchema: new Uint64Schema(), + }, + { + key: 'pp', // proposerPayout + valueSchema: new Uint64Schema(), + }, + { + key: '', + valueSchema: RewardState.encodingSchema, + embedded: true, + }, + { + key: '', + valueSchema: UpgradeState.encodingSchema, + embedded: true, + }, + { + key: '', + valueSchema: UpgradeVote.encodingSchema, + embedded: true, + }, + { + key: 'tc', // txnCounter + valueSchema: new Uint64Schema(), + }, + { + key: 'spt', // stateproofTracking + valueSchema: new Uint64MapSchema(StateProofTrackingData.encodingSchema), + }, + { + key: '', + valueSchema: ParticipationUpdates.encodingSchema, + embedded: true, + }, + ]) + ); + + /** + * Round number + */ + public round: bigint; + + /** + * Previous block hash + */ + public branch: Uint8Array; + + /** + * Sortition seed + */ + public seed: Uint8Array; + + public txnCommitments: TxnCommitments; + + /** + * Timestamp in seconds since epoch + */ + public timestamp: bigint; + + /** + * Genesis ID to which this block belongs. + */ + public genesisID: string; + + /** + * Genesis hash to which this block belongs. + */ + public genesisHash: Uint8Array; + + /** + * Proposer is the proposer of this block. Like the Seed, agreement adds this after the block is + * assembled by the transaction pool, so that the same block can be prepared for multiple + * participating accounts in the same node. Populated if proto.Payouts.Enabled + */ + public proposer: Address; + + /** + * FeesCollected is the sum of all fees paid by transactions in this block. Populated if + * proto.EnableMining. + */ + public feesCollected: bigint; + + /** + * Bonus is the bonus incentive to be paid for proposing this block. It begins as a consensus + * parameter value, and decays periodically. + */ + public bonus: bigint; + + /** + * ProposerPayout is the amount that should be moved from the FeeSink to the Proposer at the start + * of the next block. It is basically the bonus + the payouts percent of FeesCollected, but may + * be zero'd by proposer ineligibility. + */ + public proposerPayout: bigint; + + public rewardState: RewardState; + + public upgradeState: UpgradeState; + + public upgradeVote: UpgradeVote; + + /** + * TxnCounter is the number of the next transaction that will be committed after this block. Genesis + * blocks can start at either 0 or 1000, depending on a consensus parameter (AppForbidLowResources). + */ + public txnCounter: bigint; + + /** + * StateProofTracking tracks the status of the state proofs, potentially for multiple types of + * ASPs (Algorand's State Proofs). + */ + public stateproofTracking: Map; + + public participationUpdates: ParticipationUpdates; + + public constructor(params: { + round: bigint; + branch: Uint8Array; + seed: Uint8Array; + txnCommitments: TxnCommitments; + timestamp: bigint; + genesisID: string; + genesisHash: Uint8Array; + proposer: Address; + feesCollected: bigint; + bonus: bigint; + proposerPayout: bigint; + rewardState: RewardState; + upgradeState: UpgradeState; + upgradeVote: UpgradeVote; + txnCounter: bigint; + stateproofTracking: Map; + participationUpdates: ParticipationUpdates; + }) { + this.round = params.round; + this.branch = params.branch; + this.seed = params.seed; + this.txnCommitments = params.txnCommitments; + this.timestamp = params.timestamp; + this.genesisID = params.genesisID; + this.genesisHash = params.genesisHash; + this.proposer = params.proposer; + this.feesCollected = params.feesCollected; + this.bonus = params.bonus; + this.proposerPayout = params.proposerPayout; + this.rewardState = params.rewardState; + this.upgradeState = params.upgradeState; + this.upgradeVote = params.upgradeVote; + this.txnCounter = params.txnCounter; + this.stateproofTracking = params.stateproofTracking; + this.participationUpdates = params.participationUpdates; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return BlockHeader.encodingSchema; + } + + public toEncodingData(): Map { + const data = new Map([ + ['rnd', this.round], + ['prev', this.branch], + ['seed', this.seed], + ['ts', this.timestamp], + ['gen', this.genesisID], + ['gh', this.genesisHash], + ['prp', this.proposer], + ['fc', this.feesCollected], + ['bi', this.bonus], + ['pp', this.proposerPayout], + ['tc', this.txnCounter], + ['spt', this.stateproofTracking], + ]); + return combineMaps( + data, + this.txnCommitments.toEncodingData(), + this.rewardState.toEncodingData(), + this.upgradeState.toEncodingData(), + this.upgradeVote.toEncodingData(), + this.participationUpdates.toEncodingData() + ); + } + + public static fromEncodingData(data: unknown): BlockHeader { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded BlockHeader: ${data}`); + } + return new BlockHeader({ + round: data.get('rnd'), + branch: data.get('prev'), + seed: data.get('seed'), + txnCommitments: TxnCommitments.fromEncodingData(data), + timestamp: data.get('ts'), + genesisHash: data.get('gen'), + genesisID: data.get('gh'), + proposer: data.get('prp'), + feesCollected: data.get('fc'), + bonus: data.get('bi'), + proposerPayout: data.get('pp'), + rewardState: RewardState.fromEncodingData(data), + upgradeState: UpgradeState.fromEncodingData(data), + upgradeVote: UpgradeVote.fromEncodingData(data), + txnCounter: data.get('tc'), + stateproofTracking: new Map( + Array.from( + (data.get('spt') as Map).entries() + ).map(([k, v]) => [Number(k), v]) + ), + participationUpdates: ParticipationUpdates.fromEncodingData(data), + }); + } +} + +/** + * A Block contains the Payset and metadata corresponding to a given Round. + */ +export class Block implements Encodable { + public static readonly encodingSchema = new NamedMapSchema( + allOmitEmpty([ + { + key: '', + valueSchema: BlockHeader.encodingSchema, + embedded: true, + }, + { + key: 'txns', // payset + valueSchema: new UntypedSchema(), // TODO: fix + }, + ]) + ); + + public header: BlockHeader; + + public payset: unknown; // TODO: fix + + public constructor(params: { header: BlockHeader; payset: unknown }) { + this.header = params.header; + this.payset = params.payset; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return Block.encodingSchema; + } + + public toEncodingData(): Map { + const data = new Map([ + ['txns', this.payset], // TODO: fix + ]); + return combineMaps(data, this.header.toEncodingData()); + } + + public static fromEncodingData(data: unknown): Block { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded BlockHeader: ${data}`); + } + return new Block({ + header: BlockHeader.fromEncodingData(data), + payset: data.get('txn'), // TODO: fix + }); + } +} diff --git a/src/types/blockHeader.ts b/src/types/blockHeader.ts deleted file mode 100644 index d1440271a..000000000 --- a/src/types/blockHeader.ts +++ /dev/null @@ -1,538 +0,0 @@ -import { Encodable, Schema } from '../encoding/encoding.js'; -import { - NamedMapSchema, - Uint64MapSchema, - ArraySchema, - StringSchema, - Uint64Schema, - AddressSchema, - ByteArraySchema, - FixedLengthByteArraySchema, - UntypedSchema, - allOmitEmpty, -} from '../encoding/schema/index.js'; -import { Address } from '../encoding/address.js'; - -/** - * StateProofTrackingData tracks the status of state proofs. - */ -export class StateProofTrackingData implements Encodable { - public static readonly encodingSchema = new NamedMapSchema( - allOmitEmpty([ - { - key: 'v', // stateProofVotersCommitment - valueSchema: new ByteArraySchema(), - }, - { - key: 't', // stateProofOnlineTotalWeight - valueSchema: new Uint64Schema(), - }, - { - key: 'n', // stateProofNextRound - valueSchema: new Uint64Schema(), - }, - ]) - ); - - /** - * StateProofVotersCommitment is the root of a vector commitment containing the online accounts - * that will help sign a state proof. The VC root, and the state proof, happen on blocks that are - * a multiple of ConsensusParams.StateProofRounds. For blocks that are not a multiple of - * ConsensusParams.StateProofRounds, this value is zero. - */ - public stateProofVotersCommitment: Uint8Array; - - /** - * StateProofOnlineTotalWeight is the total number of microalgos held by the online accounts during - * the StateProof round (or zero, if the merkle root is zero - no commitment for StateProof voters). - * This is intended for computing the threshold of votes to expect from StateProofVotersCommitment. - */ - public stateProofOnlineTotalWeight: bigint; - - /** - * StateProofNextRound is the next round for which we will accept a StateProof transaction. - */ - public stateProofNextRound: bigint; - - public constructor(params: { - stateProofVotersCommitment: Uint8Array; - stateProofOnlineTotalWeight: bigint; - stateProofNextRound: bigint; - }) { - this.stateProofVotersCommitment = params.stateProofVotersCommitment; - this.stateProofOnlineTotalWeight = params.stateProofOnlineTotalWeight; - this.stateProofNextRound = params.stateProofNextRound; - } - - // eslint-disable-next-line class-methods-use-this - public getEncodingSchema(): Schema { - return StateProofTrackingData.encodingSchema; - } - - public toEncodingData(): unknown { - return new Map([ - ['v', this.stateProofVotersCommitment], - ['t', this.stateProofOnlineTotalWeight], - ['n', this.stateProofNextRound], - ]); - } - - public static fromEncodingData(data: unknown): StateProofTrackingData { - if (!(data instanceof Map)) { - throw new Error(`Invalid decoded StateProofTrackingData: ${data}`); - } - return new StateProofTrackingData({ - stateProofVotersCommitment: data.get('v'), - stateProofOnlineTotalWeight: data.get('t'), - stateProofNextRound: data.get('n'), - }); - } -} - -/** - * TxnCommitments represents the commitments computed from the transactions in the block. - * It contains multiple commitments based on different algorithms and hash functions, to support - * different use cases. - */ -export interface TxnCommitments { - /** - * Root of transaction merkle tree using SHA512_256 hash function. This commitment is computed - * based on the PaysetCommit type specified in the block's consensus protocol. - */ - nativeSha512_256Commitment: Uint8Array; - - /** - * Root of transaction vector commitment merkle tree using SHA256 hash function - */ - sha256Commitment: Uint8Array; -} - -/** - * RewardsState represents the global parameters controlling the rate at which accounts accrue rewards. - */ -export interface RewardState { - /** - * The FeeSink address. - */ - feeSink: Address; - - /** - * The RewardsPool address. - */ - rewardsPool: Address; - - /** - * RewardsLevel specifies how many rewards, in MicroAlgos, have been distributed to each - * config.Protocol.RewardUnit of MicroAlgos since genesis. - */ - rewaredsLevel: bigint; - - /** - * The number of new MicroAlgos added to the participation stake from rewards at the next round. - */ - rewardsRate: bigint; - - /** - * The number of leftover MicroAlgos after the distribution of RewardsRate/rewardUnits MicroAlgos for - * every reward unit in the next round. - */ - rewardsResidue: bigint; - - /** - * The round at which the RewardsRate will be recalculated. - */ - rewardsRecalculationRound: bigint; -} - -/** - * UpgradeState tracks the protocol upgrade state machine. It is, strictly speaking, computable from - * the history of all UpgradeVotes but we keep it in the block for explicitness and convenience - * (instead of materializing it separately, like balances). - */ -export interface UpgradeState { - currentProtocol: string; - - nextProtocol: string; - - nextProtocolApprovals: bigint; - - /** - * NextProtocolVoteBefore specify the last voting round for the next protocol proposal. If there - * is no voting for an upgrade taking place, this would be zero. - */ - nextProtocolVoteBefore: bigint; - - /** - * NextProtocolSwitchOn specify the round number at which the next protocol would be adopted. If - * there is no upgrade taking place, nor a wait for the next protocol, this would be zero. - */ - nextProtocolSwitchOn: bigint; -} - -/** - * UpgradeVote represents the vote of the block proposer with respect to protocol upgrades. - */ -export interface UpgradeVote { - /** - * UpgradePropose indicates a proposed upgrade - */ - upgradePropose: string; - - /** - * UpgradeDelay indicates the time between acceptance and execution - */ - upgradeDelay: bigint; - - /** - * UpgradeApprove indicates a yes vote for the current proposal - */ - upgradeApprove: boolean; -} - -/** - * ParticipationUpdates represents participation account data that needs to be checked/acted on by - * the network - */ -export interface ParticipationUpdates { - /** - * ExpiredParticipationAccounts contains a list of online accounts that needs to be converted to - * offline since their participation key expired. - */ - expiredParticipationAccounts: Address[]; - - /** - * AbsentParticipationAccounts contains a list of online accounts that needs to be converted to - * offline since they are not proposing. - */ - absentParticipationAccounts: Address[]; -} - -/** - * Represents the metadata and state of a block. - * - * For more information, refer to: https://github.com/algorand/go-algorand/blob/master/data/bookkeeping/block.go - */ -export interface BlockHeader { - /** - * Round number - */ - round: bigint; - - /** - * Previous block hash - */ - branch: Uint8Array; - - /** - * Sortition seed - */ - seed: Uint8Array; - - txnCommitments: TxnCommitments; - - /** - * Timestamp in seconds since epoch - */ - timestamp: bigint; - - /** - * Genesis ID to which this block belongs. - */ - genesisID: string; - - /** - * Genesis hash to which this block belongs. - */ - genesisHash: Uint8Array; - - /** - * Proposer is the proposer of this block. Like the Seed, agreement adds this after the block is - * assembled by the transaction pool, so that the same block can be prepared for multiple - * participating accounts in the same node. Populated if proto.Payouts.Enabled - */ - proposer: Address; - - /** - * FeesCollected is the sum of all fees paid by transactions in this block. Populated if - * proto.EnableMining. - */ - feesCollected: bigint; - - /** - * Bonus is the bonus incentive to be paid for proposing this block. It begins as a consensus - * parameter value, and decays periodically. - */ - bonus: bigint; - - /** - * ProposerPayout is the amount that should be moved from the FeeSink to the Proposer at the start - * of the next block. It is basically the bonus + the payouts percent of FeesCollected, but may - * be zero'd by proposer ineligibility. - */ - proposerPayout: bigint; - - rewardState: RewardState; - - upgradeState: UpgradeState; - - upgradeVote: UpgradeVote; - - /** - * TxnCounter is the number of the next transaction that will be committed after this block. Genesis - * blocks can start at either 0 or 1000, depending on a consensus parameter (AppForbidLowResources). - */ - txnCounter: bigint; - - /** - * StateProofTracking tracks the status of the state proofs, potentially for multiple types of - * ASPs (Algorand's State Proofs). - */ - stateproofTracking: Map; - - participationUpdates: ParticipationUpdates; -} - -/** - * A Block contains the Payset and metadata corresponding to a given Round. - */ -export class Block implements Encodable { - public static readonly encodingSchema = new NamedMapSchema( - allOmitEmpty([ - { - key: 'rnd', // header.round - valueSchema: new Uint64Schema(), - }, - { - key: 'prev', // header.branch - valueSchema: new ByteArraySchema(), - }, - { - key: 'seed', // header.seed - valueSchema: new ByteArraySchema(), - }, - { - key: 'txn', // header.txnCommitments.nativeSha512_256Commitment - valueSchema: new FixedLengthByteArraySchema(32), - }, - { - key: 'txn256', // header.txnCommitments.sha256Commitment - valueSchema: new FixedLengthByteArraySchema(32), - }, - { - key: 'ts', // header.timestamp - valueSchema: new Uint64Schema(), - }, - { - key: 'gen', // header.genesisID - valueSchema: new StringSchema(), - }, - { - key: 'gh', // header.genesisHash - valueSchema: new FixedLengthByteArraySchema(32), - }, - { - key: 'prp', // header.proposer - valueSchema: new AddressSchema(), - }, - { - key: 'fc', // header.feesCollected - valueSchema: new Uint64Schema(), - }, - { - key: 'bi', // header.bonus - valueSchema: new Uint64Schema(), - }, - { - key: 'pp', // header.proposerPayout - valueSchema: new Uint64Schema(), - }, - { - key: 'fees', // header.rewardState.feeSink - valueSchema: new AddressSchema(), - }, - { - key: 'rwd', // header.rewardState.rewardsPool - valueSchema: new AddressSchema(), - }, - { - key: 'earn', // header.rewardState.rewaredsLevel - valueSchema: new Uint64Schema(), - }, - { - key: 'rate', // header.rewardState.rewardsRate - valueSchema: new Uint64Schema(), - }, - { - key: 'frac', // header.rewardState.rewardsResidue - valueSchema: new Uint64Schema(), - }, - { - key: 'rwcalr', // header.rewardState.rewardsRecalculationRound - valueSchema: new Uint64Schema(), - }, - { - key: 'proto', // header.upgradeState.currentProtocol - valueSchema: new StringSchema(), - }, - { - key: 'nextproto', // header.upgradeState.nextProtocol - valueSchema: new StringSchema(), - }, - { - key: 'nextyes', // header.upgradeState.nextProtocolApprovals - valueSchema: new Uint64Schema(), - }, - { - key: 'nextbefore', // header.upgradeState.nextProtocolVoteBefore - valueSchema: new Uint64Schema(), - }, - { - key: 'nextswitch', // header.upgradeState.nextProtocolSwitchOn - valueSchema: new Uint64Schema(), - }, - { - key: 'upgradeprop', // header.upgradeVote.upgradePropose - valueSchema: new StringSchema(), - }, - { - key: 'upgradedelay', // header.upgradeVote.upgradeDelay - valueSchema: new Uint64Schema(), - }, - { - key: 'upgradeyes', // header.upgradeVote.upgradeApprove - valueSchema: new Uint64Schema(), - }, - { - key: 'tc', // header.txnCounter - valueSchema: new Uint64Schema(), - }, - { - key: 'spt', // header.stateproofTracking - valueSchema: new Uint64MapSchema(StateProofTrackingData.encodingSchema), - }, - { - key: 'partupdrmv', // header.participationUpdates.expiredParticipationAccounts - valueSchema: new ArraySchema(new AddressSchema()), - }, - { - key: 'partupdabs', // header.participationUpdates.absentParticipationAccounts - valueSchema: new ArraySchema(new AddressSchema()), - }, - { - key: 'txns', // payset - valueSchema: new UntypedSchema(), // TODO: fix - }, - ]) - ); - - public header: BlockHeader; - - public payset: unknown; // TODO: fix - - public constructor(params: { header: BlockHeader; payset: unknown }) { - this.header = params.header; - this.payset = params.payset; - } - - // eslint-disable-next-line class-methods-use-this - public getEncodingSchema(): Schema { - return Block.encodingSchema; - } - - public toEncodingData(): unknown { - return new Map([ - ['rnd', this.header.round], - ['prev', this.header.branch], - ['seed', this.header.seed], - ['txn', this.header.txnCommitments.nativeSha512_256Commitment], - ['txn256', this.header.txnCommitments.sha256Commitment], - ['ts', this.header.timestamp], - ['gen', this.header.genesisID], - ['gh', this.header.genesisHash], - ['prp', this.header.proposer], - ['fc', this.header.feesCollected], - ['bi', this.header.bonus], - ['pp', this.header.proposerPayout], - ['fees', this.header.rewardState.feeSink], - ['rwd', this.header.rewardState.rewardsPool], - ['earn', this.header.rewardState.rewaredsLevel], - ['rate', this.header.rewardState.rewardsRate], - ['frac', this.header.rewardState.rewardsResidue], - ['rwcalr', this.header.rewardState.rewardsRecalculationRound], - ['proto', this.header.upgradeState.currentProtocol], - ['nextproto', this.header.upgradeState.nextProtocol], - ['nextyes', this.header.upgradeState.nextProtocolApprovals], - ['nextbefore', this.header.upgradeState.nextProtocolVoteBefore], - ['nextswitch', this.header.upgradeState.nextProtocolSwitchOn], - ['upgradeprop', this.header.upgradeVote.upgradePropose], - ['upgradedelay', this.header.upgradeVote.upgradeDelay], - ['upgradeyes', this.header.upgradeVote.upgradeApprove], - ['tc', this.header.txnCounter], - ['spt', this.header.stateproofTracking], - [ - 'partupdrmv', - this.header.participationUpdates.expiredParticipationAccounts, - ], - [ - 'partupdabs', - this.header.participationUpdates.absentParticipationAccounts, - ], - ['txns', this.payset], // TODO: fix - ]); - } - - public static fromEncodingData(data: unknown): Block { - if (!(data instanceof Map)) { - throw new Error(`Invalid decoded BlockHeader: ${data}`); - } - return new Block({ - header: { - round: data.get('rnd'), - branch: data.get('prev'), - seed: data.get('seed'), - txnCommitments: { - nativeSha512_256Commitment: data.get('txn'), - sha256Commitment: data.get('txn256'), - }, - timestamp: data.get('ts'), - genesisHash: data.get('gen'), - genesisID: data.get('gh'), - proposer: data.get('prp'), - feesCollected: data.get('fc'), - bonus: data.get('bi'), - proposerPayout: data.get('pp'), - rewardState: { - feeSink: data.get('fees'), - rewardsPool: data.get('rwd'), - rewaredsLevel: data.get('earn'), - rewardsRate: data.get('rate'), - rewardsResidue: data.get('frac'), - rewardsRecalculationRound: data.get('rwcalr'), - }, - upgradeState: { - currentProtocol: data.get('proto'), - nextProtocol: data.get('nextproto'), - nextProtocolApprovals: data.get('nextyes'), - nextProtocolVoteBefore: data.get('nextbefore'), - nextProtocolSwitchOn: data.get('nextswitch'), - }, - upgradeVote: { - upgradePropose: data.get('upgradeprop'), - upgradeDelay: data.get('upgradedelay'), - upgradeApprove: data.get('upgradeyes'), - }, - txnCounter: data.get('tc'), - stateproofTracking: new Map( - Array.from( - (data.get('spt') as Map).entries() - ).map(([k, v]) => [Number(k), v]) - ), - participationUpdates: { - expiredParticipationAccounts: data.get('partupdrmv'), - absentParticipationAccounts: data.get('partupdabs'), - }, - }, - payset: data.get('txn'), // TODO: fix - }); - } -} From cba3c2aaa10650cd1d675fb220a4c76fb72c7ebc Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Thu, 13 Jun 2024 13:37:10 -0400 Subject: [PATCH 5/9] Implement block payset --- src/encoding/schema/index.ts | 2 + src/encoding/schema/map.ts | 115 ++++++++- src/types/block.ts | 471 ++++++++++++++++++++++++++++++++++- 3 files changed, 574 insertions(+), 14 deletions(-) diff --git a/src/encoding/schema/index.ts b/src/encoding/schema/index.ts index 88c3173e3..01077572c 100644 --- a/src/encoding/schema/index.ts +++ b/src/encoding/schema/index.ts @@ -11,7 +11,9 @@ export { NamedMapEntry, allOmitEmpty, combineMaps, + convertMap, Uint64MapSchema, + StringMapSchema, } from './map.js'; export { OptionalSchema } from './optional.js'; diff --git a/src/encoding/schema/map.ts b/src/encoding/schema/map.ts index ee1145987..9f1e26af7 100644 --- a/src/encoding/schema/map.ts +++ b/src/encoding/schema/map.ts @@ -208,6 +208,18 @@ export function combineMaps(...maps: Array>): Map { return combined; } +export function convertMap( + map: Map, + func: (k: K1, v: V1) => [K2, V2] +): Map { + const mapped = new Map(); + for (const [key, value] of map) { + const [newKey, newValue] = func(key, value); + mapped.set(newKey, newValue); + } + return mapped; +} + /** * Schema for a map with a variable number of uint64 keys. */ @@ -236,7 +248,7 @@ export class Uint64MapSchema extends Schema { if (prepared.has(bigintKey)) { throw new Error(`Duplicate key: ${bigintKey}`); } - prepared.set(key, this.valueSchema.prepareMsgpack(value)); + prepared.set(bigintKey, this.valueSchema.prepareMsgpack(value)); } return prepared; } @@ -270,7 +282,7 @@ export class Uint64MapSchema extends Schema { if (prepared.has(bigintKey)) { throw new Error(`Duplicate key: ${bigintKey}`); } - prepared.set(key, this.valueSchema.prepareJSON(value)); + prepared.set(bigintKey, this.valueSchema.prepareJSON(value)); } // Convert map to object const obj: { [key: string]: JSONEncodingData } = {}; @@ -286,7 +298,7 @@ export class Uint64MapSchema extends Schema { typeof encoded !== 'object' || Array.isArray(encoded) ) { - throw new Error('NamedMapSchema data must be an object'); + throw new Error('Uint64MapSchema data must be an object'); } const map = new Map(); for (const [key, value] of Object.entries(encoded)) { @@ -299,3 +311,100 @@ export class Uint64MapSchema extends Schema { return map; } } + +/** + * Schema for a map with a variable number of uint64 keys. + */ +export class StringMapSchema extends Schema { + constructor(public readonly valueSchema: Schema) { + super(); + } + + public defaultValue(): Map { + return new Map(); + } + + public isDefaultValue(data: unknown): boolean { + return data instanceof Map && data.size === 0; + } + + public prepareMsgpack(data: unknown): MsgpackEncodingData { + if (!(data instanceof Map)) { + throw new Error( + `StringMapSchema data must be a Map. Got (${typeof data}) ${data}` + ); + } + const prepared = new Map(); + for (const [key, value] of data) { + if (typeof key !== 'string') { + throw new Error(`Invalid key: ${key}`); + } + if (prepared.has(key)) { + throw new Error(`Duplicate key: ${key}`); + } + prepared.set(key, this.valueSchema.prepareMsgpack(value)); + } + return prepared; + } + + public fromPreparedMsgpack( + encoded: MsgpackEncodingData + ): Map { + if (!(encoded instanceof Map)) { + throw new Error('StringMapSchema data must be a Map'); + } + const map = new Map(); + for (const [key, value] of encoded) { + if (typeof key !== 'string') { + throw new Error(`Invalid key: ${key}`); + } + if (map.has(key)) { + throw new Error(`Duplicate key: ${key}`); + } + map.set(key, this.valueSchema.fromPreparedMsgpack(value)); + } + return map; + } + + public prepareJSON(data: unknown): JSONEncodingData { + if (!(data instanceof Map)) { + throw new Error( + `StringMapSchema data must be a Map. Got (${typeof data}) ${data}` + ); + } + const prepared = new Map(); + for (const [key, value] of data) { + if (typeof key !== 'string') { + throw new Error(`Invalid key: ${key}`); + } + if (prepared.has(key)) { + throw new Error(`Duplicate key: ${key}`); + } + prepared.set(key, this.valueSchema.prepareJSON(value)); + } + // Convert map to object + const obj: { [key: string]: JSONEncodingData } = {}; + for (const [key, value] of prepared) { + obj[key] = value; + } + return obj; + } + + public fromPreparedJSON(encoded: JSONEncodingData): Map { + if ( + encoded == null || + typeof encoded !== 'object' || + Array.isArray(encoded) + ) { + throw new Error('StringMapSchema data must be an object'); + } + const map = new Map(); + for (const [key, value] of Object.entries(encoded)) { + if (map.has(key)) { + throw new Error(`Duplicate key: ${key}`); + } + map.set(key, this.valueSchema.fromPreparedJSON(value)); + } + return map; + } +} diff --git a/src/types/block.ts b/src/types/block.ts index 380d812de..65fe3807c 100644 --- a/src/types/block.ts +++ b/src/types/block.ts @@ -2,17 +2,20 @@ import { Encodable, Schema } from '../encoding/encoding.js'; import { NamedMapSchema, Uint64MapSchema, + StringMapSchema, ArraySchema, StringSchema, + BooleanSchema, Uint64Schema, AddressSchema, ByteArraySchema, FixedLengthByteArraySchema, - UntypedSchema, allOmitEmpty, combineMaps, + convertMap, } from '../encoding/schema/index.js'; import { Address } from '../encoding/address.js'; +import { SignedTransaction } from '../signedTransaction.js'; /** * StateProofTrackingData tracks the status of state proofs. @@ -705,7 +708,13 @@ export class BlockHeader implements Encodable { ['bi', this.bonus], ['pp', this.proposerPayout], ['tc', this.txnCounter], - ['spt', this.stateproofTracking], + [ + 'spt', + convertMap(this.stateproofTracking, (key, value) => [ + key, + value.toEncodingData(), + ]), + ], ]); return combineMaps( data, @@ -737,16 +746,453 @@ export class BlockHeader implements Encodable { upgradeState: UpgradeState.fromEncodingData(data), upgradeVote: UpgradeVote.fromEncodingData(data), txnCounter: data.get('tc'), - stateproofTracking: new Map( - Array.from( - (data.get('spt') as Map).entries() - ).map(([k, v]) => [Number(k), v]) + stateproofTracking: convertMap( + data.get('spt') as Map, + (key, value) => [ + Number(key), + StateProofTrackingData.fromEncodingData(value), + ] ), participationUpdates: ParticipationUpdates.fromEncodingData(data), }); } } +export class ValueDelta implements Encodable { + public static readonly encodingSchema = new NamedMapSchema( + allOmitEmpty([ + { + key: 'at', // action + valueSchema: new Uint64Schema(), + }, + { + key: 'bs', // bytes + valueSchema: new StringSchema(), + }, + { + key: 'ui', // uint + valueSchema: new Uint64Schema(), + }, + ]) + ); + + public action: number; + public bytes: string; + public uint: bigint; + + public constructor(params: { action: number; bytes: string; uint: bigint }) { + this.action = params.action; + this.bytes = params.bytes; + this.uint = params.uint; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return ValueDelta.encodingSchema; + } + + public toEncodingData(): Map { + return new Map([ + ['at', this.action], + ['bs', this.bytes], + ['ui', this.uint], + ]); + } + + public static fromEncodingData(data: unknown): ValueDelta { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded ValueDelta: ${data}`); + } + return new ValueDelta({ + action: Number(data.get('at')), + bytes: data.get('bs'), + uint: data.get('ui'), + }); + } +} + +export class EvalDelta implements Encodable { + private static encodingSchemaValue: Schema | undefined; + + public static get encodingSchema(): Schema { + // This is declared like this in order to break the circular dependency of + // SignedTxnWithAD -> ApplyData -> EvalDelta -> SignedTxnWithAD + if (!this.encodingSchemaValue) { + this.encodingSchemaValue = new NamedMapSchema([]); + (this.encodingSchemaValue as NamedMapSchema).pushEntries( + ...allOmitEmpty([ + { + key: 'gd', // globalDelta + valueSchema: new StringMapSchema(ValueDelta.encodingSchema), + }, + { + key: 'ld', // localDeltas + valueSchema: new Uint64MapSchema( + new StringMapSchema(ValueDelta.encodingSchema) + ), + }, + { + key: 'sa', // sharedAccts + valueSchema: new ArraySchema(new AddressSchema()), + }, + { + key: 'lg', // logs + valueSchema: new ArraySchema(new StringSchema()), + }, + { + key: 'itx', // innerTxns + // eslint-disable-next-line no-use-before-define + valueSchema: new ArraySchema(SignedTxnWithAD.encodingSchema), + }, + ]) + ); + } + return this.encodingSchemaValue; + } + + public globalDelta: Map; + + /** + * When decoding EvalDeltas, the integer key represents an offset into + * [txn.Sender, txn.Accounts[0], txn.Accounts[1], ...] + */ + public localDeltas: Map>; + + /** + * If a program modifies the local of an account that is not the Sender, or + * in txn.Accounts, it must be recorded here, so that the key in LocalDeltas + * can refer to it. + */ + public sharedAccts: Address[]; + + public logs: string[]; + + // eslint-disable-next-line no-use-before-define + public innerTxns: SignedTxnWithAD[]; + + public constructor(params: { + globalDelta: Map; + localDeltas: Map>; + sharedAccts: Address[]; + logs: string[]; + // eslint-disable-next-line no-use-before-define + innerTxns: SignedTxnWithAD[]; + }) { + this.globalDelta = params.globalDelta; + this.localDeltas = params.localDeltas; + this.sharedAccts = params.sharedAccts; + this.logs = params.logs; + this.innerTxns = params.innerTxns; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return EvalDelta.encodingSchema; + } + + public toEncodingData(): Map { + return new Map([ + [ + 'gd', + convertMap(this.globalDelta, (key, value) => [ + key, + value.toEncodingData(), + ]), + ], + [ + 'ld', + convertMap(this.localDeltas, (key, value) => [ + key, + convertMap(value, (k, v) => [k, v.toEncodingData()]), + ]), + ], + ['sa', this.sharedAccts], + ['lg', this.logs], + ['itx', this.innerTxns.map((t) => t.toEncodingData())], + ]); + } + + public static fromEncodingData(data: unknown): EvalDelta { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded EvalDelta: ${data}`); + } + return new EvalDelta({ + globalDelta: convertMap( + data.get('gd') as Map, + (key, value) => [key, ValueDelta.fromEncodingData(value)] + ), + localDeltas: convertMap( + data.get('ld') as Map>, + (key, value) => [ + Number(key), + convertMap(value, (k, v) => [k, ValueDelta.fromEncodingData(v)]), + ] + ), + sharedAccts: data.get('sa'), + logs: data.get('lg'), + // eslint-disable-next-line no-use-before-define + innerTxns: data.get('itx').map(SignedTxnWithAD.fromEncodingData), + }); + } +} + +export class ApplyData implements Encodable { + private static encodingSchemaValue: Schema | undefined; + + public static get encodingSchema(): Schema { + // This is declared like this in order to break the circular dependency of + // SignedTxnWithAD -> ApplyData -> EvalDelta -> SignedTxnWithAD + if (!this.encodingSchemaValue) { + this.encodingSchemaValue = new NamedMapSchema([]); + (this.encodingSchemaValue as NamedMapSchema).pushEntries( + ...allOmitEmpty([ + { + key: 'ca', // closingAmount + valueSchema: new Uint64Schema(), + }, + { + key: 'aca', // assetClosingAmount + valueSchema: new Uint64Schema(), + }, + { + key: 'rs', // senderRewards + valueSchema: new Uint64Schema(), + }, + { + key: 'rr', // receiverRewards + valueSchema: new Uint64Schema(), + }, + { + key: 'rc', // closeRewards + valueSchema: new Uint64Schema(), + }, + { + key: 'dt', // evalDelta + valueSchema: EvalDelta.encodingSchema, + }, + { + key: 'caid', // configAsset + valueSchema: new Uint64Schema(), + }, + { + key: 'apid', // applicationID + valueSchema: new Uint64Schema(), + }, + ]) + ); + } + return this.encodingSchemaValue; + } + + /** + * Closing amount for transaction. + */ + public closingAmount: bigint; + + /** + * Closing amount for asset transaction. + */ + public assetClosingAmount: bigint; + + /** + * Rewards applied to the Sender. + */ + public senderRewards: bigint; + + /** + * Rewards applied to the Receiver. + */ + public receiverRewards: bigint; + + /** + * Rewards applied to the CloseRemainderTo account. + */ + public closeRewards: bigint; + + public evalDelta: EvalDelta; + + public configAsset: bigint; + public applicationID: bigint; + + public constructor(params: { + closingAmount: bigint; + assetClosingAmount: bigint; + senderRewards: bigint; + receiverRewards: bigint; + closeRewards: bigint; + evalDelta: EvalDelta; + configAsset: bigint; + applicationID: bigint; + }) { + this.closingAmount = params.closingAmount; + this.assetClosingAmount = params.assetClosingAmount; + this.senderRewards = params.senderRewards; + this.receiverRewards = params.receiverRewards; + this.closeRewards = params.closeRewards; + this.evalDelta = params.evalDelta; + this.configAsset = params.configAsset; + this.applicationID = params.applicationID; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return ApplyData.encodingSchema; + } + + public toEncodingData(): Map { + return new Map([ + ['ca', this.closingAmount], + ['aca', this.assetClosingAmount], + ['rs', this.senderRewards], + ['rr', this.receiverRewards], + ['rc', this.closeRewards], + ['dt', this.evalDelta.toEncodingData()], + ['caid', this.configAsset], + ['apid', this.applicationID], + ]); + } + + public static fromEncodingData(data: unknown): ApplyData { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded ApplyData: ${data}`); + } + return new ApplyData({ + closingAmount: data.get('ca'), + assetClosingAmount: data.get('aca'), + senderRewards: data.get('rs'), + receiverRewards: data.get('rr'), + closeRewards: data.get('rc'), + evalDelta: EvalDelta.fromEncodingData(data.get('dt')), + configAsset: data.get('caid'), + applicationID: data.get('apid'), + }); + } +} + +export class SignedTxnWithAD implements Encodable { + private static encodingSchemaValue: Schema | undefined; + + public static get encodingSchema(): Schema { + // This is declared like this in order to break the circular dependency of + // SignedTxnWithAD -> ApplyData -> EvalDelta -> SignedTxnWithAD + if (!this.encodingSchemaValue) { + this.encodingSchemaValue = new NamedMapSchema([]); + (this.encodingSchemaValue as NamedMapSchema).pushEntries( + ...allOmitEmpty([ + { + key: '', + valueSchema: SignedTransaction.encodingSchema, + embedded: true, + }, + { + key: '', + valueSchema: ApplyData.encodingSchema, + embedded: true, + }, + ]) + ); + } + return this.encodingSchemaValue; + } + + public signedTxn: SignedTransaction; + + public applyData: ApplyData; + + public constructor(params: { + signedTxn: SignedTransaction; + applyData: ApplyData; + }) { + this.signedTxn = params.signedTxn; + this.applyData = params.applyData; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return SignedTxnWithAD.encodingSchema; + } + + public toEncodingData(): Map { + return combineMaps( + this.signedTxn.toEncodingData(), + this.applyData.toEncodingData() + ); + } + + public static fromEncodingData(data: unknown): SignedTxnWithAD { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded SignedTxnWithAD: ${data}`); + } + return new SignedTxnWithAD({ + signedTxn: SignedTransaction.fromEncodingData(data), + applyData: ApplyData.fromEncodingData(data), + }); + } +} + +/** + * SignedTxnInBlock is how a signed transaction is encoded in a block. + */ +export class SignedTxnInBlock implements Encodable { + public static readonly encodingSchema = new NamedMapSchema( + allOmitEmpty([ + { + key: '', + valueSchema: SignedTxnWithAD.encodingSchema, + embedded: true, + }, + { + key: 'hgi', // hasGenesisID + valueSchema: new BooleanSchema(), + }, + { + key: 'hgh', // hasGenesisHash + valueSchema: new BooleanSchema(), + }, + ]) + ); + + public signedTxn: SignedTxnWithAD; + + public hasGenesisID: boolean; + + public hasGenesisHash: boolean; + + public constructor(params: { + signedTxn: SignedTxnWithAD; + hasGenesisID: boolean; + hasGenesisHash: boolean; + }) { + this.signedTxn = params.signedTxn; + this.hasGenesisID = params.hasGenesisID; + this.hasGenesisHash = params.hasGenesisHash; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return SignedTxnInBlock.encodingSchema; + } + + public toEncodingData(): Map { + const data = new Map([ + ['hgi', this.hasGenesisID], + ['hgh', this.hasGenesisHash], + ]); + return combineMaps(data, this.signedTxn.toEncodingData()); + } + + public static fromEncodingData(data: unknown): SignedTxnInBlock { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded SignedTxnInBlock: ${data}`); + } + return new SignedTxnInBlock({ + signedTxn: SignedTxnWithAD.fromEncodingData(data), + hasGenesisID: data.get('hgi'), + hasGenesisHash: data.get('hgh'), + }); + } +} + /** * A Block contains the Payset and metadata corresponding to a given Round. */ @@ -760,16 +1206,19 @@ export class Block implements Encodable { }, { key: 'txns', // payset - valueSchema: new UntypedSchema(), // TODO: fix + valueSchema: new ArraySchema(SignedTxnInBlock.encodingSchema), }, ]) ); public header: BlockHeader; - public payset: unknown; // TODO: fix + public payset: SignedTxnInBlock[]; - public constructor(params: { header: BlockHeader; payset: unknown }) { + public constructor(params: { + header: BlockHeader; + payset: SignedTxnInBlock[]; + }) { this.header = params.header; this.payset = params.payset; } @@ -781,7 +1230,7 @@ export class Block implements Encodable { public toEncodingData(): Map { const data = new Map([ - ['txns', this.payset], // TODO: fix + ['txns', this.payset.map((p) => p.toEncodingData())], ]); return combineMaps(data, this.header.toEncodingData()); } @@ -792,7 +1241,7 @@ export class Block implements Encodable { } return new Block({ header: BlockHeader.fromEncodingData(data), - payset: data.get('txn'), // TODO: fix + payset: data.get('txns').map(SignedTxnInBlock.fromEncodingData), }); } } From c036cbf475031f3e2c559a142a96a63077f3f60c Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Fri, 14 Jun 2024 17:16:33 -0400 Subject: [PATCH 6/9] Test BlockResponse decoding and fix bugs --- src/main.ts | 1 + src/types/block.ts | 134 ++++++++++++++----------- tests/2.Encoding.ts | 231 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 308 insertions(+), 58 deletions(-) diff --git a/src/main.ts b/src/main.ts index c13e053b1..2e2e7b6b0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -138,6 +138,7 @@ export { encodeUint64, decodeUint64 } from './encoding/uint64.js'; export { parseJSON, ParseJSONOptions, stringifyJSON } from './utils/utils.js'; export { default as generateAccount } from './account.js'; export * from './types/block.js'; +export { UntypedValue } from './client/v2/untypedmodel.js'; export * as modelsv2 from './client/v2/algod/models/types.js'; export * as indexerModels from './client/v2/indexer/models/types.js'; export { diff --git a/src/types/block.ts b/src/types/block.ts index 65fe3807c..501077269 100644 --- a/src/types/block.ts +++ b/src/types/block.ts @@ -10,6 +10,7 @@ import { AddressSchema, ByteArraySchema, FixedLengthByteArraySchema, + OptionalSchema, allOmitEmpty, combineMaps, convertMap, @@ -374,7 +375,7 @@ export class UpgradeVote implements Encodable { }, { key: 'upgradeyes', // upgradeApprove - valueSchema: new Uint64Schema(), + valueSchema: new BooleanSchema(), }, ]) ); @@ -736,8 +737,8 @@ export class BlockHeader implements Encodable { seed: data.get('seed'), txnCommitments: TxnCommitments.fromEncodingData(data), timestamp: data.get('ts'), - genesisHash: data.get('gen'), - genesisID: data.get('gh'), + genesisID: data.get('gen'), + genesisHash: data.get('gh'), proposer: data.get('prp'), feesCollected: data.get('fc'), bonus: data.get('bi'), @@ -823,26 +824,36 @@ export class EvalDelta implements Encodable { ...allOmitEmpty([ { key: 'gd', // globalDelta - valueSchema: new StringMapSchema(ValueDelta.encodingSchema), + valueSchema: new OptionalSchema( + new StringMapSchema(ValueDelta.encodingSchema) + ), }, { key: 'ld', // localDeltas - valueSchema: new Uint64MapSchema( - new StringMapSchema(ValueDelta.encodingSchema) + valueSchema: new OptionalSchema( + new Uint64MapSchema( + new StringMapSchema(ValueDelta.encodingSchema) + ) ), }, { key: 'sa', // sharedAccts - valueSchema: new ArraySchema(new AddressSchema()), + valueSchema: new OptionalSchema( + new ArraySchema(new AddressSchema()) + ), }, { key: 'lg', // logs - valueSchema: new ArraySchema(new StringSchema()), + valueSchema: new OptionalSchema( + new ArraySchema(new StringSchema()) + ), }, { key: 'itx', // innerTxns - // eslint-disable-next-line no-use-before-define - valueSchema: new ArraySchema(SignedTxnWithAD.encodingSchema), + valueSchema: new OptionalSchema( + // eslint-disable-next-line no-use-before-define + new ArraySchema(SignedTxnWithAD.encodingSchema) + ), }, ]) ); @@ -871,18 +882,19 @@ export class EvalDelta implements Encodable { public innerTxns: SignedTxnWithAD[]; public constructor(params: { - globalDelta: Map; - localDeltas: Map>; - sharedAccts: Address[]; - logs: string[]; + globalDelta?: Map; + localDeltas?: Map>; + sharedAccts?: Address[]; + logs?: string[]; // eslint-disable-next-line no-use-before-define - innerTxns: SignedTxnWithAD[]; + innerTxns?: SignedTxnWithAD[]; }) { - this.globalDelta = params.globalDelta; - this.localDeltas = params.localDeltas; - this.sharedAccts = params.sharedAccts; - this.logs = params.logs; - this.innerTxns = params.innerTxns; + this.globalDelta = params.globalDelta ?? new Map(); + this.localDeltas = + params.localDeltas ?? new Map>(); + this.sharedAccts = params.sharedAccts ?? []; + this.logs = params.logs ?? []; + this.innerTxns = params.innerTxns ?? []; } // eslint-disable-next-line class-methods-use-this @@ -917,21 +929,25 @@ export class EvalDelta implements Encodable { throw new Error(`Invalid decoded EvalDelta: ${data}`); } return new EvalDelta({ - globalDelta: convertMap( - data.get('gd') as Map, - (key, value) => [key, ValueDelta.fromEncodingData(value)] - ), - localDeltas: convertMap( - data.get('ld') as Map>, - (key, value) => [ - Number(key), - convertMap(value, (k, v) => [k, ValueDelta.fromEncodingData(v)]), - ] - ), + globalDelta: data.get('gd') + ? convertMap(data.get('gd') as Map, (key, value) => [ + key, + ValueDelta.fromEncodingData(value), + ]) + : undefined, + localDeltas: data.get('ld') + ? convertMap( + data.get('ld') as Map>, + (key, value) => [ + Number(key), + convertMap(value, (k, v) => [k, ValueDelta.fromEncodingData(v)]), + ] + ) + : undefined, sharedAccts: data.get('sa'), logs: data.get('lg'), // eslint-disable-next-line no-use-before-define - innerTxns: data.get('itx').map(SignedTxnWithAD.fromEncodingData), + innerTxns: (data.get('itx') ?? []).map(SignedTxnWithAD.fromEncodingData), }); } } @@ -948,35 +964,35 @@ export class ApplyData implements Encodable { ...allOmitEmpty([ { key: 'ca', // closingAmount - valueSchema: new Uint64Schema(), + valueSchema: new OptionalSchema(new Uint64Schema()), }, { key: 'aca', // assetClosingAmount - valueSchema: new Uint64Schema(), + valueSchema: new OptionalSchema(new Uint64Schema()), }, { key: 'rs', // senderRewards - valueSchema: new Uint64Schema(), + valueSchema: new OptionalSchema(new Uint64Schema()), }, { key: 'rr', // receiverRewards - valueSchema: new Uint64Schema(), + valueSchema: new OptionalSchema(new Uint64Schema()), }, { key: 'rc', // closeRewards - valueSchema: new Uint64Schema(), + valueSchema: new OptionalSchema(new Uint64Schema()), }, { key: 'dt', // evalDelta - valueSchema: EvalDelta.encodingSchema, + valueSchema: new OptionalSchema(EvalDelta.encodingSchema), }, { key: 'caid', // configAsset - valueSchema: new Uint64Schema(), + valueSchema: new OptionalSchema(new Uint64Schema()), }, { key: 'apid', // applicationID - valueSchema: new Uint64Schema(), + valueSchema: new OptionalSchema(new Uint64Schema()), }, ]) ); @@ -987,42 +1003,42 @@ export class ApplyData implements Encodable { /** * Closing amount for transaction. */ - public closingAmount: bigint; + public closingAmount?: bigint; /** * Closing amount for asset transaction. */ - public assetClosingAmount: bigint; + public assetClosingAmount?: bigint; /** * Rewards applied to the Sender. */ - public senderRewards: bigint; + public senderRewards?: bigint; /** * Rewards applied to the Receiver. */ - public receiverRewards: bigint; + public receiverRewards?: bigint; /** * Rewards applied to the CloseRemainderTo account. */ - public closeRewards: bigint; + public closeRewards?: bigint; - public evalDelta: EvalDelta; + public evalDelta?: EvalDelta; - public configAsset: bigint; - public applicationID: bigint; + public configAsset?: bigint; + public applicationID?: bigint; public constructor(params: { - closingAmount: bigint; - assetClosingAmount: bigint; - senderRewards: bigint; - receiverRewards: bigint; - closeRewards: bigint; - evalDelta: EvalDelta; - configAsset: bigint; - applicationID: bigint; + closingAmount?: bigint; + assetClosingAmount?: bigint; + senderRewards?: bigint; + receiverRewards?: bigint; + closeRewards?: bigint; + evalDelta?: EvalDelta; + configAsset?: bigint; + applicationID?: bigint; }) { this.closingAmount = params.closingAmount; this.assetClosingAmount = params.assetClosingAmount; @@ -1046,7 +1062,7 @@ export class ApplyData implements Encodable { ['rs', this.senderRewards], ['rr', this.receiverRewards], ['rc', this.closeRewards], - ['dt', this.evalDelta.toEncodingData()], + ['dt', this.evalDelta ? this.evalDelta.toEncodingData() : undefined], ['caid', this.configAsset], ['apid', this.applicationID], ]); @@ -1062,7 +1078,9 @@ export class ApplyData implements Encodable { senderRewards: data.get('rs'), receiverRewards: data.get('rr'), closeRewards: data.get('rc'), - evalDelta: EvalDelta.fromEncodingData(data.get('dt')), + evalDelta: data.get('dt') + ? EvalDelta.fromEncodingData(data.get('dt')) + : undefined, configAsset: data.get('caid'), applicationID: data.get('apid'), }); diff --git a/tests/2.Encoding.ts b/tests/2.Encoding.ts index 6c208ce79..ec2188320 100644 --- a/tests/2.Encoding.ts +++ b/tests/2.Encoding.ts @@ -1725,4 +1725,235 @@ describe('encoding', () => { }); }); }); + describe('BlockResponse', () => { + it('should decode block response correctly', () => { + const encodedBlockResponse = algosdk.base64ToBytes( + 'gqVibG9ja94AEqJiac4AmJaAomZjzQPopGZlZXPEIAfay0ttntFBsXV2vUWa5kIdSG2j1O8iR8QJo5a4LqIho2dlbqd0ZXN0LXYxomdoxCBAkI9g4Zidj7KmD7u3TupPRyxIUOsvsCzO1IlIiDKoqKRwcmV2xCDwEylSD3iD5mcCkowyEagukv+yiXVP06RyaMykuKPaqaVwcm90b6ZmdXR1cmWjcHJwxCB/k/5DqCp+P1WW/80hkucvP2neluTb4OL7yjQQnAxn6aNybmRepnJ3Y2Fscs4AB6Ego3J3ZMQg//////////////////////////////////////////+kc2VlZMQgADr2hmA6p7J28mz5Xje3TrogRklc+wKrrknYFoSPXIqjc3B0gQCBoW7NAgCidGPNA+6idHPOZmyEZaN0eG7EILLaFZI8ZSO3lpCwDTjv6JdxgLRqnLkOCthaTJyfoaSipnR4bjI1NsQgyA7uIgAR0IxH57DVsL4snrEy5FdvuTtWPvyPiJfzZGSkdHhuc5GEomNhzwAAJGE4t9aGo2hnacOjc2lnxEAT74Xeryh/ZJtRxGqcKf8UueJmWXmHH9NuQYTIrJqzKI1kKsFHn7smLAwoa0hDSUeUGI5kvWZvM28ggFiOxykMo3R4boelY2xvc2XEIAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAo2ZlZc0D6KJmdlyibHbNBESjcmN2xCABAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKNzbmTEIH+T/kOoKn4/VZb/zSGS5y8/ad6W5Nvg4vvKNBCcDGfppHR5cGWjcGF5pGNlcnSEpHByb3CDo2RpZ8Qgl8J9zpVrkwvOVTZlN3p6b0iKuUpWii4Z0ga04n/XeFGmZW5jZGlnxCCG+emw6b9UVPNwpFN+l3F4SDOSkIKxkgpWSiBi0O62q6VvcHJvcMQgf5P+Q6gqfj9Vlv/NIZLnLz9p3pbk2+Di+8o0EJwMZ+mjcm5kXqRzdGVwAqR2b3RlkYOkY3JlZIGicGbEUIcs4SBw5LBFVDrqyGzHbeuh/PsY5Fr/1oZ+DoPl+N8aAs2ZiEsuPoE/+6oNsiX6YJNFVSBQKaRQBWdPDndXc9w3jq6WR6cEEoi4rCAyyv8Io3NpZ4ahcMQgmOdtSatJuUOlf8qRypCU3uEm3AewgEq+xVOIhtmWUZejcDFzxEAbCsynu50W2/vt6HPCCTAf37rvW1RHk1Y8EnLvBrFIzxi6nZRPeoYdgr4D8yIEKB4Gc7BMFrQbzd1HGKxLKS8GonAyxCCaal9Yfd6VseKV5WCb5lFEeYo3J0X1uOxEspMS8z9uaKNwMnPEQGmZZYbLBiwlzCbu7pdhDl9jSsyWCKW5aM5u0jeSVJGdYzC5SwpVGXlasYN8yj0Z5DKqwweY0ATwg02PCK1xkw2icHPEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAChc8RAD7PBhIk/Yl40TpUojfBQj79CHOXwpmAToXgmRWG2rkDvmIh4sUjAaXVHdFla1uBMqgLrOk1rEo12QUthenYrB6NzbmTEIH+T/kOoKn4/VZb/zSGS5y8/ad6W5Nvg4vvKNBCcDGfp' + ); + const blockResponse = algosdk.decodeMsgpack( + encodedBlockResponse, + algosdk.modelsv2.BlockResponse + ); + const expectedBlockResponse = new algosdk.modelsv2.BlockResponse({ + block: new algosdk.Block({ + header: new algosdk.BlockHeader({ + round: BigInt(94), + branch: algosdk.base64ToBytes( + '8BMpUg94g+ZnApKMMhGoLpL/sol1T9OkcmjMpLij2qk=' + ), + seed: algosdk.base64ToBytes( + 'ADr2hmA6p7J28mz5Xje3TrogRklc+wKrrknYFoSPXIo=' + ), + txnCommitments: new algosdk.TxnCommitments({ + nativeSha512_256Commitment: algosdk.base64ToBytes( + 'stoVkjxlI7eWkLANOO/ol3GAtGqcuQ4K2FpMnJ+hpKI=' + ), + sha256Commitment: algosdk.base64ToBytes( + 'yA7uIgAR0IxH57DVsL4snrEy5FdvuTtWPvyPiJfzZGQ=' + ), + }), + timestamp: BigInt(1718387813), + genesisID: 'test-v1', + genesisHash: algosdk.base64ToBytes( + 'QJCPYOGYnY+ypg+7t07qT0csSFDrL7AsztSJSIgyqKg=' + ), + proposer: new algosdk.Address( + algosdk.base64ToBytes( + 'f5P+Q6gqfj9Vlv/NIZLnLz9p3pbk2+Di+8o0EJwMZ+k=' + ) + ), + feesCollected: BigInt(1000), + bonus: BigInt(10000000), + proposerPayout: BigInt(0), + rewardState: new algosdk.RewardState({ + feeSink: new algosdk.Address( + algosdk.base64ToBytes( + 'B9rLS22e0UGxdXa9RZrmQh1IbaPU7yJHxAmjlrguoiE=' + ) + ), + rewardsPool: new algosdk.Address( + algosdk.base64ToBytes( + '//////////////////////////////////////////8=' + ) + ), + rewardsLevel: BigInt(0), + rewardsRate: BigInt(0), + rewardsResidue: BigInt(0), + rewardsRecalculationRound: BigInt(500000), + }), + upgradeState: new algosdk.UpgradeState({ + currentProtocol: 'future', + nextProtocol: '', + nextProtocolApprovals: BigInt(0), + nextProtocolVoteBefore: BigInt(0), + nextProtocolSwitchOn: BigInt(0), + }), + upgradeVote: new algosdk.UpgradeVote({ + upgradePropose: '', + upgradeDelay: BigInt(0), + upgradeApprove: false, + }), + txnCounter: BigInt(1006), + stateproofTracking: new Map( + [ + [ + 0, + new algosdk.StateProofTrackingData({ + stateProofVotersCommitment: new Uint8Array(), + stateProofOnlineTotalWeight: BigInt(0), + stateProofNextRound: BigInt(512), + }), + ], + ] + ), + participationUpdates: new algosdk.ParticipationUpdates({ + expiredParticipationAccounts: [], + absentParticipationAccounts: [], + }), + }), + payset: [ + new algosdk.SignedTxnInBlock({ + hasGenesisID: true, + hasGenesisHash: false, + signedTxn: new algosdk.SignedTxnWithAD({ + signedTxn: new algosdk.SignedTransaction({ + txn: new algosdk.Transaction({ + sender: new algosdk.Address( + algosdk.base64ToBytes( + 'f5P+Q6gqfj9Vlv/NIZLnLz9p3pbk2+Di+8o0EJwMZ+k=' + ) + ), + type: algosdk.TransactionType.pay, + suggestedParams: { + flatFee: true, + fee: BigInt(1000), + firstValid: BigInt(92), + lastValid: BigInt(1092), + minFee: BigInt(1000), + }, + paymentParams: { + amount: 0, + receiver: new algosdk.Address( + algosdk.base64ToBytes( + 'AQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' + ) + ), + closeRemainderTo: new algosdk.Address( + algosdk.base64ToBytes( + 'AQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' + ) + ), + }, + }), + sig: algosdk.base64ToBytes( + 'E++F3q8of2SbUcRqnCn/FLniZll5hx/TbkGEyKyasyiNZCrBR5+7JiwMKGtIQ0lHlBiOZL1mbzNvIIBYjscpDA==' + ), + }), + applyData: new algosdk.ApplyData({ + closingAmount: BigInt('39999981999750'), + }), + }), + }), + ], + }), + cert: new algosdk.UntypedValue( + new Map([ + [ + 'prop', + new Map([ + [ + 'dig', + algosdk.base64ToBytes( + 'l8J9zpVrkwvOVTZlN3p6b0iKuUpWii4Z0ga04n/XeFE=' + ), + ], + [ + 'encdig', + algosdk.base64ToBytes( + 'hvnpsOm/VFTzcKRTfpdxeEgzkpCCsZIKVkogYtDutqs=' + ), + ], + [ + 'oprop', + algosdk.base64ToBytes( + 'f5P+Q6gqfj9Vlv/NIZLnLz9p3pbk2+Di+8o0EJwMZ+k=' + ), + ], + ]), + ], + ['rnd', 94], + ['step', 2], + [ + 'vote', + [ + new Map([ + [ + 'cred', + new Map([ + [ + 'pf', + algosdk.base64ToBytes( + 'hyzhIHDksEVUOurIbMdt66H8+xjkWv/Whn4Og+X43xoCzZmISy4+gT/7qg2yJfpgk0VVIFAppFAFZ08Od1dz3DeOrpZHpwQSiLisIDLK/wg=' + ), + ], + ]), + ], + [ + 'sig', + new Map([ + [ + 'p1s', + algosdk.base64ToBytes( + 'GwrMp7udFtv77ehzwgkwH9+671tUR5NWPBJy7waxSM8Yup2UT3qGHYK+A/MiBCgeBnOwTBa0G83dRxisSykvBg==' + ), + ], + [ + 'p2', + algosdk.base64ToBytes( + 'mmpfWH3elbHileVgm+ZRRHmKNydF9bjsRLKTEvM/bmg=' + ), + ], + [ + 'p2s', + algosdk.base64ToBytes( + 'aZllhssGLCXMJu7ul2EOX2NKzJYIpblozm7SN5JUkZ1jMLlLClUZeVqxg3zKPRnkMqrDB5jQBPCDTY8IrXGTDQ==' + ), + ], + [ + 'p', + algosdk.base64ToBytes( + 'mOdtSatJuUOlf8qRypCU3uEm3AewgEq+xVOIhtmWUZc=' + ), + ], + [ + 'ps', + algosdk.base64ToBytes( + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==' + ), + ], + [ + 's', + algosdk.base64ToBytes( + 'D7PBhIk/Yl40TpUojfBQj79CHOXwpmAToXgmRWG2rkDvmIh4sUjAaXVHdFla1uBMqgLrOk1rEo12QUthenYrBw==' + ), + ], + ]), + ], + [ + 'snd', + algosdk.base64ToBytes( + 'f5P+Q6gqfj9Vlv/NIZLnLz9p3pbk2+Di+8o0EJwMZ+k=' + ), + ], + ]), + ], + ], + ]) + ), + }); + assert.deepStrictEqual(blockResponse, expectedBlockResponse); + const reencoded = algosdk.encodeMsgpack(blockResponse); + assert.deepStrictEqual(reencoded, encodedBlockResponse); + }); + }); }); From 2f681f96fd71459b6dcc2d7f4efdc7e7d4d8873e Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Fri, 21 Jun 2024 15:49:28 -0400 Subject: [PATCH 7/9] Add coverage of ApplyData encoding & decoding --- src/types/block.ts | 7 +++ tests/2.Encoding.ts | 106 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/src/types/block.ts b/src/types/block.ts index 501077269..08256be3b 100644 --- a/src/types/block.ts +++ b/src/types/block.ts @@ -1027,7 +1027,14 @@ export class ApplyData implements Encodable { public evalDelta?: EvalDelta; + /** + * If an ASA is being created, this is its newly created ID. Else 0. + */ public configAsset?: bigint; + + /** + * If an application is being created, this is its newly created ID. Else 0. + */ public applicationID?: bigint; public constructor(params: { diff --git a/tests/2.Encoding.ts b/tests/2.Encoding.ts index ec2188320..a202f226a 100644 --- a/tests/2.Encoding.ts +++ b/tests/2.Encoding.ts @@ -1955,5 +1955,111 @@ describe('encoding', () => { const reencoded = algosdk.encodeMsgpack(blockResponse); assert.deepStrictEqual(reencoded, encodedBlockResponse); }); + it('should decode ApplyData correctly', () => { + const encodedApplyData = algosdk.base64ToBytes( + 'iKNhY2HP//////////+kYXBpZM0iuKJjYc8AACRhOLfWhqRjYWlkzR5homR0haJnZIKqZ2xvYmFsS2V5MYKiYXQBomJzo2FiY6pnbG9iYWxLZXkygqJhdAKidWkyo2l0eJGComR0gaJsZ5KkbG9nM6Rsb2c0o3R4boakYXBpZM0eYaNmZWXNA+iiZnZcomx2zQREo3NuZMQgf5P+Q6gqfj9Vlv/NIZLnLz9p3pbk2+Di+8o0EJwMZ+mkdHlwZaRhcHBsomxkggCBqWxvY2FsS2V5MYKiYXQBomJzo2RlZgKBqWxvY2FsS2V5MoKiYXQConVpM6JsZ5KkbG9nMaRsb2cyonNhksQgCbEzlTT2uNiZwobypXnCOg5IqgxtO92MuwR8vJwv3ePEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAonJjEaJycgSicnN7' + ); + const applyData = algosdk.decodeMsgpack( + encodedApplyData, + algosdk.ApplyData + ); + const expectedApplyData = new algosdk.ApplyData({ + closingAmount: BigInt('39999981999750'), + assetClosingAmount: BigInt('0xffffffffffffffff'), + senderRewards: BigInt(123), + receiverRewards: BigInt(4), + closeRewards: BigInt(17), + configAsset: BigInt(7777), + applicationID: BigInt(8888), + evalDelta: new algosdk.EvalDelta({ + globalDelta: new Map([ + [ + 'globalKey1', + new algosdk.ValueDelta({ + action: 1, + uint: BigInt(0), + bytes: 'abc', + }), + ], + [ + 'globalKey2', + new algosdk.ValueDelta({ + action: 2, + uint: BigInt(50), + bytes: '', + }), + ], + ]), + localDeltas: new Map>([ + [ + 0, + new Map([ + [ + 'localKey1', + new algosdk.ValueDelta({ + action: 1, + uint: BigInt(0), + bytes: 'def', + }), + ], + ]), + ], + [ + 2, + new Map([ + [ + 'localKey2', + new algosdk.ValueDelta({ + action: 2, + uint: BigInt(51), + bytes: '', + }), + ], + ]), + ], + ]), + sharedAccts: [ + algosdk.Address.fromString( + 'BGYTHFJU624NRGOCQ3ZKK6OCHIHERKQMNU553DF3AR6LZHBP3XR5JLNCUI' + ), + algosdk.Address.zeroAddress(), + ], + logs: ['log1', 'log2'], + innerTxns: [ + new algosdk.SignedTxnWithAD({ + signedTxn: new algosdk.SignedTransaction({ + txn: new algosdk.Transaction({ + sender: new algosdk.Address( + algosdk.base64ToBytes( + 'f5P+Q6gqfj9Vlv/NIZLnLz9p3pbk2+Di+8o0EJwMZ+k=' + ) + ), + type: algosdk.TransactionType.appl, + suggestedParams: { + flatFee: true, + fee: BigInt(1000), + firstValid: BigInt(92), + lastValid: BigInt(1092), + minFee: BigInt(1000), + }, + appCallParams: { + appIndex: BigInt(7777), + onComplete: algosdk.OnApplicationComplete.NoOpOC, + }, + }), + }), + applyData: new algosdk.ApplyData({ + evalDelta: new algosdk.EvalDelta({ + logs: ['log3', 'log4'], + }), + }), + }), + ], + }), + }); + assert.deepStrictEqual(applyData, expectedApplyData); + const reencoded = algosdk.encodeMsgpack(applyData); + assert.deepStrictEqual(reencoded, encodedApplyData); + }); }); }); From 4f5eae3036ef11c06d32e61eb4ded0fc5b14f5f7 Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Mon, 24 Jun 2024 11:35:17 -0400 Subject: [PATCH 8/9] More test coverage for map utils --- src/encoding/schema/map.ts | 20 ++++- tests/4.Utils.ts | 145 ++++++++++++++++++++++++++++++++++++- 2 files changed, 161 insertions(+), 4 deletions(-) diff --git a/src/encoding/schema/map.ts b/src/encoding/schema/map.ts index 9f1e26af7..21984f06b 100644 --- a/src/encoding/schema/map.ts +++ b/src/encoding/schema/map.ts @@ -29,6 +29,11 @@ export interface NamedMapEntry { embedded?: boolean; } +/** + * Applies the omitEmpty flag to all entries in the array. + * @param entries - The entries to apply the flag to. + * @returns A new array with the omitEmpty flag applied to all entries. + */ export function allOmitEmpty( entries: Array> ): NamedMapEntry[] { @@ -36,7 +41,7 @@ export function allOmitEmpty( } /** - * Schema for a map/struct with a fixed set of string fields. + * Schema for a map/struct with a fixed set of known string fields. */ export class NamedMapSchema extends Schema { private readonly entries: NamedMapEntry[]; @@ -195,6 +200,11 @@ export class NamedMapSchema extends Schema { } } +/** + * Combines multiple maps into a single map. Throws an error if any of the maps have duplicate keys. + * @param maps - The maps to combine. + * @returns A new map with all the entries from the input maps. + */ export function combineMaps(...maps: Array>): Map { const combined = new Map(); for (const map of maps) { @@ -208,6 +218,12 @@ export function combineMaps(...maps: Array>): Map { return combined; } +/** + * Converts a map to a new map with different keys and values. + * @param map - The map to convert. + * @param func - The function to convert each entry. + * @returns A new map with the converted entries. + */ export function convertMap( map: Map, func: (k: K1, v: V1) => [K2, V2] @@ -313,7 +329,7 @@ export class Uint64MapSchema extends Schema { } /** - * Schema for a map with a variable number of uint64 keys. + * Schema for a map with a variable number of string keys. */ export class StringMapSchema extends Schema { constructor(public readonly valueSchema: Schema) { diff --git a/tests/4.Utils.ts b/tests/4.Utils.ts index f478c0a1a..824e521bd 100644 --- a/tests/4.Utils.ts +++ b/tests/4.Utils.ts @@ -1,7 +1,8 @@ /* eslint-env mocha */ import assert from 'assert'; -import * as utils from '../src/utils/utils'; -import * as nacl from '../src/nacl/naclWrappers'; +import * as utils from '../src/utils/utils.js'; +import * as nacl from '../src/nacl/naclWrappers.js'; +import { combineMaps, convertMap } from '../src/encoding/schema/index.js'; describe('utils', () => { describe('concatArrays', () => { @@ -422,3 +423,143 @@ describe('nacl wrapper', () => { assert.strictEqual(nacl.isValidSignatureLength(64), true); }); }); + +describe('encoding utils', () => { + describe('combineMaps', () => { + it('should work on no inputs', () => { + const actual = combineMaps(); + const expected = new Map(); + assert.deepStrictEqual(actual, expected); + }); + + it('should work on one input', () => { + const a = new Map([ + ['a', 1], + ['b', 2], + ]); + + const actual = combineMaps(a); + const expected = new Map([ + ['a', 1], + ['b', 2], + ]); + assert.deepStrictEqual(actual, expected); + + assert.notEqual(actual, a); + }); + + it('should combine two maps', () => { + const a = new Map([ + ['a', 1], + ['b', 2], + ]); + const b = new Map([ + ['c', 3], + ['d', 4], + ]); + + const actual = combineMaps(a, b); + const expected = new Map([ + ['a', 1], + ['b', 2], + ['c', 3], + ['d', 4], + ]); + assert.deepStrictEqual(actual, expected); + + assert.notEqual(actual, a); + assert.notEqual(actual, b); + }); + + it('should combine three maps', () => { + const a = new Map([ + ['a', 1], + ['b', 2], + ]); + const b = new Map([ + ['c', 3], + ['d', 4], + ]); + const c = new Map([ + ['e', 5], + ['f', 6], + ]); + + const actual = combineMaps(a, b, c); + const expected = new Map([ + ['a', 1], + ['b', 2], + ['c', 3], + ['d', 4], + ['e', 5], + ['f', 6], + ]); + assert.deepStrictEqual(actual, expected); + + assert.notEqual(actual, a); + assert.notEqual(actual, b); + }); + + it('should error on duplicate keys', () => { + const a = new Map([ + ['a', 1], + ['b', 2], + ]); + const b = new Map([ + ['c', 3], + ['d', 4], + ['a', 5], + ]); + + assert.throws(() => combineMaps(a, b), new Error('Duplicate key: a')); + }); + }); + + describe('convertMap', () => { + it('should produce correct results', () => { + const map = new Map([ + ['a', 1], + ['b', 2], + ['c', 3], + ]); + + const func = (key: string, value: number): [number, string] => [ + value + 1, + key.toUpperCase(), + ]; + + const actual = convertMap(map, func); + const expected = new Map([ + [2, 'A'], + [3, 'B'], + [4, 'C'], + ]); + assert.deepStrictEqual(actual, expected); + + assert.notEqual(actual, map); + }); + + it('should produce correct results even under a key collision', () => { + const map = new Map([ + [2, 'a'], + [3, 'b'], + [4, 'c'], + ]); + + const func = (key: number, value: string): [number, string] => [ + Math.floor(key / 2), + value, + ]; + + const actual = convertMap(map, func); + const expected = new Map([ + // The 'a' value also gets mapped to the 1 key, but it is overwritten + [1, 'b'], + [2, 'c'], + ]); + assert.deepStrictEqual(actual, expected); + + assert.notEqual(actual, map); + }); + }); +}); From 0649b472214dbe2b72aa477e3cd294b3a7a5638d Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Mon, 24 Jun 2024 11:38:51 -0400 Subject: [PATCH 9/9] StringMapSchema coverage --- tests/2.Encoding.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/2.Encoding.ts b/tests/2.Encoding.ts index a202f226a..05d7c7afc 100644 --- a/tests/2.Encoding.ts +++ b/tests/2.Encoding.ts @@ -18,6 +18,7 @@ import { NamedMapSchema, NamedMapEntry, Uint64MapSchema, + StringMapSchema, UntypedSchema, OptionalSchema, allOmitEmpty, @@ -1085,6 +1086,37 @@ describe('encoding', () => { }, ], }, + { + name: 'StringMapSchema of BooleanSchema', + schema: new StringMapSchema(new BooleanSchema()), + values: [ + new Map(), + new Map([ + ['a', true], + ['b', false], + ['c', true], + ['', true], + ]), + ], + preparedMsgpackValues: [ + new Map(), + new Map([ + ['a', true], + ['b', false], + ['c', true], + ['', true], + ]), + ], + preparedJsonValues: [ + {}, + { + a: true, + b: false, + c: true, + '': true, + }, + ], + }, ]; const primitiveTestcases = testcases.slice(); @@ -1302,6 +1334,16 @@ describe('encoding', () => { [BigInt('18446744073709551615'), true], ]), }, + { + schema: new StringMapSchema(new BooleanSchema()), + emptyValue: new Map(), + nonemptyValue: new Map([ + ['a', true], + ['b', false], + ['c', true], + ['', true], + ]), + }, ]; for (const testValue of testValues.slice()) {