diff --git a/.gitignore b/.gitignore index 5ac92e3bd..47d5bc3d7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ temp # Environment information examples/.env +test-harness/ tests/cucumber/features/ tests/cucumber/browser/build tests/browser/bundle.* diff --git a/.test-env b/.test-env new file mode 100644 index 000000000..eff6690c0 --- /dev/null +++ b/.test-env @@ -0,0 +1,14 @@ +# Configs for testing repo download: +SDK_TESTING_URL="https://github.com/algorand/algorand-sdk-testing" +SDK_TESTING_BRANCH="feature/box-storage" +SDK_TESTING_HARNESS="test-harness" + +VERBOSE_HARNESS=0 + +# WARNING: If set to 1, new features will be LOST when downloading the test harness. +# REGARDLESS: modified features are ALWAYS overwritten. +REMOVE_LOCAL_FEATURES=0 + +# WARNING: Be careful when turning on the next variable. +# In that case you'll need to provide all variables expected by `algorand-sdk-testing`'s `.env` +OVERWRITE_TESTING_ENVIRONMENT=0 diff --git a/CHANGELOG.md b/CHANGELOG.md index e0b1167a1..d0059da0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# v1.19.1 + +### Enhancements + +- API: Support attaching signatures to standard and multisig transactions by @jdtzmn in https://github.com/algorand/js-algorand-sdk/pull/595 +- AVM: Consolidate TEAL and AVM versions by @michaeldiamant in https://github.com/algorand/js-algorand-sdk/pull/609 +- Testing: Use Dev mode network for cucumber tests by @algochoi in https://github.com/algorand/js-algorand-sdk/pull/614 + # v1.19.0 ## What's Changed diff --git a/Makefile b/Makefile index d33c1b011..6f9bb461a 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,28 @@ +UNIT_TAGS := "$(subst :, or ,$(shell awk '{print $2}' tests/cucumber/unit.tags | paste -s -d: -))" +INTEGRATIONS_TAGS := "$(subst :, or ,$(shell awk '{print $2}' tests/cucumber/integration.tags | paste -s -d: -))" + unit: - node_modules/.bin/cucumber-js --tags "@unit.offline or @unit.algod or @unit.indexer or @unit.rekey or @unit.tealsign or @unit.dryrun or @unit.applications or @unit.applications.boxes or @unit.responses or @unit.transactions or @unit.transactions.keyreg or @unit.transactions.payment or @unit.responses.231 or @unit.feetest or @unit.indexer.logs or @unit.abijson or @unit.abijson.byname or @unit.atomic_transaction_composer or @unit.responses.unlimited_assets or @unit.indexer.ledger_refactoring or @unit.algod.ledger_refactoring or @unit.dryrun.trace.application or @unit.sourcemap" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js + node_modules/.bin/cucumber-js --tags $(UNIT_TAGS) tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js integration: - node_modules/.bin/cucumber-js --tags "@algod or @assets or @auction or @kmd or @send or @indexer or @rekey_v1 or @send.keyregtxn or @dryrun or @compile or @applications or @indexer.applications or @applications.verified or @applications.boxes or @indexer.231 or @abi or @c2c or @compile.sourcemap" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js + node_modules/.bin/cucumber-js --tags $(INTEGRATIONS_TAGS) tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js + +# The following assumes that all cucumber steps are defined in `./tests/cucumber/steps/steps.js` and begin past line 135 of that file. +# Please note any deviations of the above before presuming correctness. +display-all-js-steps: + tail -n +135 tests/cucumber/steps/steps.js | grep -v '^ *//' | awk "/(Given|Then|When)/,/',/" | grep -E "\'.+\'" | sed "s/^[^']*'\([^']*\)'.*/\1/g" + +harness: + ./test-harness.sh + +docker-build: + docker build -t js-sdk-testing -f tests/cucumber/docker/Dockerfile $(CURDIR) --build-arg TEST_BROWSER --build-arg CI=true + +docker-run: + docker ps -a + docker run -it --network host js-sdk-testing:latest -docker-test: - ./tests/cucumber/docker/run_docker.sh +docker-test: harness docker-build docker-run format: npm run format diff --git a/README.md b/README.md index d28deac83..24bd04c7a 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ Include a minified browser bundle directly in your HTML like so: ```html ``` @@ -32,8 +32,8 @@ or ```html ``` diff --git a/package-lock.json b/package-lock.json index 95eab649b..f533394fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "algosdk", - "version": "1.19.0", + "version": "1.19.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "algosdk", - "version": "1.19.0", + "version": "1.19.1", "license": "MIT", "dependencies": { "algo-msgpack-with-bigint": "^2.1.1", diff --git a/package.json b/package.json index 9648348b2..7a3234197 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "algosdk", - "version": "1.19.0", + "version": "1.19.1", "description": "The official JavaScript SDK for Algorand", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/src/client/v2/algod/algod.ts b/src/client/v2/algod/algod.ts index dbe15ca6c..df0b86a19 100644 --- a/src/client/v2/algod/algod.ts +++ b/src/client/v2/algod/algod.ts @@ -6,6 +6,7 @@ import AccountApplicationInformation from './accountApplicationInformation'; import Block from './block'; import Compile from './compile'; import Dryrun from './dryrun'; +import Genesis from './genesis'; import GetAssetByID from './getAssetByID'; import GetApplicationByID from './getApplicationByID'; import GetApplicationBoxByName from './getApplicationBoxByName'; @@ -14,19 +15,20 @@ import HealthCheck from './healthCheck'; import PendingTransactionInformation from './pendingTransactionInformation'; import PendingTransactions from './pendingTransactions'; import PendingTransactionsByAddress from './pendingTransactionsByAddress'; +import GetTransactionProof from './getTransactionProof'; import SendRawTransaction from './sendRawTransaction'; import Status from './status'; import StatusAfterBlock from './statusAfterBlock'; import SuggestedParams from './suggestedParams'; import Supply from './supply'; import Versions from './versions'; -import Genesis from './genesis'; -import Proof from './proof'; import { BaseHTTPClient } from '../../baseHTTPClient'; import { AlgodTokenHeader, CustomTokenHeader, } from '../../urlTokenBaseHTTPClient'; +import LightBlockHeaderProof from './lightBlockHeaderProof'; +import StateProof from './stateproof'; /** * Algod client connects an application to the Algorand blockchain. The algod client requires a valid algod REST endpoint IP address and algod token from an Algorand node that is connected to the network you plan to interact with. @@ -488,7 +490,7 @@ export default class AlgodClient extends ServiceClient { * ```typescript * const round = 18038133; * const txId = "MEUOC4RQJB23CQZRFRKYEI6WBO73VTTPST5A7B3S5OKBUY6LFUDA"; - * const proof = await algodClient.getProof(round, txId).do(); + * const proof = await algodClient.getTransactionProof(round, txId).do(); * ``` * * [Response data schema details](https://developer.algorand.org/docs/rest-apis/algod/v2/#get-v2blocksroundtransactionstxidproof) @@ -496,7 +498,39 @@ export default class AlgodClient extends ServiceClient { * @param txID - The transaction ID for which to generate a proof. * @category GET */ - getProof(round: number, txID: string) { - return new Proof(this.c, this.intDecoding, round, txID); + getTransactionProof(round: number, txID: string) { + return new GetTransactionProof(this.c, this.intDecoding, round, txID); + } + + /** + * Gets a proof for a given light block header inside a state proof commitment. + * + * #### Example + * ```typescript + * const round = 11111111; + * const lightBlockHeaderProof = await algodClient.getLightBlockHeaderProof(round).do(); + * ``` + * + * [Response data schema details](https://developer.algorand.org/docs/rest-apis/algod/v2#get-v2blocksroundlightheaderproof) + * @param round + */ + getLightBlockHeaderProof(round: number) { + return new LightBlockHeaderProof(this.c, this.intDecoding, round); + } + + /** + * Gets a state proof that covers a given round. + * + * #### Example + * ```typescript + * const round = 11111111; + * const stateProof = await algodClient.getStateProof(round).do(); + * ``` + * + * [Response data schema details](https://developer.algorand.org/docs/rest-apis/algod/v2#get-v2stateproofsround) + * @param round + */ + getStateProof(round: number) { + return new StateProof(this.c, this.intDecoding, round); } } diff --git a/src/client/v2/algod/getTransactionProof.ts b/src/client/v2/algod/getTransactionProof.ts new file mode 100644 index 000000000..398039a43 --- /dev/null +++ b/src/client/v2/algod/getTransactionProof.ts @@ -0,0 +1,43 @@ +import JSONRequest from '../jsonrequest'; +import HTTPClient from '../../client'; +import IntDecoding from '../../../types/intDecoding'; + +export default class GetTransactionProof extends JSONRequest { + constructor( + c: HTTPClient, + intDecoding: IntDecoding, + private round: number, + private txID: string + ) { + super(c, intDecoding); + + this.round = round; + this.txID = txID; + } + + path() { + return `/v2/blocks/${this.round}/transactions/${this.txID}/proof`; + } + + /** + * Exclude assets and application data from results + * The type of hash function used to create the proof, must be one of: "sha512_256", "sha256" + * + * #### Example + * ```typescript + * const hashType = "sha256"; + * const round = 123456; + * const txId = "abc123; + * const txProof = await algodClient.getTransactionProof(round, txId) + * .hashType(hashType) + * .do(); + * ``` + * + * @param hashType + * @category query + */ + hashType(hashType: string) { + this.query.hashtype = hashType; + return this; + } +} diff --git a/src/client/v2/algod/lightBlockHeaderProof.ts b/src/client/v2/algod/lightBlockHeaderProof.ts new file mode 100644 index 000000000..ac3794c36 --- /dev/null +++ b/src/client/v2/algod/lightBlockHeaderProof.ts @@ -0,0 +1,15 @@ +import JSONRequest from '../jsonrequest'; +import HTTPClient from '../../client'; +import IntDecoding from '../../../types/intDecoding'; + +export default class LightBlockHeaderProof extends JSONRequest { + constructor(c: HTTPClient, intDecoding: IntDecoding, private round: number) { + super(c, intDecoding); + + this.round = round; + } + + path() { + return `/v2/blocks/${this.round}/lightheader/proof`; + } +} diff --git a/src/client/v2/algod/models/types.ts b/src/client/v2/algod/models/types.ts index bdeeaa1e1..a5bdf4674 100644 --- a/src/client/v2/algod/models/types.ts +++ b/src/client/v2/algod/models/types.ts @@ -1714,6 +1714,54 @@ export class EvalDeltaKeyValue extends BaseModel { } } +/** + * Proof of membership and position of a light block header. + */ +export class LightBlockHeaderProof extends BaseModel { + /** + * The index of the light block header in the vector commitment tree + */ + public index: number | bigint; + + /** + * The encoded proof. + */ + public proof: Uint8Array; + + /** + * Represents the depth of the tree that is being proven, i.e. the number of edges + * from a leaf to the root. + */ + public treedepth: number | bigint; + + /** + * Creates a new `LightBlockHeaderProof` object. + * @param index - The index of the light block header in the vector commitment tree + * @param proof - The encoded proof. + * @param treedepth - Represents the depth of the tree that is being proven, i.e. the number of edges + * from a leaf to the root. + */ + constructor( + index: number | bigint, + proof: string | Uint8Array, + treedepth: number | bigint + ) { + super(); + this.index = index; + this.proof = + typeof proof === 'string' + ? new Uint8Array(Buffer.from(proof, 'base64')) + : proof; + this.treedepth = treedepth; + + this.attribute_map = { + index: 'index', + proof: 'proof', + treedepth: 'treedepth', + }; + } +} + /** * */ @@ -2121,80 +2169,112 @@ export class PostTransactionsResponse extends BaseModel { } /** - * Proof of transaction in a block. + * Represents a state proof and its corresponding message */ -export class ProofResponse extends BaseModel { +export class StateProof extends BaseModel { /** - * Index of the transaction in the block's payset. + * Represents the message that the state proofs are attesting to. */ - public idx: number | bigint; + public message: StateProofMessage; /** - * Merkle proof of transaction membership. + * The encoded StateProof for the message. */ - public proof: Uint8Array; + public stateproof: Uint8Array; /** - * Hash of SignedTxnInBlock for verifying proof. + * Creates a new `StateProof` object. + * @param message - Represents the message that the state proofs are attesting to. + * @param stateproof - The encoded StateProof for the message. */ - public stibhash: Uint8Array; + constructor(message: StateProofMessage, stateproof: string | Uint8Array) { + super(); + this.message = message; + this.stateproof = + typeof stateproof === 'string' + ? new Uint8Array(Buffer.from(stateproof, 'base64')) + : stateproof; + + this.attribute_map = { + message: 'Message', + stateproof: 'StateProof', + }; + } +} +/** + * Represents the message that the state proofs are attesting to. + */ +export class StateProofMessage extends BaseModel { /** - * Represents the depth of the tree that is being proven, i.e. the number of edges - * from a leaf to the root. + * The vector commitment root on all light block headers within a state proof + * interval. */ - public treedepth: number | bigint; + public blockheaderscommitment: Uint8Array; /** - * The type of hash function used to create the proof, must be one of: - * * sha512_256 - * * sha256 + * The first round the message attests to. */ - public hashtype?: string; + public firstattestedround: number | bigint; /** - * Creates a new `ProofResponse` object. - * @param idx - Index of the transaction in the block's payset. - * @param proof - Merkle proof of transaction membership. - * @param stibhash - Hash of SignedTxnInBlock for verifying proof. - * @param treedepth - Represents the depth of the tree that is being proven, i.e. the number of edges - * from a leaf to the root. - * @param hashtype - The type of hash function used to create the proof, must be one of: - * * sha512_256 - * * sha256 + * The last round the message attests to. + */ + public lastattestedround: number | bigint; + + /** + * An integer value representing the natural log of the proven weight with 16 bits + * of precision. This value would be used to verify the next state proof. + */ + public lnprovenweight: number | bigint; + + /** + * The vector commitment root of the top N accounts to sign the next StateProof. + */ + public voterscommitment: Uint8Array; + + /** + * Creates a new `StateProofMessage` object. + * @param blockheaderscommitment - The vector commitment root on all light block headers within a state proof + * interval. + * @param firstattestedround - The first round the message attests to. + * @param lastattestedround - The last round the message attests to. + * @param lnprovenweight - An integer value representing the natural log of the proven weight with 16 bits + * of precision. This value would be used to verify the next state proof. + * @param voterscommitment - The vector commitment root of the top N accounts to sign the next StateProof. */ constructor({ - idx, - proof, - stibhash, - treedepth, - hashtype, + blockheaderscommitment, + firstattestedround, + lastattestedround, + lnprovenweight, + voterscommitment, }: { - idx: number | bigint; - proof: string | Uint8Array; - stibhash: string | Uint8Array; - treedepth: number | bigint; - hashtype?: string; + blockheaderscommitment: string | Uint8Array; + firstattestedround: number | bigint; + lastattestedround: number | bigint; + lnprovenweight: number | bigint; + voterscommitment: string | Uint8Array; }) { super(); - this.idx = idx; - this.proof = - typeof proof === 'string' - ? new Uint8Array(Buffer.from(proof, 'base64')) - : proof; - this.stibhash = - typeof stibhash === 'string' - ? new Uint8Array(Buffer.from(stibhash, 'base64')) - : stibhash; - this.treedepth = treedepth; - this.hashtype = hashtype; + this.blockheaderscommitment = + typeof blockheaderscommitment === 'string' + ? new Uint8Array(Buffer.from(blockheaderscommitment, 'base64')) + : blockheaderscommitment; + this.firstattestedround = firstattestedround; + this.lastattestedround = lastattestedround; + this.lnprovenweight = lnprovenweight; + this.voterscommitment = + typeof voterscommitment === 'string' + ? new Uint8Array(Buffer.from(voterscommitment, 'base64')) + : voterscommitment; this.attribute_map = { - idx: 'idx', - proof: 'proof', - stibhash: 'stibhash', - treedepth: 'treedepth', - hashtype: 'hashtype', + blockheaderscommitment: 'BlockHeadersCommitment', + firstattestedround: 'FirstAttestedRound', + lastattestedround: 'LastAttestedRound', + lnprovenweight: 'LnProvenWeight', + voterscommitment: 'VotersCommitment', }; } } @@ -2400,6 +2480,85 @@ export class TransactionParametersResponse extends BaseModel { } } +/** + * Proof of transaction in a block. + */ +export class TransactionProofResponse extends BaseModel { + /** + * Index of the transaction in the block's payset. + */ + public idx: number | bigint; + + /** + * Proof of transaction membership. + */ + public proof: Uint8Array; + + /** + * Hash of SignedTxnInBlock for verifying proof. + */ + public stibhash: Uint8Array; + + /** + * Represents the depth of the tree that is being proven, i.e. the number of edges + * from a leaf to the root. + */ + public treedepth: number | bigint; + + /** + * The type of hash function used to create the proof, must be one of: + * * sha512_256 + * * sha256 + */ + public hashtype?: string; + + /** + * Creates a new `TransactionProofResponse` object. + * @param idx - Index of the transaction in the block's payset. + * @param proof - Proof of transaction membership. + * @param stibhash - Hash of SignedTxnInBlock for verifying proof. + * @param treedepth - Represents the depth of the tree that is being proven, i.e. the number of edges + * from a leaf to the root. + * @param hashtype - The type of hash function used to create the proof, must be one of: + * * sha512_256 + * * sha256 + */ + constructor({ + idx, + proof, + stibhash, + treedepth, + hashtype, + }: { + idx: number | bigint; + proof: string | Uint8Array; + stibhash: string | Uint8Array; + treedepth: number | bigint; + hashtype?: string; + }) { + super(); + this.idx = idx; + this.proof = + typeof proof === 'string' + ? new Uint8Array(Buffer.from(proof, 'base64')) + : proof; + this.stibhash = + typeof stibhash === 'string' + ? new Uint8Array(Buffer.from(stibhash, 'base64')) + : stibhash; + this.treedepth = treedepth; + this.hashtype = hashtype; + + this.attribute_map = { + idx: 'idx', + proof: 'proof', + stibhash: 'stibhash', + treedepth: 'treedepth', + hashtype: 'hashtype', + }; + } +} + /** * algod version information. */ diff --git a/src/client/v2/algod/proof.ts b/src/client/v2/algod/proof.ts deleted file mode 100644 index a8b78e21d..000000000 --- a/src/client/v2/algod/proof.ts +++ /dev/null @@ -1,21 +0,0 @@ -import JSONRequest from '../jsonrequest'; -import HTTPClient from '../../client'; -import IntDecoding from '../../../types/intDecoding'; - -export default class Proof extends JSONRequest { - constructor( - c: HTTPClient, - intDecoding: IntDecoding, - private round: number, - private txID: string - ) { - super(c, intDecoding); - - this.round = round; - this.txID = txID; - } - - path() { - return `/v2/blocks/${this.round}/transactions/${this.txID}/proof`; - } -} diff --git a/src/client/v2/algod/stateproof.ts b/src/client/v2/algod/stateproof.ts new file mode 100644 index 000000000..98bab12a7 --- /dev/null +++ b/src/client/v2/algod/stateproof.ts @@ -0,0 +1,15 @@ +import JSONRequest from '../jsonrequest'; +import HTTPClient from '../../client'; +import IntDecoding from '../../../types/intDecoding'; + +export default class StateProof extends JSONRequest { + constructor(c: HTTPClient, intDecoding: IntDecoding, private round: number) { + super(c, intDecoding); + + this.round = round; + } + + path() { + return `/v2/stateproofs/${this.round}`; + } +} diff --git a/src/client/v2/indexer/lookupAccountTransactions.ts b/src/client/v2/indexer/lookupAccountTransactions.ts index 69f41cd9b..b2343f1ca 100644 --- a/src/client/v2/indexer/lookupAccountTransactions.ts +++ b/src/client/v2/indexer/lookupAccountTransactions.ts @@ -76,7 +76,7 @@ export default class LookupAccountTransactions extends JSONRequest { * .do(); * ``` * - * @param type - one of `pay`, `keyreg`, `acfg`, `axfer`, `afrz`, `appl` + * @param type - one of `pay`, `keyreg`, `acfg`, `axfer`, `afrz`, `appl`, `stpf` * @category query */ txType(type: string) { diff --git a/src/client/v2/indexer/searchForTransactions.ts b/src/client/v2/indexer/searchForTransactions.ts index 5afa84c29..7583059cd 100644 --- a/src/client/v2/indexer/searchForTransactions.ts +++ b/src/client/v2/indexer/searchForTransactions.ts @@ -52,7 +52,7 @@ export default class SearchForTransactions extends JSONRequest { * .do(); * ``` * - * @param type - one of `pay`, `keyreg`, `acfg`, `axfer`, `afrz`, `appl` + * @param type - one of `pay`, `keyreg`, `acfg`, `axfer`, `afrz`, `appl`, `stpf` * @category query */ txType(type: string) { diff --git a/src/logic/logic.ts b/src/logic/logic.ts index 9f66b23f6..f42532940 100644 --- a/src/logic/logic.ts +++ b/src/logic/logic.ts @@ -3,10 +3,12 @@ * Utilities for working with program bytes. */ +/** @deprecated langspec.json is deprecated aross all SDKs */ import langspec from './langspec.json'; /** * Langspec Op Structure + * @deprecated for langspec.json is deprecated aross all SDKs */ interface OpStructure { Opcode: number; @@ -23,13 +25,17 @@ interface OpStructure { Groups: string[]; } +/** @deprecated for langspec.json is deprecated aross all SDKs */ let opcodes: { [key: number]: OpStructure; }; +/** @deprecated for langspec.json is deprecated aross all SDKs */ const maxCost = 20000; +/** @deprecated for langspec.json is deprecated aross all SDKs */ const maxLength = 1000; +/** @deprecated for langspec.json is deprecated aross all SDKs */ export function parseUvarint( array: Uint8Array ): [numberFound: number, size: number] { @@ -49,6 +55,7 @@ export function parseUvarint( return [0, 0]; } +/** @deprecated for langspec.json is deprecated aross all SDKs */ function readIntConstBlock( program: Uint8Array, pc: number @@ -79,6 +86,7 @@ function readIntConstBlock( return [size, ints]; } +/** @deprecated for langspec.json is deprecated aross all SDKs */ function readByteConstBlock( program: Uint8Array, pc: number @@ -116,6 +124,7 @@ function readByteConstBlock( return [size, byteArrays]; } +/** @deprecated for langspec.json is deprecated aross all SDKs */ function readPushIntOp( program: Uint8Array, pc: number @@ -129,6 +138,7 @@ function readPushIntOp( return [size, numberFound]; } +/** @deprecated for langspec.json is deprecated aross all SDKs */ function readPushByteOp( program: Uint8Array, pc: number @@ -151,6 +161,12 @@ function readPushByteOp( /** readProgram validates program for length and running cost, * and additionally provides the found int variables and byte blocks + * + * @deprecated Validation relies on metadata (`langspec.json`) that + * does not accurately represent opcode behavior across program versions. + * The behavior of `readProgram` relies on `langspec.json`. + * Thus, this method is being deprecated. + * * @param program - Program to check * @param args - Program arguments as array of Uint8Array arrays * @throws @@ -254,6 +270,12 @@ export function readProgram( /** * checkProgram validates program for length and running cost + * + * @deprecated Validation relies on metadata (`langspec.json`) that + * does not accurately represent opcode behavior across program versions. + * The behavior of `checkProgram` relies on `langspec.json`. + * Thus, this method is being deprecated. + * * @param program - Program to check * @param args - Program arguments as array of Uint8Array arrays * @throws @@ -264,25 +286,31 @@ export function checkProgram(program: Uint8Array, args?: Uint8Array[]) { return success; } +/** @deprecated for langspec.json is deprecated aross all SDKs */ export function checkIntConstBlock(program: Uint8Array, pc: number) { const [size] = readIntConstBlock(program, pc); return size; } +/** @deprecated for langspec.json is deprecated aross all SDKs */ export function checkByteConstBlock(program: Uint8Array, pc: number) { const [size] = readByteConstBlock(program, pc); return size; } +/** @deprecated for langspec.json is deprecated aross all SDKs */ export function checkPushIntOp(program: Uint8Array, pc: number) { const [size] = readPushIntOp(program, pc); return size; } +/** @deprecated for langspec.json is deprecated aross all SDKs */ export function checkPushByteOp(program: Uint8Array, pc: number) { const [size] = readPushByteOp(program, pc); return size; } +/** @deprecated for langspec.json is deprecated aross all SDKs */ export const langspecEvalMaxVersion = langspec.EvalMaxVersion; +/** @deprecated for langspec.json is deprecated aross all SDKs */ export const langspecLogicSigVersion = langspec.LogicSigVersion; diff --git a/src/logicsig.ts b/src/logicsig.ts index 4904d3c52..74c0e0bef 100644 --- a/src/logicsig.ts +++ b/src/logicsig.ts @@ -1,10 +1,10 @@ import * as nacl from './nacl/naclWrappers'; import * as address from './encoding/address'; import * as encoding from './encoding/encoding'; -import * as logic from './logic/logic'; import { verifyMultisig } from './multisig'; import * as utils from './utils/utils'; import * as txnBuilder from './transaction'; +import { isValidAddress } from './encoding/address'; import { EncodedLogicSig, EncodedLogicSigAccount, @@ -20,6 +20,38 @@ interface LogicSigStorageStructure { msig?: EncodedMultisig; } +/** sanityCheckProgram performs heuristic program validation: + * check if passed in bytes are Algorand address or is B64 encoded, rather than Teal bytes + * + * @param program - Program bytes to check + */ +export function sanityCheckProgram(program: Uint8Array) { + if (!program || program.length === 0) throw new Error('empty program'); + + const lineBreakOrd = '\n'.charCodeAt(0); + const blankSpaceOrd = ' '.charCodeAt(0); + const tildeOrd = '~'.charCodeAt(0); + + const isPrintable = (x: number) => blankSpaceOrd <= x && x <= tildeOrd; + const isAsciiPrintable = program.every( + (x: number) => x === lineBreakOrd || isPrintable(x) + ); + + if (isAsciiPrintable) { + const programStr = Buffer.from(program).toString(); + + if (isValidAddress(programStr)) + throw new Error('requesting program bytes, get Algorand address'); + + if (Buffer.from(programStr, 'base64').toString('base64') === programStr) + throw new Error('program should not be b64 encoded'); + + throw new Error( + 'program bytes are all ASCII printable characters, not looking like Teal byte code' + ); + } +} + /** LogicSig implementation */ @@ -49,9 +81,7 @@ export class LogicSig implements LogicSigStorageStructure { if (programArgs != null) args = programArgs.map((arg) => new Uint8Array(arg)); - if (!logic.checkProgram(program, args)) { - throw new Error('Invalid program'); - } + sanityCheckProgram(program); this.logic = program; this.args = args; @@ -93,7 +123,7 @@ export class LogicSig implements LogicSigStorageStructure { } try { - logic.checkProgram(this.logic, this.args); + sanityCheckProgram(this.logic); } catch (e) { return false; } diff --git a/src/transaction.ts b/src/transaction.ts index 29a4f0207..af4efd849 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -113,6 +113,9 @@ interface TransactionStorageStructure group?: Buffer; extraPages?: number; boxes?: BoxReference[]; + stateProofType?: number | bigint; + stateProof?: Uint8Array; + stateProofMessage?: Uint8Array; } function getKeyregKey( @@ -202,6 +205,9 @@ export class Transaction implements TransactionStorageStructure { nonParticipation?: boolean; group?: Buffer; extraPages?: number; + stateProofType?: number | bigint; + stateProof?: Uint8Array; + stateProofMessage?: Uint8Array; constructor({ ...transaction }: AnyTransaction) { // Populate defaults @@ -546,6 +552,27 @@ export class Transaction implements TransactionStorageStructure { // say we are aware of groups this.group = undefined; + + // stpf fields + if ( + txn.stateProofType !== undefined && + (!Number.isSafeInteger(txn.stateProofType) || txn.stateProofType < 0) + ) + throw Error( + 'State Proof type must be a positive number and smaller than 2^53-1' + ); + if (txn.stateProofMessage !== undefined) { + if (txn.stateProofMessage.constructor !== Uint8Array) + throw Error('stateProofMessage must be a Uint8Array.'); + } else { + txn.stateProofMessage = new Uint8Array(0); + } + if (txn.stateProof !== undefined) { + if (txn.stateProof.constructor !== Uint8Array) + throw Error('stateProof must be a Uint8Array.'); + } else { + txn.stateProof = new Uint8Array(0); + } } // eslint-disable-next-line camelcase @@ -854,6 +881,42 @@ export class Transaction implements TransactionStorageStructure { if (txn.grp === undefined) delete txn.grp; return txn; } + if (this.type === 'stpf') { + // state proof txn + const txn: EncodedTransaction = { + fee: this.fee, + fv: this.firstRound, + lv: this.lastRound, + note: Buffer.from(this.note), + snd: Buffer.from(this.from.publicKey), + type: this.type, + gen: this.genesisID, + gh: this.genesisHash, + lx: Buffer.from(this.lease), + sptype: this.stateProofType, + spmsg: Buffer.from(this.stateProofMessage), + sp: Buffer.from(this.stateProof), + }; + // allowed zero values + if (!txn.sptype) delete txn.sptype; + if (!txn.note.length) delete txn.note; + if (!txn.lx.length) delete txn.lx; + if (!txn.amt) delete txn.amt; + if (!txn.fee) delete txn.fee; + if (!txn.fv) delete txn.fv; + if (!txn.gen) delete txn.gen; + if (!txn.apid) delete txn.apid; + if (!txn.apaa || !txn.apaa.length) delete txn.apaa; + if (!txn.apap) delete txn.apap; + if (!txn.apsu) delete txn.apsu; + if (!txn.apan) delete txn.apan; + if (!txn.apfa || !txn.apfa.length) delete txn.apfa; + if (!txn.apas || !txn.apas.length) delete txn.apas; + if (!txn.apat || !txn.apat.length) delete txn.apat; + if (!txn.apep) delete txn.apep; + if (txn.grp === undefined) delete txn.grp; + return txn; + } return undefined; } @@ -1031,6 +1094,16 @@ export class Transaction implements TransactionStorageStructure { name: box.n, })); } + } else if (txnForEnc.type === 'stpf') { + if (txnForEnc.sptype !== undefined) { + txn.stateProofType = txnForEnc.sptype; + } + if (txnForEnc.sp !== undefined) { + txn.stateProof = txnForEnc.sp; + } + if (txnForEnc.spmsg !== undefined) { + txn.stateProofMessage = txnForEnc.spmsg; + } } return txn; } diff --git a/src/types/blockHeader.ts b/src/types/blockHeader.ts index 7fa1c2e05..96f2af265 100644 --- a/src/types/blockHeader.ts +++ b/src/types/blockHeader.ts @@ -65,7 +65,17 @@ export default interface BlockHeader { ts: number; /** - * Transaction root + * Transaction root SHA512_256 */ txn: string; + + /** + * Transaction root SHA256 + */ + txn256: string; + + /** + * StateProofTracking map of type to tracking data + */ + spt: Map; } diff --git a/src/types/transactions/base.ts b/src/types/transactions/base.ts index f94ebdcbd..391983221 100644 --- a/src/types/transactions/base.ts +++ b/src/types/transactions/base.ts @@ -33,6 +33,10 @@ export enum TransactionType { * Application transaction */ appl = 'appl', + /** + * State proof transaction + */ + stpf = 'stpf', } export function isTransactionType(s: string): s is TransactionType { @@ -42,7 +46,8 @@ export function isTransactionType(s: string): s is TransactionType { s === TransactionType.acfg || s === TransactionType.axfer || s === TransactionType.afrz || - s === TransactionType.appl + s === TransactionType.appl || + s === TransactionType.stpf ); } @@ -405,4 +410,19 @@ export interface TransactionParams { * A grouping of the app ID and name of the box in an Uint8Array */ boxes?: BoxReference[]; + + /* + * Uint64 identifying a particular configuration of state proofs. + */ + stateProofType?: number | bigint; + + /** + * Byte array containing the state proof. + */ + stateProof?: Uint8Array; + + /** + * Byte array containing the state proof message. + */ + stateProofMessage?: Uint8Array; } diff --git a/src/types/transactions/encoded.ts b/src/types/transactions/encoded.ts index 2fdd7e48d..ff3d5609d 100644 --- a/src/types/transactions/encoded.ts +++ b/src/types/transactions/encoded.ts @@ -313,6 +313,21 @@ export interface EncodedTransaction { * boxes */ apbx?: EncodedBoxReference[]; + + /* + * stateProofType + */ + sptype?: number | bigint; + + /** + * stateProof + */ + sp?: Buffer; + + /** + * stateProofMessage + */ + spmsg?: Buffer; } export interface EncodedSubsig { diff --git a/src/types/transactions/index.ts b/src/types/transactions/index.ts index 1ad181ffb..fc4d8543b 100644 --- a/src/types/transactions/index.ts +++ b/src/types/transactions/index.ts @@ -16,6 +16,7 @@ import { ApplicationClearStateTransaction as AppClearStateTxn, ApplicationNoOpTransaction as AppNoOpTxn, } from './application'; +import StateProofTxn from './stateproof'; // Utilities export { @@ -49,6 +50,7 @@ export { ApplicationClearStateTransaction as AppClearStateTxn, ApplicationNoOpTransaction as AppNoOpTxn, } from './application'; +export { default as StateProofTxn } from './stateproof'; // All possible transaction types type AnyTransaction = @@ -65,5 +67,6 @@ type AnyTransaction = | AppOptInTxn | AppCloseOutTxn | AppClearStateTxn - | AppNoOpTxn; + | AppNoOpTxn + | StateProofTxn; export default AnyTransaction; diff --git a/src/types/transactions/stateproof.ts b/src/types/transactions/stateproof.ts new file mode 100644 index 000000000..7f2e1167f --- /dev/null +++ b/src/types/transactions/stateproof.ts @@ -0,0 +1,17 @@ +import { TransactionType, TransactionParams } from './base'; +import { ConstructTransaction } from './builder'; + +type SpecificParameters = Pick< + TransactionParams, + 'stateProofType' | 'stateProof' | 'stateProofMessage' +>; + +interface Overwrites { + type?: TransactionType.stpf; +} + +type StateProofTransaction = ConstructTransaction< + SpecificParameters, + Overwrites +>; +export default StateProofTransaction; diff --git a/test-harness.sh b/test-harness.sh new file mode 100755 index 000000000..ad68eaf63 --- /dev/null +++ b/test-harness.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +set -euo pipefail + +START=$(date "+%s") + +THIS=$(basename "$0") +ENV_FILE=".test-env" +TEST_DIR="tests/cucumber" + +set -a +source "$ENV_FILE" +set +a + +rootdir=$(dirname "$0") +pushd "$rootdir" + +echo "$THIS: VERBOSE_HARNESS=$VERBOSE_HARNESS" + +## Reset test harness +if [ -d "$SDK_TESTING_HARNESS" ]; then + pushd "$SDK_TESTING_HARNESS" + ./scripts/down.sh + popd + rm -rf "$SDK_TESTING_HARNESS" +else + echo "$THIS: directory $SDK_TESTING_HARNESS does not exist - NOOP" +fi + +git clone --depth 1 --single-branch --branch "$SDK_TESTING_BRANCH" "$SDK_TESTING_URL" "$SDK_TESTING_HARNESS" + + +echo "$THIS: OVERWRITE_TESTING_ENVIRONMENT=$OVERWRITE_TESTING_ENVIRONMENT" +if [[ $OVERWRITE_TESTING_ENVIRONMENT == 1 ]]; then + echo "$THIS: OVERWRITE downloaded $SDK_TESTING_HARNESS/.env with $ENV_FILE:" + cp "$ENV_FILE" "$SDK_TESTING_HARNESS"/.env +fi + +echo "$THIS: REMOVE_LOCAL_FEATURES=$REMOVE_LOCAL_FEATURES" +## Copy feature files into the project resources +if [[ $REMOVE_LOCAL_FEATURES == 1 ]]; then + echo "$THIS: OVERWRITE wipes clean $TEST_DIR/features" + if [[ $VERBOSE_HARNESS == 1 ]]; then + ( tree $TEST_DIR/features && echo "$THIS: see the previous for files deleted" ) || true + fi + rm -rf $TEST_DIR/features +fi +mkdir -p $TEST_DIR/features +cp -r "$SDK_TESTING_HARNESS"/features/* $TEST_DIR/features +if [[ $VERBOSE_HARNESS == 1 ]]; then + ( tree $TEST_DIR/features && echo "$THIS: see the previous for files copied over" ) || true +fi +echo "$THIS: seconds it took to get to end of cloning and copying: $(($(date "+%s") - START))s" + +## Start test harness environment +pushd "$SDK_TESTING_HARNESS" + +[[ "$VERBOSE_HARNESS" = 1 ]] && V_FLAG="-v" || V_FLAG="" +echo "$THIS: standing up harnness with command [./up.sh $V_FLAG]" +./scripts/up.sh "$V_FLAG" + +popd +echo "$THIS: seconds it took to finish testing sdk's up.sh: $(($(date "+%s") - START))s" +echo "" +echo "--------------------------------------------------------------------------------" +echo "|" +echo "| To run sandbox commands, cd into $SDK_TESTING_HARNESS/.sandbox " +echo "|" +echo "--------------------------------------------------------------------------------" diff --git a/tests/5.Transaction.js b/tests/5.Transaction.js index cf8eeba07..a3118fefc 100644 --- a/tests/5.Transaction.js +++ b/tests/5.Transaction.js @@ -345,6 +345,40 @@ describe('Sign', () => { assert.deepStrictEqual(reencRep, encRep); }); + it('should correctly serialize and deserialize a state proof transaction from msgpack representation', () => { + const o = { + from: 'XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU', + fee: 10, + firstRound: 51, + lastRound: 61, + note: new Uint8Array([123, 12, 200]), + genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=', + voteKey: '5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKE=', + selectionKey: 'oImqaSLjuZj63/bNSAjd+eAh5JROOJ6j1cY4eGaJGX4=', + voteFirst: 123, + voteLast: 456, + voteKeyDilution: 1234, + genesisID: '', + type: 'stpf', + stateProofType: 0, + stateProof: new Uint8Array([1, 1, 1, 1]), + stateProofMessage: new Uint8Array([0, 0, 0, 0]), + }; + const expectedTxn = new algosdk.Transaction(o); + console.log( + `${expectedTxn.stateProofType} ${expectedTxn.stateProofMessage} ${expectedTxn.stateProof} ${expectedTxn.type}` + ); + const encRep = expectedTxn.get_obj_for_encoding(); + console.log( + `${encRep.sptype} ${encRep.spmsg} ${encRep.sp} ${encRep.type}` + ); + const encTxn = algosdk.encodeObj(encRep); + const decEncRep = algosdk.decodeObj(encTxn); + const decTxn = algosdk.Transaction.from_obj_for_encoding(decEncRep); + const reencRep = decTxn.get_obj_for_encoding(); + assert.deepStrictEqual(reencRep, encRep); + }); + it('should correctly serialize and deserialize a key registration transaction from msgpack representation', () => { const o = { from: 'XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU', diff --git a/tests/7.AlgoSDK.js b/tests/7.AlgoSDK.js index e831786b9..6cc0c67ee 100644 --- a/tests/7.AlgoSDK.js +++ b/tests/7.AlgoSDK.js @@ -830,11 +830,6 @@ describe('Algosdk (AKA end to end)', () => { assert.equal(lsig.logic, program); assert.deepEqual(lsig.args, args); }); - it('should throw on invalid program', () => { - const program = Uint8Array.from([1, 32, 1, 1, 34]); - program[0] = 128; - assert.throws(() => algosdk.makeLogicSig(program)); - }); }); describe('Single logic sig', () => { it('should work on valid program', () => { diff --git a/tests/8.LogicSig.ts b/tests/8.LogicSig.ts index e7e7a5fba..08be74abf 100644 --- a/tests/8.LogicSig.ts +++ b/tests/8.LogicSig.ts @@ -65,11 +65,6 @@ describe('LogicSig', () => { const verified = lsig.verify(pk); assert.strictEqual(verified, false); }); - it('should fail on invalid program', () => { - const program = Uint8Array.from([1, 32, 1, 1, 34]); - program[0] = 128; - assert.throws(() => algosdk.makeLogicSig(program)); - }); }); describe('address', () => { @@ -129,11 +124,6 @@ describe('LogicSigAccount', () => { const decoded = algosdk.LogicSigAccount.fromByte(encoded); assert.deepStrictEqual(decoded, lsigAccount); }); - it('should fail on invalid program', () => { - const program = Uint8Array.from([1, 32, 1, 1, 34]); - program[0] = 128; - assert.throws(() => new algosdk.LogicSigAccount(program)); - }); }); describe('sign', () => { diff --git a/tests/cucumber/docker/run_docker.sh b/tests/cucumber/docker/run_docker.sh deleted file mode 100755 index 84f7bf0cf..000000000 --- a/tests/cucumber/docker/run_docker.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# cleanup last test run -rm -rf test-harness -rm -rf tests/cucumber/features - -# clone test harness -git clone --single-branch --branch feature/box-storage https://github.com/algorand/algorand-sdk-testing.git test-harness - -# move feature files and example files to destination -mv test-harness/features tests/cucumber/features - -if [ $TEST_BROWSER == "chrome" ]; then - # use latest version of chromedriver for compatability with the current Chrome version - npm install chromedriver@latest - # print the version installed - npm ls chromedriver -fi - -# build test environment -docker build -t js-sdk-testing -f tests/cucumber/docker/Dockerfile "$(pwd)" --build-arg TEST_BROWSER --build-arg CI=true - -# Start test harness environment -./test-harness/scripts/up.sh - -docker run -it \ - --network host \ - js-sdk-testing:latest diff --git a/tests/cucumber/integration.tags b/tests/cucumber/integration.tags new file mode 100644 index 000000000..03cb481b6 --- /dev/null +++ b/tests/cucumber/integration.tags @@ -0,0 +1,14 @@ +@abi +@algod +@applications +@applications.verified +@assets +@auction +@c2c +@compile +@compile.sourcemap +@dryrun +@kmd +@rekey_v1 +@send +@send.keyregtxn \ No newline at end of file diff --git a/tests/cucumber/steps/index.js b/tests/cucumber/steps/index.js index 0c9c51f0a..0117dc84e 100644 --- a/tests/cucumber/steps/index.js +++ b/tests/cucumber/steps/index.js @@ -347,6 +347,16 @@ if (browser) { rpcArgs = args.slice(0, rpcArgs.length - 1); } + for (const arg of rpcArgs) { + if (arg instanceof Uint8Array) { + // cannot send Uint8Array or Buffer objects because the arguments will get JSON + // encoded when transmitted to the browser + throw new Error( + `Attempted to send binary data to the browser when invoking test '${type} ${name}'` + ); + } + } + const { error } = await driver.executeAsyncScript( // variables are `scoped` because they exist in the upper scope async (scopedType, scopedName, ...rest) => { @@ -391,34 +401,54 @@ for (const name of Object.keys(steps.given)) { const fn = steps.given[name]; if (name === 'mock http responses in {string} loaded from {string}') { Given(name, function (fileName, jsonDirectory) { - const body1 = setupMockServerForResponses( + let body1 = setupMockServerForResponses( fileName, jsonDirectory, algodMockServerResponder ); - const body2 = setupMockServerForResponses( + let body2 = setupMockServerForResponses( fileName, jsonDirectory, indexerMockServerResponder ); - return fn.call(this, body2 || body1); + let format = 'json'; + if (fileName.endsWith('base64')) { + format = 'msgp'; + } + if (Buffer.isBuffer(body1)) { + body1 = body1.toString('base64'); + } + if (Buffer.isBuffer(body2)) { + body2 = body2.toString('base64'); + } + return fn.call(this, body2 || body1, format); }); } else if ( name === 'mock http responses in {string} loaded from {string} with status {int}.' ) { Given(name, function (fileName, jsonDirectory, status) { - const body1 = setupMockServerForResponses( + let body1 = setupMockServerForResponses( fileName, jsonDirectory, algodMockServerResponder ); - const body2 = setupMockServerForResponses( + let body2 = setupMockServerForResponses( fileName, jsonDirectory, indexerMockServerResponder ); - return fn.call(this, body2 || body1, status); + let format = 'json'; + if (fileName.endsWith('base64')) { + format = 'msgp'; + } + if (Buffer.isBuffer(body1)) { + body1 = body1.toString('base64'); + } + if (Buffer.isBuffer(body2)) { + body2 = body2.toString('base64'); + } + return fn.call(this, body2 || body1, status, format); }); } else if (name === 'mock server recording request paths') { Given(name, function () { diff --git a/tests/cucumber/steps/steps.js b/tests/cucumber/steps/steps.js index f50749e99..7a3e13688 100644 --- a/tests/cucumber/steps/steps.js +++ b/tests/cucumber/steps/steps.js @@ -76,14 +76,6 @@ function parseJSON(json) { // END OBJECT CREATION FUNCTIONS -function formatIncludeAll(includeAll) { - if (!['true', 'false'].includes(includeAll)) { - throw new Error(`Unknown value for includeAll: ${includeAll}`); - } - - return includeAll === 'true'; -} - const steps = { given: {}, when: {}, @@ -320,29 +312,6 @@ module.exports = function getSteps(options) { this.pk = algosdk.multisigAddress(this.msig); }); - When('I create the payment transaction', function () { - this.txn = { - from: this.pk, - to: this.to, - fee: this.fee, - firstRound: this.fv, - lastRound: this.lv, - genesisHash: this.gh, - }; - if (this.gen) { - this.txn.genesisID = this.gen; - } - if (this.close) { - this.txn.closeRemainderTo = this.close; - } - if (this.note) { - this.txn.note = this.note; - } - if (this.amt) { - this.txn.amount = this.amt; - } - }); - When('I sign the transaction with the private key', function () { const obj = algosdk.signTransaction(this.txn, this.sk); this.stx = obj.blob; @@ -684,26 +653,6 @@ module.exports = function getSteps(options) { ); }); - Then('I get transactions by address only', async function () { - const transactions = await this.acl.transactionByAddress(this.accounts[0]); - assert.deepStrictEqual( - true, - Object.entries(transactions).length === 0 || - 'transactions' in transactions - ); - }); - - Then('I get transactions by address and date', async function () { - const transactions = await this.acl.transactionByAddressAndDate( - this.accounts[0] - ); - assert.deepStrictEqual( - true, - Object.entries(transactions).length === 0 || - 'transactions' in transactions - ); - }); - Then('I get pending transactions', async function () { const transactions = await this.acl.pendingTransactions(10); assert.deepStrictEqual( @@ -935,19 +884,16 @@ module.exports = function getSteps(options) { }); Then('the transaction should go through', async function () { - let info = await this.acl.pendingTransactionInformation(this.txid); - assert.deepStrictEqual(true, 'type' in info); - // let localParams = await this.acl.getTransactionParams(); - // this.lastRound = localParams.lastRound; await waitForAlgodInDevMode(); - info = await this.acl.transactionById(this.txid); + const info = await this.acl.pendingTransactionInformation(this.txid); assert.deepStrictEqual(true, 'type' in info); - }); - Then('I can get the transaction by ID', async function () { - await waitForAlgodInDevMode(); - const info = await this.acl.transactionById(this.txid); - assert.deepStrictEqual(true, 'type' in info); + // TODO: this needs to be modified/removed when v1 is no longer supported + // let localParams = await this.acl.getTransactionParams(); + // this.lastRound = localParams.lastRound; + // await waitForAlgodInDevMode(); + // info = await this.acl.transactionById(this.txid); + // assert.deepStrictEqual(true, 'type' in info); }); Then('the transaction should not go through', function () { @@ -1065,60 +1011,6 @@ module.exports = function getSteps(options) { return this.kcl.deleteKey(this.handle, this.wallet_pswd, this.pk); }); - Given( - 'key registration transaction parameters {int} {int} {int} {string} {string} {string} {int} {int} {int} {string} {string}', - function ( - fee, - fv, - lv, - gh, - votekey, - selkey, - votefst, - votelst, - votekd, - gen, - note - ) { - this.fee = parseInt(fee); - this.fv = parseInt(fv); - this.lv = parseInt(lv); - this.gh = gh; - if (gen !== 'none') { - this.gen = gen; - } - if (note !== 'none') { - this.note = makeUint8Array(Buffer.from(note, 'base64')); - } - this.votekey = votekey; - this.selkey = selkey; - this.votefst = votefst; - this.votelst = votelst; - this.votekd = votekd; - } - ); - - When('I create the key registration transaction', function () { - this.txn = { - fee: this.fee, - firstRound: this.fv, - lastRound: this.lv, - genesisHash: this.gh, - voteKey: this.votekey, - selectionKey: this.selkey, - voteFirst: this.votefst, - voteLast: this.votelst, - voteKeyDilution: this.votekd, - type: 'keyreg', - }; - if (this.gen) { - this.txn.genesisID = this.gen; - } - if (this.note) { - this.txn.note = this.note; - } - }); - Given( 'default V2 key registration transaction {string}', async function (type) { @@ -1176,13 +1068,6 @@ module.exports = function getSteps(options) { } ); - When( - 'I get recent transactions, limited by {int} transactions', - function (int) { - this.acl.transactionByAddress(this.accounts[0], parseInt(int)); - } - ); - /// ///////////////////////////////// // begin asset tests /// ///////////////////////////////// @@ -1683,13 +1568,20 @@ module.exports = function getSteps(options) { } = options; let expectedMockResponse; + let responseFormat; Given( 'mock http responses in {string} loaded from {string}', - function (expectedBody) { + function (expectedBody, format) { if (expectedBody !== null) { expectedMockResponse = expectedBody; + if (format === 'msgp') { + expectedMockResponse = new Uint8Array( + Buffer.from(expectedMockResponse, 'base64') + ); + } } + responseFormat = format; this.v2Client = new algosdk.Algodv2( '', `http://${mockAlgodResponderHost}`, @@ -1707,10 +1599,16 @@ module.exports = function getSteps(options) { Given( 'mock http responses in {string} loaded from {string} with status {int}.', - function (expectedBody, status) { + function (expectedBody, status, format) { if (expectedBody !== null) { expectedMockResponse = expectedBody; + if (format === 'msgp') { + expectedMockResponse = new Uint8Array( + Buffer.from(expectedMockResponse, 'base64') + ); + } } + responseFormat = format; this.v2Client = new algosdk.Algodv2( '', `http://${mockAlgodResponderHost}`, @@ -1734,7 +1632,11 @@ module.exports = function getSteps(options) { try { if (client === 'algod') { // endpoints are ignored by mock server, see setupMockServerForResponses - this.actualMockResponse = await this.v2Client.status().do(); + if (responseFormat === 'msgp') { + this.actualMockResponse = await this.v2Client.block(0).do(); + } else { + this.actualMockResponse = await this.v2Client.status().do(); + } } else if (client === 'indexer') { // endpoints are ignored by mock server, see setupMockServerForResponses this.actualMockResponse = await this.indexerClient @@ -1760,10 +1662,22 @@ module.exports = function getSteps(options) { Then('the parsed response should equal the mock response.', function () { if (this.expectedMockResponseCode === 200) { - assert.strictEqual( - JSON.stringify(JSON.parse(expectedMockResponse)), - JSON.stringify(this.actualMockResponse) - ); + // assert.deepStrictEqual considers a Buffer and Uint8Array with the same contents as unequal. + // These types are fairly interchangable in different parts of the SDK, so we need to normalize + // them before comparing, which is why we chain encoding/decoding below. + if (responseFormat === 'json') { + assert.strictEqual( + JSON.stringify(JSON.parse(expectedMockResponse)), + JSON.stringify(this.actualMockResponse) + ); + } else { + assert.deepStrictEqual( + algosdk.decodeObj( + new Uint8Array(algosdk.encodeObj(this.actualMockResponse)) + ), + algosdk.decodeObj(expectedMockResponse) + ); + } } }); @@ -1816,13 +1730,6 @@ module.exports = function getSteps(options) { } ); - When( - 'we make a Pending Transaction Information against txid {string} with max {int}', - function (txid, max) { - this.v2Client.pendingTransactionInformation(txid).max(max).do(); - } - ); - When( 'we make a Pending Transaction Information against txid {string} with format {string}', async function (txid, format) { @@ -2113,19 +2020,6 @@ module.exports = function getSteps(options) { } ); - When( - 'we make a Lookup Asset Balances call against asset index {int} with limit {int} nextToken {string} currencyGreaterThan {int} currencyLessThan {int}', - async function (index, limit, nextToken, currencyGreater, currencyLesser) { - await this.indexerClient - .lookupAssetBalances(index) - .limit(limit) - .currencyGreaterThan(currencyGreater) - .currencyLessThan(currencyLesser) - .nextToken(nextToken) - .do(); - } - ); - When( 'we make a Lookup Asset Balances call against asset index {int} with limit {int} afterAddress {string} currencyGreaterThan {int} currencyLessThan {int}', async function ( @@ -2144,51 +2038,6 @@ module.exports = function getSteps(options) { } ); - When( - 'we make a Lookup Asset Transactions call against asset index {int} with NotePrefix {string} TxType {string} SigType {string} txid {string} round {int} minRound {int} maxRound {int} limit {int} beforeTime {int} afterTime {int} currencyGreaterThan {int} currencyLessThan {int} address {string} addressRole {string} ExcluseCloseTo {string}', - async function ( - assetIndex, - notePrefix, - txType, - sigType, - txid, - round, - minRound, - maxRound, - limit, - beforeTime, - afterTime, - currencyGreater, - currencyLesser, - address, - addressRole, - excludeCloseToAsString - ) { - let excludeCloseTo = false; - if (excludeCloseToAsString === 'true') { - excludeCloseTo = true; - } - await this.indexerClient - .lookupAssetTransactions(assetIndex) - .notePrefix(notePrefix) - .txType(txType) - .sigType(sigType) - .txid(txid) - .round(round) - .minRound(minRound) - .maxRound(maxRound) - .limit(limit) - .beforeTime(beforeTime) - .afterTime(afterTime) - .currencyGreaterThan(currencyGreater) - .currencyLessThan(currencyLesser) - .address(address) - .addressRole(addressRole) - .excludeCloseTo(excludeCloseTo) - .do(); - } - ); - When( 'we make a Lookup Asset Transactions call against asset index {int} with NotePrefix {string} TxType {string} SigType {string} txid {string} round {int} minRound {int} maxRound {int} limit {int} beforeTime {string} afterTime {string} currencyGreaterThan {int} currencyLessThan {int} address {string} addressRole {string} ExcluseCloseTo {string}', async function ( @@ -2393,13 +2242,6 @@ module.exports = function getSteps(options) { } ); - When( - 'we make a LookupApplications call with {int} and {int}', - async function (index, round) { - await this.indexerClient.lookupApplications(index).round(round).do(); - } - ); - When( 'we make a LookupApplicationLogsByID call with applicationID {int} limit {int} minRound {int} maxRound {int} nextToken {string} sender {string} and txID {string}', async function (appID, limit, minRound, maxRound, nextToken, sender, txID) { @@ -2415,26 +2257,6 @@ module.exports = function getSteps(options) { } ); - When( - 'we make a Search Accounts call with assetID {int} limit {int} currencyGreaterThan {int} currencyLessThan {int} and nextToken {string}', - async function ( - assetIndex, - limit, - currencyGreater, - currencyLesser, - nextToken - ) { - await this.indexerClient - .searchAccounts() - .assetID(assetIndex) - .limit(limit) - .currencyGreaterThan(currencyGreater) - .currencyLessThan(currencyLesser) - .nextToken(nextToken) - .do(); - } - ); - When( 'we make a Search Accounts call with assetID {int} limit {int} currencyGreaterThan {int} currencyLessThan {int} and round {int}', async function (assetIndex, limit, currencyGreater, currencyLesser, round) { @@ -2478,63 +2300,6 @@ module.exports = function getSteps(options) { } ); - When( - 'we make a Search For Transactions call with account {string} NotePrefix {string} TxType {string} SigType {string} txid {string} round {int} minRound {int} maxRound {int} limit {int} beforeTime {int} afterTime {int} currencyGreaterThan {int} currencyLessThan {int} assetIndex {int} addressRole {string} ExcluseCloseTo {string}', - async function ( - account, - notePrefix, - txType, - sigType, - txid, - round, - minRound, - maxRound, - limit, - beforeTime, - afterTime, - currencyGreater, - currencyLesser, - assetIndex, - addressRole, - excludeCloseToAsString - ) { - let excludeCloseTo = false; - if (excludeCloseToAsString === 'true') { - excludeCloseTo = true; - } - await this.indexerClient - .searchForTransactions() - .address(account) - .notePrefix(notePrefix) - .txType(txType) - .sigType(sigType) - .txid(txid) - .round(round) - .minRound(minRound) - .maxRound(maxRound) - .limit(limit) - .beforeTime(beforeTime) - .afterTime(afterTime) - .currencyGreaterThan(currencyGreater) - .currencyLessThan(currencyLesser) - .assetID(assetIndex) - .addressRole(addressRole) - .excludeCloseTo(excludeCloseTo) - .do(); - } - ); - - When( - 'we make a SearchForApplications call with {int} and {int}', - async function (index, round) { - await this.indexerClient - .searchForApplications() - .index(index) - .round(round) - .do(); - } - ); - When( 'we make a SearchForApplications call with creator {string}', async function (creator) { @@ -2640,21 +2405,6 @@ module.exports = function getSteps(options) { } ); - When( - 'we make a SearchForAssets call with limit {int} creator {string} name {string} unit {string} index {int} and nextToken {string}', - async function (limit, creator, name, unit, index, nextToken) { - await this.indexerClient - .searchForAssets() - .limit(limit) - .creator(creator) - .name(name) - .unit(unit) - .index(index) - .nextToken(nextToken) - .do(); - } - ); - When( 'we make a SearchForAssets call with limit {int} creator {string} name {string} unit {string} index {int}', async function (limit, creator, name, unit, index) { @@ -2676,13 +2426,6 @@ module.exports = function getSteps(options) { } ); - When( - 'we make a SearchForApplications call with creator {int}', - async function (index) { - await this.indexerClient.searchForApplications().creator(index).do(); - } - ); - When( 'we make a LookupApplications call with applicationID {int}', async function (index) { @@ -2993,825 +2736,12 @@ module.exports = function getSteps(options) { ); /// ///////////////////////////////// - // begin indexer and integration tests + // begin rekey test helpers /// ///////////////////////////////// - const indexerIntegrationClients = {}; - - Given( - 'indexer client {int} at {string} port {int} with token {string}', - (clientNum, indexerHost, indexerPort, indexerToken) => { - let mutableIndexerHost = indexerHost; - - if (!mutableIndexerHost.startsWith('http')) { - mutableIndexerHost = `http://${mutableIndexerHost}`; - } - indexerIntegrationClients[clientNum] = new algosdk.Indexer( - indexerToken, - mutableIndexerHost, - indexerPort, - {} - ); - } - ); - - When('I use {int} to check the services health', async (clientNum) => { - const ic = indexerIntegrationClients[clientNum]; - await ic.makeHealthCheck().do(); - }); - - Then('I receive status code {int}', async (code) => { - // Currently only supports the good case. code != 200 should throw an exception. - assert.strictEqual(code, 200); - }); - - let integrationBlockResponse; - - When('I use {int} to lookup block {int}', async (clientNum, blockNum) => { - const ic = indexerIntegrationClients[clientNum]; - integrationBlockResponse = await ic.lookupBlock(blockNum).do(); - }); - - Then( - 'The block was confirmed at {int}, contains {int} transactions, has the previous block hash {string}', - (timestamp, numTransactions, prevHash) => { - assert.strictEqual(timestamp, integrationBlockResponse.timestamp); - assert.strictEqual( - numTransactions, - integrationBlockResponse.transactions.length - ); - assert.strictEqual( - prevHash, - integrationBlockResponse['previous-block-hash'] - ); - } - ); - - let integrationLookupAccountResponse; - - When( - 'I use {int} to lookup account {string} at round {int}', - async (clientNum, account, round) => { - const ic = indexerIntegrationClients[clientNum]; - integrationLookupAccountResponse = await ic - .lookupAccountByID(account) - .round(round) - .do(); - } - ); - - Then( - 'The account has {int} assets, the first is asset {int} has a frozen status of {string} and amount {int}.', - (numAssets, firstAssetIndex, firstAssetFrozenStatus, firstAssetAmount) => { - const firstAssetFrozenBool = firstAssetFrozenStatus === 'true'; - assert.strictEqual( - numAssets, - integrationLookupAccountResponse.account.assets.length - ); - if (numAssets === 0) { - return; - } - const scrutinizedAsset = - integrationLookupAccountResponse.account.assets[0]; - assert.strictEqual(firstAssetIndex, scrutinizedAsset['asset-id']); - assert.strictEqual(firstAssetFrozenBool, scrutinizedAsset['is-frozen']); - assert.strictEqual(firstAssetAmount, scrutinizedAsset.amount); - } - ); - - Then( - 'The account created {int} assets, the first is asset {int} is named {string} with a total amount of {int} {string}', - ( - numCreatedAssets, - firstCreatedAssetIndex, - assetName, - assetIssuance, - assetUnit - ) => { - assert.strictEqual( - numCreatedAssets, - integrationLookupAccountResponse.account['created-assets'].length - ); - const scrutinizedAsset = - integrationLookupAccountResponse.account['created-assets'][0]; - assert.strictEqual(firstCreatedAssetIndex, scrutinizedAsset.index); - assert.strictEqual(assetName, scrutinizedAsset.params.name); - assert.strictEqual(assetIssuance, scrutinizedAsset.params.total); - assert.strictEqual(assetUnit, scrutinizedAsset.params['unit-name']); - } - ); - - Then( - 'The account has {int} μalgos and {int} assets, {int} has {int}', - (microAlgos, numAssets, assetIndexToScrutinize, assetAmount) => { - assert.strictEqual( - microAlgos, - integrationLookupAccountResponse.account.amount - ); - if (numAssets === 0) { - return; - } - assert.strictEqual( - numAssets, - integrationLookupAccountResponse.account.assets.length - ); - if (assetIndexToScrutinize === 0) { - return; - } - for ( - let idx = 0; - idx < integrationLookupAccountResponse.account.assets.length; - idx++ - ) { - const scrutinizedAsset = - integrationLookupAccountResponse.account.assets[idx]; - if (scrutinizedAsset.index === assetIndexToScrutinize) { - assert.strictEqual(assetAmount, scrutinizedAsset.amount); - } - } - } - ); - - let integrationLookupAssetResponse; - - When('I use {int} to lookup asset {int}', async (clientNum, assetIndex) => { - const ic = indexerIntegrationClients[clientNum]; - integrationLookupAssetResponse = await ic.lookupAssetByID(assetIndex).do(); - }); - - Then( - 'The asset found has: {string}, {string}, {string}, {int}, {string}, {int}, {string}', - ( - name, - units, - creator, - decimals, - defaultFrozen, - totalIssuance, - clawback - ) => { - const assetParams = integrationLookupAssetResponse.asset.params; - assert.strictEqual(name, assetParams.name); - assert.strictEqual(units, assetParams['unit-name']); - assert.strictEqual(creator, assetParams.creator); - assert.strictEqual(decimals, assetParams.decimals); - const defaultFrozenBool = defaultFrozen === 'true'; - assert.strictEqual(defaultFrozenBool, assetParams['default-frozen']); - assert.strictEqual(totalIssuance, assetParams.total); - assert.strictEqual(clawback, assetParams.clawback); - } - ); - - let integrationLookupAssetBalancesResponse; - - When( - 'I use {int} to lookup asset balances for {int} with {int}, {int}, {int} and token {string}', - async ( - clientNum, - assetIndex, - currencyGreater, - currencyLesser, - limit, - nextToken - ) => { - const ic = indexerIntegrationClients[clientNum]; - integrationLookupAssetBalancesResponse = await ic - .lookupAssetBalances(assetIndex) - .currencyGreaterThan(currencyGreater) - .currencyLessThan(currencyLesser) - .limit(limit) - .nextToken(nextToken) - .do(); - } - ); - - When( - 'I get the next page using {int} to lookup asset balances for {int} with {int}, {int}, {int}', - async (clientNum, assetIndex, currencyGreater, currencyLesser, limit) => { - const ic = indexerIntegrationClients[clientNum]; - const nextToken = integrationLookupAssetBalancesResponse['next-token']; - integrationLookupAssetBalancesResponse = await ic - .lookupAssetBalances(assetIndex) - .currencyGreaterThan(currencyGreater) - .currencyLessThan(currencyLesser) - .limit(limit) - .nextToken(nextToken) - .do(); - } - ); - - Then( - 'There are {int} with the asset, the first is {string} has {string} and {int}', - (numAccounts, firstAccountAddress, isFrozenString, accountAmount) => { - assert.strictEqual( - numAccounts, - integrationLookupAssetBalancesResponse.balances.length - ); - if (numAccounts === 0) { - return; - } - const firstHolder = integrationLookupAssetBalancesResponse.balances[0]; - assert.strictEqual(firstAccountAddress, firstHolder.address); - const isFrozenBool = isFrozenString === 'true'; - assert.strictEqual(isFrozenBool, firstHolder['is-frozen']); - assert.strictEqual(accountAmount, firstHolder.amount); - } - ); - - let integrationSearchAccountsResponse; - - When( - 'I use {int} to search for an account with {int}, {int}, {int}, {int} and token {string}', - async ( - clientNum, - assetIndex, - limit, - currencyGreater, - currencyLesser, - nextToken - ) => { - const ic = indexerIntegrationClients[clientNum]; - integrationSearchAccountsResponse = await ic - .searchAccounts() - .assetID(assetIndex) - .currencyGreaterThan(currencyGreater) - .currencyLessThan(currencyLesser) - .limit(limit) - .nextToken(nextToken) - .do(); - } - ); - - When( - 'I use {int} to search for an account with {int}, {int}, {int}, {int}, {string}, {int} and token {string}', - async function ( - clientNum, - assetIndex, - limit, - currencyGreater, - currencyLesser, - authAddr, - appID, - nextToken - ) { - const ic = indexerIntegrationClients[clientNum]; - integrationSearchAccountsResponse = await ic - .searchAccounts() - .assetID(assetIndex) - .currencyGreaterThan(currencyGreater) - .currencyLessThan(currencyLesser) - .limit(limit) - .authAddr(authAddr) - .applicationID(appID) - .nextToken(nextToken) - .do(); - this.responseForDirectJsonComparison = integrationSearchAccountsResponse; - } - ); - - When( - 'I use {int} to search for an account with {int}, {int}, {int}, {int}, {string}, {int}, {string} and token {string}', - async function ( - clientNum, - assetIndex, - limit, - currencyGreater, - currencyLesser, - authAddr, - appID, - includeAll, - nextToken - ) { - const ic = indexerIntegrationClients[clientNum]; - integrationSearchAccountsResponse = await ic - .searchAccounts() - .assetID(assetIndex) - .currencyGreaterThan(currencyGreater) - .currencyLessThan(currencyLesser) - .limit(limit) - .authAddr(authAddr) - .applicationID(appID) - .includeAll(formatIncludeAll(includeAll)) - .nextToken(nextToken) - .do(); - this.responseForDirectJsonComparison = integrationSearchAccountsResponse; - } - ); - - Then( - 'There are {int}, the first has {int}, {int}, {int}, {int}, {string}, {int}, {string}, {string}', - ( - numAccounts, - pendingRewards, - rewardsBase, - rewards, - withoutRewards, - address, - amount, - status, - type - ) => { - assert.strictEqual( - numAccounts, - integrationSearchAccountsResponse.accounts.length - ); - if (numAccounts === 0) { - return; - } - const scrutinizedAccount = integrationSearchAccountsResponse.accounts[0]; - assert.strictEqual(pendingRewards, scrutinizedAccount['pending-rewards']); - assert.strictEqual(rewardsBase, scrutinizedAccount['reward-base']); - assert.strictEqual(rewards, scrutinizedAccount.rewards); - assert.strictEqual( - withoutRewards, - scrutinizedAccount['amount-without-pending-rewards'] - ); - assert.strictEqual(address, scrutinizedAccount.address); - assert.strictEqual(amount, scrutinizedAccount.amount); - assert.strictEqual(status, scrutinizedAccount.status); - if (type) { - assert.strictEqual(type, scrutinizedAccount['sig-type']); - } - } - ); - - Then( - 'I get the next page using {int} to search for an account with {int}, {int}, {int} and {int}', - async (clientNum, assetIndex, limit, currencyGreater, currencyLesser) => { - const ic = indexerIntegrationClients[clientNum]; - const nextToken = integrationSearchAccountsResponse['next-token']; - integrationSearchAccountsResponse = await ic - .searchAccounts() - .assetID(assetIndex) - .currencyGreaterThan(currencyGreater) - .currencyLessThan(currencyLesser) - .limit(limit) - .nextToken(nextToken) - .do(); - } - ); - - Then( - 'The first account is online and has {string}, {int}, {int}, {int}, {string}, {string}', - (address, keyDilution, firstValid, lastValid, voteKey, selKey) => { - const scrutinizedAccount = integrationSearchAccountsResponse.accounts[0]; - assert.strictEqual('Online', scrutinizedAccount.status); - assert.strictEqual(address, scrutinizedAccount.address); - assert.strictEqual( - keyDilution, - scrutinizedAccount.participation['vote-key-dilution'] - ); - assert.strictEqual( - firstValid, - scrutinizedAccount.participation['vote-first-valid'] - ); - assert.strictEqual( - lastValid, - scrutinizedAccount.participation['vote-last-valid'] - ); - assert.strictEqual( - voteKey, - scrutinizedAccount.participation['vote-participation-key'] - ); - assert.strictEqual( - selKey, - scrutinizedAccount.participation['selection-participation-key'] - ); - } - ); - - let integrationSearchTransactionsResponse; - - When( - 'I use {int} to search for transactions with {int}, {string}, {string}, {string}, {string}, {int}, {int}, {int}, {int}, {string}, {string}, {int}, {int}, {string}, {string}, {string} and token {string}', - async ( - clientNum, - limit, - notePrefix, - txType, - sigType, - txid, - round, - minRound, - maxRound, - assetId, - beforeTime, - afterTime, - currencyGreater, - currencyLesser, - address, - addressRole, - excludeCloseToString, - nextToken - ) => { - const ic = indexerIntegrationClients[clientNum]; - const excludeCloseToBool = excludeCloseToString === 'true'; - integrationSearchTransactionsResponse = await ic - .searchForTransactions() - .limit(limit) - .notePrefix(notePrefix) - .txType(txType) - .sigType(sigType) - .txid(txid) - .round(round) - .minRound(minRound) - .maxRound(maxRound) - .assetID(assetId) - .beforeTime(beforeTime) - .afterTime(afterTime) - .currencyGreaterThan(currencyGreater) - .currencyLessThan(currencyLesser) - .address(address) - .addressRole(addressRole) - .excludeCloseTo(excludeCloseToBool) - .nextToken(nextToken) - .do(); - } - ); - - When( - 'I use {int} to search for transactions with {int}, {string}, {string}, {string}, {string}, {int}, {int}, {int}, {int}, {string}, {string}, {int}, {int}, {string}, {string}, {string}, {int} and token {string}', - async function ( - clientNum, - limit, - notePrefix, - txType, - sigType, - txid, - round, - minRound, - maxRound, - assetId, - beforeTime, - afterTime, - currencyGreater, - currencyLesser, - address, - addressRole, - excludeCloseToString, - appID, - nextToken - ) { - const ic = indexerIntegrationClients[clientNum]; - const excludeCloseToBool = excludeCloseToString === 'true'; - integrationSearchTransactionsResponse = await ic - .searchForTransactions() - .limit(limit) - .notePrefix(notePrefix) - .txType(txType) - .sigType(sigType) - .txid(txid) - .round(round) - .minRound(minRound) - .maxRound(maxRound) - .assetID(assetId) - .beforeTime(beforeTime) - .afterTime(afterTime) - .currencyGreaterThan(currencyGreater) - .currencyLessThan(currencyLesser) - .address(address) - .addressRole(addressRole) - .excludeCloseTo(excludeCloseToBool) - .applicationID(appID) - .nextToken(nextToken) - .do(); - this.responseForDirectJsonComparison = integrationSearchTransactionsResponse; - } - ); - - When( - 'I use {int} to search for all {string} transactions', - async (clientNum, account) => { - const ic = indexerIntegrationClients[clientNum]; - integrationSearchTransactionsResponse = await ic - .searchForTransactions() - .address(account) - .do(); - } - ); - - When( - 'I use {int} to search for all {int} asset transactions', - async (clientNum, assetIndex) => { - const ic = indexerIntegrationClients[clientNum]; - integrationSearchTransactionsResponse = await ic - .searchForTransactions() - .assetID(assetIndex) - .do(); - } - ); - - When( - 'I use {int} to search for applications with {int}, {int}, and token {string}', - async function (clientNum, limit, appID, token) { - const ic = indexerIntegrationClients[clientNum]; - this.responseForDirectJsonComparison = await ic - .searchForApplications() - .limit(limit) - .index(appID) - .nextToken(token) - .do(); - } - ); - - When( - 'I use {int} to search for applications with {int}, {int}, {string} and token {string}', - async function (clientNum, limit, appID, includeAll, token) { - const ic = indexerIntegrationClients[clientNum]; - this.responseForDirectJsonComparison = await ic - .searchForApplications() - .limit(limit) - .index(appID) - .includeAll(formatIncludeAll(includeAll)) - .nextToken(token) - .do(); - } - ); - - When( - 'I use {int} to lookup application with {int}', - async function (clientNum, appID) { - const ic = indexerIntegrationClients[clientNum]; - this.responseForDirectJsonComparison = await ic - .lookupApplications(appID) - .do(); - } - ); - - When( - 'I use {int} to lookup application with {int} and {string}', - async function (clientNum, appID, includeAll) { - const ic = indexerIntegrationClients[clientNum]; - try { - this.responseForDirectJsonComparison = await ic - .lookupApplications(appID) - .includeAll(formatIncludeAll(includeAll)) - .do(); - } catch (err) { - if (err.status !== 404) { - throw err; - } - this.responseForDirectJsonComparison = err.response.body; - } - } - ); - - function sortKeys(x) { - // recursively sorts on keys, unless the passed object is an array of dicts that all contain the property 'key', - // in which case it sorts on the value corresponding to key 'key' - if (typeof x !== 'object' || !x) return x; - if (Array.isArray(x)) { - if ( - x.every( - (subobject) => - typeof subobject === 'object' && - Object.prototype.hasOwnProperty.call(subobject, 'key') - ) - ) { - return x.sort((a, b) => (a.key > b.key ? 1 : -1)); - } - return x.map(sortKeys); - } - return Object.keys(x) - .sort() - .reduce((o, k) => ({ ...o, [k]: sortKeys(x[k]) }), {}); - } - - Then('the parsed response should equal {string}.', async function (jsonFile) { - const rawResponse = await loadResource(jsonFile); - const responseFromFile = sortKeys(JSON.parse(rawResponse.toString())); - this.responseForDirectJsonComparison = sortKeys( - this.responseForDirectJsonComparison - ); - assert.strictEqual( - JSON.stringify(this.responseForDirectJsonComparison), - JSON.stringify(responseFromFile) - ); - }); - - When( - 'I get the next page using {int} to search for transactions with {int} and {int}', - async (clientNum, limit, maxRound) => { - const ic = indexerIntegrationClients[clientNum]; - const nextToken = integrationSearchTransactionsResponse['next-token']; - integrationSearchTransactionsResponse = await ic - .searchForTransactions() - .limit(limit) - .maxRound(maxRound) - .nextToken(nextToken) - .do(); - } - ); - - Then( - 'there are {int} transactions in the response, the first is {string}.', - (numTransactions, txid) => { - assert.strictEqual( - numTransactions, - integrationSearchTransactionsResponse.transactions.length - ); - if (numTransactions === 0) { - return; - } - assert.strictEqual( - txid, - integrationSearchTransactionsResponse.transactions[0].id - ); - } - ); - - Then('Every transaction has tx-type {string}', (txType) => { - for ( - let idx = 0; - idx < integrationSearchTransactionsResponse.transactions.length; - idx++ - ) { - const scrutinizedTxn = - integrationSearchTransactionsResponse.transactions[idx]; - assert.strictEqual(txType, scrutinizedTxn['tx-type']); - } - }); - - Then('Every transaction has sig-type {string}', (sigType) => { - function getSigTypeFromTxnResponse(txn) { - if (txn.signature.logicsig) { - return 'lsig'; - } - if (txn.signature.sig) { - return 'sig'; - } - if (txn.signature.multisig) { - return 'msig'; - } - return 'did not recognize sigtype of txn'; - } - for ( - let idx = 0; - idx < integrationSearchTransactionsResponse.transactions.length; - idx++ - ) { - const scrutinizedTxn = - integrationSearchTransactionsResponse.transactions[idx]; - assert.strictEqual(sigType, getSigTypeFromTxnResponse(scrutinizedTxn)); - } - }); - - Then('Every transaction has round {int}', (round) => { - for ( - let idx = 0; - idx < integrationSearchTransactionsResponse.transactions.length; - idx++ - ) { - const scrutinizedTxn = - integrationSearchTransactionsResponse.transactions[idx]; - assert.strictEqual(round, scrutinizedTxn['confirmed-round']); - } - }); - - Then('Every transaction has round >= {int}', (round) => { - for ( - let idx = 0; - idx < integrationSearchTransactionsResponse.transactions.length; - idx++ - ) { - const scrutinizedTxn = - integrationSearchTransactionsResponse.transactions[idx]; - assert.ok(round <= scrutinizedTxn['confirmed-round']); - } - }); - - Then('Every transaction has round <= {int}', (round) => { - for ( - let idx = 0; - idx < integrationSearchTransactionsResponse.transactions.length; - idx++ - ) { - const scrutinizedTxn = - integrationSearchTransactionsResponse.transactions[idx]; - assert.ok(round >= scrutinizedTxn['confirmed-round']); - } - }); - - Then('Every transaction works with asset-id {int}', (assetId) => { - function extractIdFromTransaction(txn) { - if (txn['created-asset-index']) { - return txn['created-asset-index']; - } - if (txn['asset-config-transaction']) { - return txn['asset-config-transaction']['asset-id']; - } - if (txn['asset-transfer-transaction']) { - return txn['asset-transfer-transaction']['asset-id']; - } - if (txn['asset-freeze-transaction']) { - return txn['asset-freeze-transaction']['asset-id']; - } - return 'could not find asset id within txn'; - } - for ( - let idx = 0; - idx < integrationSearchTransactionsResponse.transactions.length; - idx++ - ) { - const scrutinizedTxn = - integrationSearchTransactionsResponse.transactions[idx]; - assert.strictEqual(assetId, extractIdFromTransaction(scrutinizedTxn)); - } - }); - - Then('Every transaction is older than {string}', (olderThan) => { - for ( - let idx = 0; - idx < integrationSearchTransactionsResponse.transactions.length; - idx++ - ) { - const scrutinizedTxn = - integrationSearchTransactionsResponse.transactions[idx]; - assert.ok(scrutinizedTxn['round-time'] < Date.parse(olderThan) / 1000); - } - }); - - Then('Every transaction is newer than {string}', (newerThan) => { - for ( - let idx = 0; - idx < integrationSearchTransactionsResponse.transactions.length; - idx++ - ) { - const scrutinizedTxn = - integrationSearchTransactionsResponse.transactions[idx]; - assert.ok(scrutinizedTxn['round-time'] > Date.parse(newerThan) / 1000); - } - }); - - Then( - 'Every transaction moves between {int} and {int} currency', - (lowerBound, upperBound) => { - function getAmountMoved(txn) { - if (txn['payment-transaction']) { - return txn['payment-transaction'].amount; - } - if (txn['asset-transfer-transaction']) { - return txn['asset-transfer-transaction'].amount; - } - return 'could not get amount moved from txn'; - } - for ( - let idx = 0; - idx < integrationSearchTransactionsResponse.transactions.length; - idx++ - ) { - const scrutinizedTxn = - integrationSearchTransactionsResponse.transactions[idx]; - const amountMoved = getAmountMoved(scrutinizedTxn); - if (upperBound !== 0) { - assert.ok(amountMoved <= upperBound); - } - assert.ok(amountMoved >= lowerBound); - } - } - ); - - let integrationSearchAssetsResponse; - - When( - 'I use {int} to search for assets with {int}, {int}, {string}, {string}, {string}, and token {string}', - async (clientNum, zero, assetId, creator, name, unit, nextToken) => { - const ic = indexerIntegrationClients[clientNum]; - integrationSearchAssetsResponse = await ic - .searchForAssets() - .index(assetId) - .creator(creator) - .name(name) - .unit(unit) - .nextToken(nextToken) - .do(); - } - ); - - Then( - 'there are {int} assets in the response, the first is {int}.', - (numAssets, firstAssetId) => { - assert.strictEqual( - numAssets, - integrationSearchAssetsResponse.assets.length - ); - if (numAssets === 0) { - return; - } - assert.strictEqual( - firstAssetId, - integrationSearchAssetsResponse.assets[0].index - ); - } - ); - - /// ///////////////////////////////// - // begin rekey test helpers - /// ///////////////////////////////// - - When('I add a rekeyTo field with address {string}', function (address) { - this.txn.reKeyTo = address; - }); + When('I add a rekeyTo field with address {string}', function (address) { + this.txn.reKeyTo = address; + }); When( 'I add a rekeyTo field with the private key algorand address', @@ -5584,6 +4514,43 @@ module.exports = function getSteps(options) { } ); + When( + 'we make a GetLightBlockHeaderProof call for round {int}', + async function (int) { + await this.v2Client.getLightBlockHeaderProof(int).do(); + } + ); + + When('we make a GetStateProof call for round {int}', async function (int) { + await this.v2Client.getStateProof(int).do(); + }); + + Given( + 'a base64 encoded program bytes for heuristic sanity check {string}', + async function (programByteStr) { + this.seeminglyProgram = new Uint8Array( + Buffer.from(programByteStr, 'base64') + ); + } + ); + + When('I start heuristic sanity check over the bytes', async function () { + this.actualErrMsg = undefined; + try { + new algosdk.LogicSigAccount(this.seeminglyProgram); // eslint-disable-line + } catch (e) { + this.actualErrMsg = e.message; + } + }); + + Then( + 'if the heuristic sanity check throws an error, the error contains {string}', + async function (errMsg) { + if (errMsg !== '') assert.ok(this.actualErrMsg.includes(errMsg)); + else assert.strictEqual(this.actualErrMsg, undefined); + } + ); + if (!options.ignoreReturn) { return steps; } diff --git a/tests/cucumber/unit.tags b/tests/cucumber/unit.tags new file mode 100644 index 000000000..2d3f46b8e --- /dev/null +++ b/tests/cucumber/unit.tags @@ -0,0 +1,26 @@ +@unit.abijson +@unit.abijson.byname +@unit.algod +@unit.algod.ledger_refactoring +@unit.applications +@unit.atomic_transaction_composer +@unit.dryrun +@unit.dryrun.trace.application +@unit.feetest +@unit.indexer +@unit.indexer.ledger_refactoring +@unit.indexer.logs +@unit.offline +@unit.program_sanity_check +@unit.rekey +@unit.responses +@unit.responses.231 +@unit.responses.unlimited_assets +@unit.sourcemap +@unit.stateproof.paths +@unit.stateproof.responses +@unit.stateproof.responses.msgp +@unit.tealsign +@unit.transactions +@unit.transactions.keyreg +@unit.transactions.payment