From 0d1dd7398c5ec2df0d33cdd46c6cfb36f6b77645 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:59:08 -0500 Subject: [PATCH 1/3] Regenerate code from specification file (#914) Co-authored-by: Algorand Generation Bot --- src/client/v2/algod/models/types.ts | 52 ++++ src/client/v2/indexer/models/types.ts | 375 ++++++++++++++++++++++++++ 2 files changed, 427 insertions(+) diff --git a/src/client/v2/algod/models/types.ts b/src/client/v2/algod/models/types.ts index 7fc46ea60..9923e93e9 100644 --- a/src/client/v2/algod/models/types.ts +++ b/src/client/v2/algod/models/types.ts @@ -2904,6 +2904,58 @@ export class BlockHashResponse implements Encodable { } } +/** + * Block header. + */ +export class BlockHeaderResponse implements Encodable { + private static encodingSchemaValue: Schema | undefined; + + static get encodingSchema(): Schema { + if (!this.encodingSchemaValue) { + this.encodingSchemaValue = new NamedMapSchema([]); + (this.encodingSchemaValue as NamedMapSchema).pushEntries({ + key: 'blockHeader', + valueSchema: Block.encodingSchema, + omitEmpty: true, + }); + } + return this.encodingSchemaValue; + } + + /** + * Block header data. + */ + public blockheader: Block; + + /** + * Creates a new `BlockHeaderResponse` object. + * @param blockheader - Block header data. + */ + constructor({ blockheader }: { blockheader: Block }) { + this.blockheader = blockheader; + } + + // eslint-disable-next-line class-methods-use-this + getEncodingSchema(): Schema { + return BlockHeaderResponse.encodingSchema; + } + + toEncodingData(): Map { + return new Map([ + ['blockHeader', this.blockheader.toEncodingData()], + ]); + } + + static fromEncodingData(data: unknown): BlockHeaderResponse { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded BlockHeaderResponse: ${data}`); + } + return new BlockHeaderResponse({ + blockheader: Block.fromEncodingData(data.get('blockHeader') ?? new Map()), + }); + } +} + /** * All logs emitted in the given round. Each app call, whether top-level or inner, * that contains logs results in a separate AppCallLogs object. Therefore there may diff --git a/src/client/v2/indexer/models/types.ts b/src/client/v2/indexer/models/types.ts index 5b8929221..3686b0427 100644 --- a/src/client/v2/indexer/models/types.ts +++ b/src/client/v2/indexer/models/types.ts @@ -3410,6 +3410,97 @@ export class Block implements Encodable { } } +/** + * + */ +export class BlockHeadersResponse implements Encodable { + private static encodingSchemaValue: Schema | undefined; + + static get encodingSchema(): Schema { + if (!this.encodingSchemaValue) { + this.encodingSchemaValue = new NamedMapSchema([]); + (this.encodingSchemaValue as NamedMapSchema).pushEntries( + { + key: 'blocks', + valueSchema: new ArraySchema(Block.encodingSchema), + omitEmpty: true, + }, + { + key: 'current-round', + valueSchema: new Uint64Schema(), + omitEmpty: true, + }, + { + key: 'next-token', + valueSchema: new OptionalSchema(new StringSchema()), + omitEmpty: true, + } + ); + } + return this.encodingSchemaValue; + } + + public blocks: Block[]; + + /** + * Round at which the results were computed. + */ + public currentRound: bigint; + + /** + * Used for pagination, when making another request provide this token with the + * next parameter. + */ + public nextToken?: string; + + /** + * Creates a new `BlockHeadersResponse` object. + * @param blocks - + * @param currentRound - Round at which the results were computed. + * @param nextToken - Used for pagination, when making another request provide this token with the + * next parameter. + */ + constructor({ + blocks, + currentRound, + nextToken, + }: { + blocks: Block[]; + currentRound: number | bigint; + nextToken?: string; + }) { + this.blocks = blocks; + this.currentRound = ensureBigInt(currentRound); + this.nextToken = nextToken; + } + + // eslint-disable-next-line class-methods-use-this + getEncodingSchema(): Schema { + return BlockHeadersResponse.encodingSchema; + } + + toEncodingData(): Map { + return new Map([ + ['blocks', this.blocks.map((v) => v.toEncodingData())], + ['current-round', this.currentRound], + ['next-token', this.nextToken], + ]); + } + + static fromEncodingData(data: unknown): BlockHeadersResponse { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded BlockHeadersResponse: ${data}`); + } + return new BlockHeadersResponse({ + blocks: (data.get('blocks') ?? []).map((v: unknown) => + Block.fromEncodingData(v) + ), + currentRound: data.get('current-round'), + nextToken: data.get('next-token'), + }); + } +} + /** * Fields relating to rewards, */ @@ -4256,6 +4347,135 @@ export class HashFactory implements Encodable { } } +/** + * (hbprf) HbProof is a signature using HeartbeatAddress's partkey, thereby showing + * it is online. + */ +export class HbProofFields implements Encodable { + private static encodingSchemaValue: Schema | undefined; + + static get encodingSchema(): Schema { + if (!this.encodingSchemaValue) { + this.encodingSchemaValue = new NamedMapSchema([]); + (this.encodingSchemaValue as NamedMapSchema).pushEntries( + { + key: 'hb-pk', + valueSchema: new OptionalSchema(new ByteArraySchema()), + omitEmpty: true, + }, + { + key: 'hb-pk1sig', + valueSchema: new OptionalSchema(new ByteArraySchema()), + omitEmpty: true, + }, + { + key: 'hb-pk2', + valueSchema: new OptionalSchema(new ByteArraySchema()), + omitEmpty: true, + }, + { + key: 'hb-pk2sig', + valueSchema: new OptionalSchema(new ByteArraySchema()), + omitEmpty: true, + }, + { + key: 'hb-sig', + valueSchema: new OptionalSchema(new ByteArraySchema()), + omitEmpty: true, + } + ); + } + return this.encodingSchemaValue; + } + + /** + * (p) Public key of the heartbeat message. + */ + public hbPk?: Uint8Array; + + /** + * (p1s) Signature of OneTimeSignatureSubkeyOffsetID(PK, Batch, Offset) under the + * key PK2. + */ + public hbPk1sig?: Uint8Array; + + /** + * (p2) Key for new-style two-level ephemeral signature. + */ + public hbPk2?: Uint8Array; + + /** + * (p2s) Signature of OneTimeSignatureSubkeyBatchID(PK2, Batch) under the master + * key (OneTimeSignatureVerifier). + */ + public hbPk2sig?: Uint8Array; + + /** + * (s) Signature of the heartbeat message. + */ + public hbSig?: Uint8Array; + + /** + * Creates a new `HbProofFields` object. + * @param hbPk - (p) Public key of the heartbeat message. + * @param hbPk1sig - (p1s) Signature of OneTimeSignatureSubkeyOffsetID(PK, Batch, Offset) under the + * key PK2. + * @param hbPk2 - (p2) Key for new-style two-level ephemeral signature. + * @param hbPk2sig - (p2s) Signature of OneTimeSignatureSubkeyBatchID(PK2, Batch) under the master + * key (OneTimeSignatureVerifier). + * @param hbSig - (s) Signature of the heartbeat message. + */ + constructor({ + hbPk, + hbPk1sig, + hbPk2, + hbPk2sig, + hbSig, + }: { + hbPk?: string | Uint8Array; + hbPk1sig?: string | Uint8Array; + hbPk2?: string | Uint8Array; + hbPk2sig?: string | Uint8Array; + hbSig?: string | Uint8Array; + }) { + this.hbPk = typeof hbPk === 'string' ? base64ToBytes(hbPk) : hbPk; + this.hbPk1sig = + typeof hbPk1sig === 'string' ? base64ToBytes(hbPk1sig) : hbPk1sig; + this.hbPk2 = typeof hbPk2 === 'string' ? base64ToBytes(hbPk2) : hbPk2; + this.hbPk2sig = + typeof hbPk2sig === 'string' ? base64ToBytes(hbPk2sig) : hbPk2sig; + this.hbSig = typeof hbSig === 'string' ? base64ToBytes(hbSig) : hbSig; + } + + // eslint-disable-next-line class-methods-use-this + getEncodingSchema(): Schema { + return HbProofFields.encodingSchema; + } + + toEncodingData(): Map { + return new Map([ + ['hb-pk', this.hbPk], + ['hb-pk1sig', this.hbPk1sig], + ['hb-pk2', this.hbPk2], + ['hb-pk2sig', this.hbPk2sig], + ['hb-sig', this.hbSig], + ]); + } + + static fromEncodingData(data: unknown): HbProofFields { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded HbProofFields: ${data}`); + } + return new HbProofFields({ + hbPk: data.get('hb-pk'), + hbPk1sig: data.get('hb-pk1sig'), + hbPk2: data.get('hb-pk2'), + hbPk2sig: data.get('hb-pk2sig'), + hbSig: data.get('hb-sig'), + }); + } +} + /** * A health check response. */ @@ -5890,6 +6110,11 @@ export class Transaction implements Encodable { valueSchema: new OptionalSchema(new ByteArraySchema()), omitEmpty: true, }, + { + key: 'heartbeat-transaction', + valueSchema: new OptionalSchema(TransactionHeartbeat.encodingSchema), + omitEmpty: true, + }, { key: 'id', valueSchema: new OptionalSchema(new StringSchema()), @@ -6087,6 +6312,13 @@ export class Transaction implements Encodable { */ public group?: Uint8Array; + /** + * Fields for a heartbeat transaction. + * Definition: + * data/transactions/heartbeat.go : HeartbeatTxnFields + */ + public heartbeatTransaction?: TransactionHeartbeat; + /** * Transaction ID */ @@ -6187,6 +6419,7 @@ export class Transaction implements Encodable { * * (afrz) asset-freeze-transaction * * (appl) application-transaction * * (stpf) state-proof-transaction + * * (hb) heartbeat-transaction */ public txType?: string; @@ -6226,6 +6459,9 @@ export class Transaction implements Encodable { * @param group - (grp) Base64 encoded byte array of a sha512/256 digest. When present indicates * that this transaction is part of a transaction group and the value is the * sha512/256 hash of the transactions in that group. + * @param heartbeatTransaction - Fields for a heartbeat transaction. + * Definition: + * data/transactions/heartbeat.go : HeartbeatTxnFields * @param id - Transaction ID * @param innerTxns - Inner transactions produced by application execution. * @param intraRoundOffset - Offset into the round where this transaction was confirmed. @@ -6265,6 +6501,7 @@ export class Transaction implements Encodable { * * (afrz) asset-freeze-transaction * * (appl) application-transaction * * (stpf) state-proof-transaction + * * (hb) heartbeat-transaction */ constructor({ fee, @@ -6285,6 +6522,7 @@ export class Transaction implements Encodable { genesisId, globalStateDelta, group, + heartbeatTransaction, id, innerTxns, intraRoundOffset, @@ -6320,6 +6558,7 @@ export class Transaction implements Encodable { genesisId?: string; globalStateDelta?: EvalDeltaKeyValue[]; group?: string | Uint8Array; + heartbeatTransaction?: TransactionHeartbeat; id?: string; innerTxns?: Transaction[]; intraRoundOffset?: number | bigint; @@ -6374,6 +6613,7 @@ export class Transaction implements Encodable { this.genesisId = genesisId; this.globalStateDelta = globalStateDelta; this.group = typeof group === 'string' ? base64ToBytes(group) : group; + this.heartbeatTransaction = heartbeatTransaction; this.id = id; this.innerTxns = innerTxns; this.intraRoundOffset = @@ -6460,6 +6700,12 @@ export class Transaction implements Encodable { : undefined, ], ['group', this.group], + [ + 'heartbeat-transaction', + typeof this.heartbeatTransaction !== 'undefined' + ? this.heartbeatTransaction.toEncodingData() + : undefined, + ], ['id', this.id], [ 'inner-txns', @@ -6562,6 +6808,12 @@ export class Transaction implements Encodable { .map((v: unknown) => EvalDeltaKeyValue.fromEncodingData(v)) : undefined, group: data.get('group'), + heartbeatTransaction: + typeof data.get('heartbeat-transaction') !== 'undefined' + ? TransactionHeartbeat.fromEncodingData( + data.get('heartbeat-transaction') + ) + : undefined, id: data.get('id'), innerTxns: typeof data.get('inner-txns') !== 'undefined' @@ -7237,6 +7489,129 @@ export class TransactionAssetTransfer implements Encodable { } } +/** + * Fields for a heartbeat transaction. + * Definition: + * data/transactions/heartbeat.go : HeartbeatTxnFields + */ +export class TransactionHeartbeat implements Encodable { + private static encodingSchemaValue: Schema | undefined; + + static get encodingSchema(): Schema { + if (!this.encodingSchemaValue) { + this.encodingSchemaValue = new NamedMapSchema([]); + (this.encodingSchemaValue as NamedMapSchema).pushEntries( + { key: 'hb-address', valueSchema: new StringSchema(), omitEmpty: true }, + { + key: 'hb-key-dilution', + valueSchema: new Uint64Schema(), + omitEmpty: true, + }, + { + key: 'hb-proof', + valueSchema: HbProofFields.encodingSchema, + omitEmpty: true, + }, + { key: 'hb-seed', valueSchema: new ByteArraySchema(), omitEmpty: true }, + { + key: 'hb-vote-id', + valueSchema: new ByteArraySchema(), + omitEmpty: true, + } + ); + } + return this.encodingSchemaValue; + } + + /** + * (hbad) HbAddress is the account this txn is proving onlineness for. + */ + public hbAddress: string; + + /** + * (hbkd) HbKeyDilution must match HbAddress account's current KeyDilution. + */ + public hbKeyDilution: bigint; + + /** + * (hbprf) HbProof is a signature using HeartbeatAddress's partkey, thereby showing + * it is online. + */ + public hbProof: HbProofFields; + + /** + * (hbsd) HbSeed must be the block seed for the this transaction's firstValid + * block. + */ + public hbSeed: Uint8Array; + + /** + * (hbvid) HbVoteID must match the HbAddress account's current VoteID. + */ + public hbVoteId: Uint8Array; + + /** + * Creates a new `TransactionHeartbeat` object. + * @param hbAddress - (hbad) HbAddress is the account this txn is proving onlineness for. + * @param hbKeyDilution - (hbkd) HbKeyDilution must match HbAddress account's current KeyDilution. + * @param hbProof - (hbprf) HbProof is a signature using HeartbeatAddress's partkey, thereby showing + * it is online. + * @param hbSeed - (hbsd) HbSeed must be the block seed for the this transaction's firstValid + * block. + * @param hbVoteId - (hbvid) HbVoteID must match the HbAddress account's current VoteID. + */ + constructor({ + hbAddress, + hbKeyDilution, + hbProof, + hbSeed, + hbVoteId, + }: { + hbAddress: string; + hbKeyDilution: number | bigint; + hbProof: HbProofFields; + hbSeed: string | Uint8Array; + hbVoteId: string | Uint8Array; + }) { + this.hbAddress = hbAddress; + this.hbKeyDilution = ensureBigInt(hbKeyDilution); + this.hbProof = hbProof; + this.hbSeed = typeof hbSeed === 'string' ? base64ToBytes(hbSeed) : hbSeed; + this.hbVoteId = + typeof hbVoteId === 'string' ? base64ToBytes(hbVoteId) : hbVoteId; + } + + // eslint-disable-next-line class-methods-use-this + getEncodingSchema(): Schema { + return TransactionHeartbeat.encodingSchema; + } + + toEncodingData(): Map { + return new Map([ + ['hb-address', this.hbAddress], + ['hb-key-dilution', this.hbKeyDilution], + ['hb-proof', this.hbProof.toEncodingData()], + ['hb-seed', this.hbSeed], + ['hb-vote-id', this.hbVoteId], + ]); + } + + static fromEncodingData(data: unknown): TransactionHeartbeat { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded TransactionHeartbeat: ${data}`); + } + return new TransactionHeartbeat({ + hbAddress: data.get('hb-address'), + hbKeyDilution: data.get('hb-key-dilution'), + hbProof: HbProofFields.fromEncodingData( + data.get('hb-proof') ?? new Map() + ), + hbSeed: data.get('hb-seed'), + hbVoteId: data.get('hb-vote-id'), + }); + } +} + /** * Fields for a keyreg transaction. * Definition: From d786559f4322978d76736c6077921f0949802146 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:51:22 -0500 Subject: [PATCH 2/3] Incentives: Support heartbeat transaction (#915) --- src/heartbeat.ts | 168 +++++++++++++++++++++++++++++++++ src/transaction.ts | 46 +++++++++ src/types/transactions/base.ts | 44 ++++++++- tests/5.Transaction.ts | 24 +++++ tests/cucumber/steps/steps.js | 44 ++++++++- tests/cucumber/unit.tags | 3 + 6 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 src/heartbeat.ts diff --git a/src/heartbeat.ts b/src/heartbeat.ts new file mode 100644 index 000000000..90404473b --- /dev/null +++ b/src/heartbeat.ts @@ -0,0 +1,168 @@ +import { Address } from './encoding/address.js'; +import { Encodable, Schema } from './encoding/encoding.js'; +import { + AddressSchema, + Uint64Schema, + ByteArraySchema, + FixedLengthByteArraySchema, + NamedMapSchema, + allOmitEmpty, +} from './encoding/schema/index.js'; + +export class HeartbeatProof implements Encodable { + public static readonly encodingSchema = new NamedMapSchema( + allOmitEmpty([ + { + key: 's', // Sig + valueSchema: new FixedLengthByteArraySchema(64), + }, + { + key: 'p', // PK + valueSchema: new FixedLengthByteArraySchema(32), + }, + { + key: 'p2', // PK2 + valueSchema: new FixedLengthByteArraySchema(32), + }, + { + key: 'p1s', // PK1Sig + valueSchema: new FixedLengthByteArraySchema(64), + }, + { + key: 'p2s', // PK2Sig + valueSchema: new FixedLengthByteArraySchema(64), + }, + ]) + ); + + public sig: Uint8Array; + + public pk: Uint8Array; + + public pk2: Uint8Array; + + public pk1Sig: Uint8Array; + + public pk2Sig: Uint8Array; + + public constructor(params: { + sig: Uint8Array; + pk: Uint8Array; + pk2: Uint8Array; + pk1Sig: Uint8Array; + pk2Sig: Uint8Array; + }) { + this.sig = params.sig; + this.pk = params.pk; + this.pk2 = params.pk2; + this.pk1Sig = params.pk1Sig; + this.pk2Sig = params.pk2Sig; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return HeartbeatProof.encodingSchema; + } + + public toEncodingData(): Map { + return new Map([ + ['s', this.sig], + ['p', this.pk], + ['p2', this.pk2], + ['p1s', this.pk1Sig], + ['p2s', this.pk2Sig], + ]); + } + + public static fromEncodingData(data: unknown): HeartbeatProof { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded HeartbeatProof: ${data}`); + } + return new HeartbeatProof({ + sig: data.get('s'), + pk: data.get('p'), + pk2: data.get('p2'), + pk1Sig: data.get('p1s'), + pk2Sig: data.get('p2s'), + }); + } +} + +export class Heartbeat implements Encodable { + public static readonly encodingSchema = new NamedMapSchema( + allOmitEmpty([ + { + key: 'a', // HbAddress + valueSchema: new AddressSchema(), + }, + { + key: 'prf', // HbProof + valueSchema: HeartbeatProof.encodingSchema, + }, + { + key: 'sd', // HbSeed + valueSchema: new ByteArraySchema(), + }, + { + key: 'vid', // HbVoteID + valueSchema: new FixedLengthByteArraySchema(32), + }, + { + key: 'kd', // HbKeyDilution + valueSchema: new Uint64Schema(), + }, + ]) + ); + + public address: Address; + + public proof: HeartbeatProof; + + public seed: Uint8Array; + + public voteID: Uint8Array; + + public keyDilution: bigint; + + public constructor(params: { + address: Address; + proof: HeartbeatProof; + seed: Uint8Array; + voteID: Uint8Array; + keyDilution: bigint; + }) { + this.address = params.address; + this.proof = params.proof; + this.seed = params.seed; + this.voteID = params.voteID; + this.keyDilution = params.keyDilution; + } + + // eslint-disable-next-line class-methods-use-this + public getEncodingSchema(): Schema { + return Heartbeat.encodingSchema; + } + + public toEncodingData(): Map { + return new Map([ + ['a', this.address], + ['prf', this.proof.toEncodingData()], + ['sd', this.seed], + ['vid', this.voteID], + ['kd', this.keyDilution], + ]); + } + + public static fromEncodingData(data: unknown): Heartbeat { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded Heartbeat: ${data}`); + } + return new Heartbeat({ + address: data.get('a'), + proof: HeartbeatProof.fromEncodingData(data.get('prf')), + seed: data.get('sd'), + voteID: data.get('vid'), + keyDilution: data.get('kd'), + }); + } +} diff --git a/src/transaction.ts b/src/transaction.ts index b27b63617..bd9864ec2 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -30,8 +30,10 @@ import { KeyRegistrationTransactionParams, ApplicationCallTransactionParams, StateProofTransactionParams, + HeartbeatTransactionParams, } from './types/transactions/base.js'; import { StateProof, StateProofMessage } from './stateproof.js'; +import { Heartbeat, HeartbeatProof } from './heartbeat.js'; import * as utils from './utils/utils.js'; const ALGORAND_TRANSACTION_LENGTH = 52; @@ -248,6 +250,14 @@ export interface StateProofTransactionFields { readonly message?: StateProofMessage; } +export interface HeartbeatTransactionFields { + readonly address: Address; + readonly proof: HeartbeatProof; + readonly seed: Uint8Array; + readonly voteID: Uint8Array; + readonly keyDilution: bigint; +} + /** * Transaction enables construction of Algorand transactions * */ @@ -438,6 +448,8 @@ export class Transaction implements encoding.Encodable { key: 'spmsg', valueSchema: new OptionalSchema(StateProofMessage.encodingSchema), }, + // Heartbeat + { key: 'hb', valueSchema: new OptionalSchema(Heartbeat.encodingSchema) }, ]) ); @@ -466,6 +478,7 @@ export class Transaction implements encoding.Encodable { public readonly assetFreeze?: AssetFreezeTransactionFields; public readonly applicationCall?: ApplicationTransactionFields; public readonly stateProof?: StateProofTransactionFields; + public readonly heartbeat?: HeartbeatTransactionFields; constructor(params: TransactionParams) { if (!isTransactionType(params.type)) { @@ -506,6 +519,7 @@ export class Transaction implements encoding.Encodable { if (params.assetFreezeParams) fieldsPresent.push(TransactionType.afrz); if (params.appCallParams) fieldsPresent.push(TransactionType.appl); if (params.stateProofParams) fieldsPresent.push(TransactionType.stpf); + if (params.heartbeatParams) fieldsPresent.push(TransactionType.hb); if (fieldsPresent.length !== 1) { throw new Error( @@ -701,6 +715,16 @@ export class Transaction implements encoding.Encodable { }; } + if (params.heartbeatParams) { + this.heartbeat = new Heartbeat({ + address: params.heartbeatParams.address, + proof: params.heartbeatParams.proof, + seed: params.heartbeatParams.seed, + voteID: params.heartbeatParams.voteID, + keyDilution: params.heartbeatParams.keyDilution, + }); + } + // Determine fee this.fee = utils.ensureUint64(params.suggestedParams.fee); @@ -842,6 +866,18 @@ export class Transaction implements encoding.Encodable { return data; } + if (this.heartbeat) { + const heartbeat = new Heartbeat({ + address: this.heartbeat.address, + proof: this.heartbeat.proof, + seed: this.heartbeat.seed, + voteID: this.heartbeat.voteID, + keyDilution: this.heartbeat.keyDilution, + }); + data.set('hb', heartbeat.toEncodingData()); + return data; + } + throw new Error(`Unexpected transaction type: ${this.type}`); } @@ -1006,6 +1042,16 @@ export class Transaction implements encoding.Encodable { : undefined, }; params.stateProofParams = stateProofParams; + } else if (params.type === TransactionType.hb) { + const heartbeat = Heartbeat.fromEncodingData(data.get('hb')); + const heartbeatParams: HeartbeatTransactionParams = { + address: heartbeat.address, + proof: heartbeat.proof, + seed: heartbeat.seed, + voteID: heartbeat.voteID, + keyDilution: heartbeat.keyDilution, + }; + params.heartbeatParams = heartbeatParams; } else { const exhaustiveCheck: never = params.type; throw new Error(`Unexpected transaction type: ${exhaustiveCheck}`); diff --git a/src/types/transactions/base.ts b/src/types/transactions/base.ts index 277c9a3dd..577bab228 100644 --- a/src/types/transactions/base.ts +++ b/src/types/transactions/base.ts @@ -1,5 +1,6 @@ import { Address } from '../../encoding/address.js'; import { StateProof, StateProofMessage } from '../../stateproof.js'; +import { HeartbeatProof } from '../../heartbeat.js'; /** * Enum for application transaction types. @@ -38,6 +39,11 @@ export enum TransactionType { * State proof transaction */ stpf = 'stpf', + + /** + * Heartbeat transaction + */ + hb = 'hb', } /** @@ -53,7 +59,8 @@ export function isTransactionType(s: string): s is TransactionType { s === TransactionType.axfer || s === TransactionType.afrz || s === TransactionType.appl || - s === TransactionType.stpf + s === TransactionType.stpf || + s === TransactionType.hb ); } @@ -466,6 +473,36 @@ export interface StateProofTransactionParams { message?: StateProofMessage; } +/** + * Contains heartbeat transaction parameters. + */ +export interface HeartbeatTransactionParams { + /* + * Account address this txn is proving onlineness for + */ + address: Address; + + /** + * Signature using HeartbeatAddress's partkey, thereby showing it is online. + */ + proof: HeartbeatProof; + + /** + * The block seed for the this transaction's firstValid block. + */ + seed: Uint8Array; + + /** + * Must match the hbAddress account's current VoteID + */ + voteID: Uint8Array; + + /** + * Must match hbAddress account's current KeyDilution. + */ + keyDilution: bigint; +} + /** * A full list of all available transaction parameters * @@ -540,4 +577,9 @@ export interface TransactionParams { * State proof transaction parameters. Only set if type is TransactionType.stpf */ stateProofParams?: StateProofTransactionParams; + + /** + * Heartbeat transaction parameters. Only set if type is TransactionType.hb + */ + heartbeatParams?: HeartbeatTransactionParams; } diff --git a/tests/5.Transaction.ts b/tests/5.Transaction.ts index a516a1466..4edb45f34 100644 --- a/tests/5.Transaction.ts +++ b/tests/5.Transaction.ts @@ -1158,6 +1158,30 @@ describe('Sign', () => { assert.deepStrictEqual(reencRep, encRep); }); + it('should correctly serialize and deserialize heartbeat transaction', () => { + const golden = algosdk.base64ToBytes( + 'gqRsc2lngaFsxAYLMSAyAxKjdHhuhqJmdmqiZ2jEIP9SQzAGyec/v8omzEOW3/GIM+a7bvPaU5D/ohX7qjFtomhihaFhxCBsU6oqjVx2U65owbsX9/6N7/YCmul+O3liZ0fO2L75/KJrZGSjcHJmhaFwxCAM1TyIrIbgm+yPLT9so6VDI3rKl33t4c4RSGJv6G12eaNwMXPEQBETln14zJzQ1Mb/SNjmDNl0fyQ4DPBQZML8iTEbhqBj+YDAgpNSEduWj7OuVkCSQMq4N/Er/+2HfKUHu//spgOicDLEIB9c5n7WgG+5aOdjfBmuxH3z4TYiQzDVYKjBLhv4IkNfo3Ayc8RAeKpQ+o/GJyGCH0I4f9luN0i7BPXlMlaJAuXLX5Ng8DTN0vtZtztjqYfkwp1cVOYPu+Fce3aIdJHVoUDaJaMIDqFzxEBQN41y5zAZhYHQWf2wWF6CGboqQk6MxDcQ76zXHvVtzrAPUWXZDt4IB8Ha1z+54Hc6LmEoG090pk0IYs+jLN8HonNkxCCPVPjiD5O7V0c3P/SVsHmED7slwllta7c92WiKwnvgoqN2aWTEIHBy8sOi/V0YKXJw8VtW40MbqhtUyO9HC9m/haf84xiGomx2dKNzbmTEIDAp2wPDnojyy8tTgb3sMH++26D5+l7nHZmyRvzFfLsOpHR5cGWiaGI=' + ); + + const decTxn = algosdk.decodeMsgpack(golden, algosdk.SignedTransaction); + const prepTxn = algosdk.SignedTransaction.encodingSchema.prepareMsgpack( + decTxn.toEncodingData() + ); + assert.ok(prepTxn instanceof Map && prepTxn.has('txn')); + + const reencRep = algosdk.encodeMsgpack(decTxn); + assert.deepStrictEqual(reencRep, golden); + const hbAddress = + 'NRJ2UKUNLR3FHLTIYG5RP576RXX7MAU25F7DW6LCM5D45WF67H6EFQMWNM'; + + assert.deepStrictEqual(decTxn.txn.type, algosdk.TransactionType.hb); + assert.deepStrictEqual( + decTxn.txn.heartbeat?.address.toString(), + hbAddress + ); + assert.deepStrictEqual(decTxn.txn.heartbeat?.keyDilution, 100n); + }); + it('reserializes correctly no genesis ID', () => { const expectedTxn = new algosdk.Transaction({ type: algosdk.TransactionType.pay, diff --git a/tests/cucumber/steps/steps.js b/tests/cucumber/steps/steps.js index 2efa21dd6..926525436 100644 --- a/tests/cucumber/steps/steps.js +++ b/tests/cucumber/steps/steps.js @@ -2297,7 +2297,17 @@ module.exports = function getSteps(options) { let anyBlockResponse; When('we make any Get Block call', async function () { - anyBlockResponse = await doOrDoRaw(this.v2Client.block(1)); + const req = this.v2Client.block(1); + if (responseFormat === 'json') { + // for json responses, we need to set the format query param and provide a custom decoder + // because the default block request only supports msgpack + req.query.format = responseFormat; + req.prepare = (response) => { + const body = new TextDecoder().decode(response.body); + return algosdk.decodeJSON(body, algosdk.modelsv2.BlockResponse); + }; + } + anyBlockResponse = await doOrDoRaw(req); }); Then( @@ -2314,6 +2324,19 @@ module.exports = function getSteps(options) { } ); + Then( + 'the parsed Get Block response should have heartbeat address {string}', + (hbAddress) => { + assert.ok( + anyBlockResponse.block.payset[0].signedTxn.signedTxn.txn.heartbeat + .address instanceof algosdk.Address + ); + const hbAddressString = + anyBlockResponse.block.payset[0].signedTxn.signedTxn.txn.heartbeat.address.toString(); + assert.strictEqual(hbAddress, hbAddressString); + } + ); + let anySuggestedTransactionsResponse; When('we make any Suggested Transaction Parameters call', async function () { @@ -3071,6 +3094,25 @@ module.exports = function getSteps(options) { } ); + Then( + 'the parsed SearchForTransactions response should be valid on round {int} and the array should be of len {int} and the element at index {int} should have hbaddress {string}', + (round, length, idx, hbAddress) => { + assert.strictEqual( + anySearchForTransactionsResponse.currentRound, + BigInt(round) + ); + assert.strictEqual( + anySearchForTransactionsResponse.transactions.length, + length + ); + assert.strictEqual( + anySearchForTransactionsResponse.transactions[idx].heartbeatTransaction + .hbAddress, + hbAddress + ); + } + ); + let anySearchForAssetsResponse; When('we make any SearchForAssets call', async function () { diff --git a/tests/cucumber/unit.tags b/tests/cucumber/unit.tags index 48232f779..c58157c40 100644 --- a/tests/cucumber/unit.tags +++ b/tests/cucumber/unit.tags @@ -2,6 +2,8 @@ @unit.abijson.byname @unit.algod @unit.algod.ledger_refactoring +@unit.algod.heartbeat +@unit.algod.heartbeat.msgp @unit.applications @unit.applications.boxes @unit.atomic_transaction_composer @@ -13,6 +15,7 @@ @unit.indexer @unit.indexer.ledger_refactoring @unit.indexer.logs +@unit.indexer.heartbeat @unit.offline @unit.program_sanity_check @unit.ready From f9132213abd975ac9038ed7ed360a6f0493816e9 Mon Sep 17 00:00:00 2001 From: gmalouf Date: Wed, 15 Jan 2025 17:33:27 +0000 Subject: [PATCH 3/3] bump up version to v3.1.0 --- CHANGELOG.md | 16 ++++++++++++++++ README.md | 8 ++++---- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80715847c..78bd822dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# v3.1.0 + + + +## What's Changed + +### Enhancements + +- Incentives: Support heartbeat transaction by @algorandskiy in https://github.com/algorand/js-algorand-sdk/pull/915 + +### Other + +- Regenerate code with the latest specification file (df06f8b1) by @github-actions in https://github.com/algorand/js-algorand-sdk/pull/914 + +**Full Changelog**: https://github.com/algorand/js-algorand-sdk/compare/v3.0.0...v3.1.0 + # v3.0.0 diff --git a/README.md b/README.md index 66c66ab5c..8e9a41256 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ Include a minified browser bundle directly in your HTML like so: ```html ``` @@ -34,8 +34,8 @@ or ```html ``` diff --git a/package-lock.json b/package-lock.json index 560544963..ffa52c5d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "algosdk", - "version": "3.0.0", + "version": "3.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "algosdk", - "version": "3.0.0", + "version": "3.1.0", "license": "MIT", "dependencies": { "algorand-msgpack": "^1.1.0", diff --git a/package.json b/package.json index 8ea2e7c09..a2bc181a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "algosdk", - "version": "3.0.0", + "version": "3.1.0", "description": "The official JavaScript SDK for Algorand", "main": "dist/cjs/index.js", "module": "dist/esm/index.js",