diff --git a/connect/CHANGELOG.md b/connect/CHANGELOG.md index 91dd1d05d..3962da5ae 100644 --- a/connect/CHANGELOG.md +++ b/connect/CHANGELOG.md @@ -2,6 +2,107 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## [0.0.75](https://github.com/permaweb/ao/compare/connect@v0.0.74...connect@v0.0.75) (2025-02-21) + +## [0.0.74](https://github.com/permaweb/ao/compare/connect@v0.0.73...connect@v0.0.74) (2025-02-20) + +## [0.0.73](https://github.com/permaweb/ao/compare/connect@v0.0.72...connect@v0.0.73) (2025-02-19) + + +### Features + +* **connect:** added error for RedirectRequested to resubmit based on Accept-Device ([829e390](https://github.com/permaweb/ao/commit/829e390331291966c8fb293a391f62f714a60440)) +* **connect:** wip added payment getOperator getNodeBalance ([e9b1e17](https://github.com/permaweb/ao/commit/e9b1e1761daeb4af3fa3b45ef036e4f70f4f5836)) + +## [0.0.72](https://github.com/permaweb/ao/compare/connect@v0.0.71...connect@v0.0.72) (2025-02-19) + + +### Bug Fixes + +* **connect:** fix bug with output response on main ([8df51b4](https://github.com/permaweb/ao/commit/8df51b4458a2d467b75e2526bafa55c57de5cc05)) + +## [0.0.71](https://github.com/permaweb/ao/compare/connect@v0.0.70...connect@v0.0.71) (2025-02-18) + + +### Features + +* **connect:** fix res.headers ([e0000cd](https://github.com/permaweb/ao/commit/e0000cd9cb30cb5ff59a07f454c4724f942b4e2b)) + +## [0.0.70](https://github.com/permaweb/ao/compare/connect@v0.0.69...connect@v0.0.70) (2025-02-18) + + +### Features + +* **connect:** add new e2e test branch ([4ea4bdb](https://github.com/permaweb/ao/commit/4ea4bdb4e40a86b930bcb979c5ea00d1e7fd12e8)) +* **connect:** add post mainnet mode with relay device ([1169d0f](https://github.com/permaweb/ao/commit/1169d0f9e7d56c6ce112fd0dc46bef9e4cc8ae88)) +* **connect:** added handlers for errors ([66cf70f](https://github.com/permaweb/ao/commit/66cf70fb9f21ecec8e9275afde2a4f89f5a01248)) +* **connect:** added handlers for errors ([fcdc3bd](https://github.com/permaweb/ao/commit/fcdc3bd90ecd4171a2b226ebf340d8f2d5884354)) +* **connect:** adding support for m2 with request ([56a89d4](https://github.com/permaweb/ao/commit/56a89d410466ef0f32bd359d0ed29c9c13bf6574)) +* **connect:** default type and alg to arweave keys, and sha256 for dataitems and sha512 for httpsig ([0903c1b](https://github.com/permaweb/ao/commit/0903c1b7a16d3148647a043a29f678aa2e24e13a)) +* **connect:** got e2e test working for m0 and m2 ([535da6a](https://github.com/permaweb/ao/commit/535da6a3465c832c8b0f2b573b0f9655bb8bdaf3)) +* **connect:** implement signers api for data items and http sig ([a8229f4](https://github.com/permaweb/ao/commit/a8229f41133db8e77984d936694a85312e0d4655)) +* **connect:** incorporated signer into m0 and m2 ([34eea1c](https://github.com/permaweb/ao/commit/34eea1ccd805352538544c7a3d097bc3053ac1f9)) +* **connect:** inject signer at top level and make optional/override on apis ([14a1054](https://github.com/permaweb/ao/commit/14a105499d5ed800e3fa6cbfaade3d992b05acc5)) +* **connect:** new apis - wip got message and result, next spawn ([36d5aeb](https://github.com/permaweb/ao/commit/36d5aeb6c31f5968f429875b7f5c04214064d9f0)) +* **connect:** new relay mode with get working ([268d70f](https://github.com/permaweb/ao/commit/268d70fd887be1baa73a70785118801073d1cc41)) +* **connect:** refactor api - wip ([0e7829c](https://github.com/permaweb/ao/commit/0e7829ce2b7237cf4454f2cf54f1e2c4fc0d9854)) +* **connect:** refactor api - wip ([910fa14](https://github.com/permaweb/ao/commit/910fa14610c97147846191df5bbd2225cc75546d)) +* **connect:** refactor api - wip ([9843dd7](https://github.com/permaweb/ao/commit/9843dd7f4ab4e68b8ad9be71e0b611587976d487)) +* **connect:** return res ([59561f2](https://github.com/permaweb/ao/commit/59561f2ab60145832164a855bf7a8dfb69cb4156)) +* **connect:** set defaults for relay and mainmode ([6b5d9f2](https://github.com/permaweb/ao/commit/6b5d9f28f22a7398774e437f71d9cd8e932b6439)) +* **connect:** side effects use new signers api shape ([6668a16](https://github.com/permaweb/ao/commit/6668a16baf7d25bc0973cf75771b44c7119e6504)) +* **connect:** signer api and implement for browser and node ([71d4661](https://github.com/permaweb/ao/commit/71d4661623f87a6cd7707c07be7c958bc0e2a6c2)) +* **connect:** use dha-team/arbundles for data item and deepHash impl ([4d710d6](https://github.com/permaweb/ao/commit/4d710d67813f73f630ebf74e712149f28eb6a726)) +* **connect:** verify data item sigs. request permission for signing data items in browser ([46c2870](https://github.com/permaweb/ao/commit/46c287042382d34690ce876727dc1f7c9912c341)) + + +### Bug Fixes + +* **connect:** do not overwrite MODE in when relay mode is detected ([3559529](https://github.com/permaweb/ao/commit/3559529df883be4b3460074e1fb20bcf8e76b110)) + +## [0.0.69](https://github.com/permaweb/ao/compare/connect@v0.0.68...connect@v0.0.69) (2025-02-12) + + +### Features + +* **connect:** encode fields into headers if valid and within max header value size ([6abeb10](https://github.com/permaweb/ao/commit/6abeb10274673689fb03b7099f0973953f785666)) +* **connect:** implement send on relay and mainnet mode ([b47c0bf](https://github.com/permaweb/ao/commit/b47c0bf959cb871c59ce5b6e35a43ef1cb9a257c)) +* **connect:** M2 compability with HyperBEAM ([d296139](https://github.com/permaweb/ao/commit/d296139a5daa9f4826a1434b1fcd0ada0035e969)) +* **connect:** overload connect args and returns based on MODE ([499676e](https://github.com/permaweb/ao/commit/499676eb00f87b7513e770f0627ee8619bcf069f)) +* **connect:** send multipart http message with deterministic boundary to HB ([c81208e](https://github.com/permaweb/ao/commit/c81208ea86db1e3bfc7f08c3ef6c918bb50695ee)) + +## [0.0.68](https://github.com/permaweb/ao/compare/connect@v0.0.67...connect@v0.0.68) (2025-02-10) + + +### Bug Fixes + +* **connect:** use staticDataItemSigner in relay mode ([d639682](https://github.com/permaweb/ao/commit/d639682f321d8fbcbabfc0430337e595e707a6b7)) + +## [0.0.67](https://github.com/permaweb/ao/compare/connect@v0.0.66...connect@v0.0.67) (2025-02-07) + + +### ⚠ BREAKING CHANGES + +* **connect:** remove connect.hb. Add modes and corresponding *_URL args to connect() + +### Features + +* **connect:** remove connect.hb. Add modes and corresponding *_URL args to connect() ([226a20a](https://github.com/permaweb/ao/commit/226a20aa4da9b065ef2af9c8ebef36c4f70998fd)) + +## [0.0.66](https://github.com/permaweb/ao/compare/connect@v0.0.65...connect@v0.0.66) (2025-02-07) + + +### Features + +* **connect:** hb mode has result on hb ([214309f](https://github.com/permaweb/ao/commit/214309f16731f24073232dea1eb58074c7e10173)) +* **connect:** hb mode has spawn on hb ([5ece939](https://github.com/permaweb/ao/commit/5ece93924e4d263ff8f2574b32606965b948d0fb)) +* **connect:** wip starting to build out http signed message to hb ([ba2439d](https://github.com/permaweb/ao/commit/ba2439da0f231202d65fcea9061cece52a947b2a)) + + +### Bug Fixes + +* **connect:** optionally add tags to hb headers. Change routes to non-process variants to start ([1cb6ef6](https://github.com/permaweb/ao/commit/1cb6ef651ce060f8eed1010c54ab5644ea85a34b)) + ## [0.0.65](https://github.com/permaweb/ao/compare/connect@v0.0.64...connect@v0.0.65) (2025-02-03) diff --git a/connect/README.md b/connect/README.md index 5843a1111..04bfe86c0 100644 --- a/connect/README.md +++ b/connect/README.md @@ -25,7 +25,7 @@ This module will run in a browser or server environment. - [`dryrun`](#dryrun) - [`assign`](#assign) - [Environment Variables](#environment-variables) - - [`createDataItemSigner`](#createdataitemsigner) + - [`createSigner`](#createsigner) - [`serializeCron`](#serializecron) - [Debug Logging](#debug-logging) - [Testing](#testing) @@ -53,10 +53,13 @@ The duration of this document will use `ESM` for examples #### Usage with Webpack -AOConnect by default exposes a file designed to be used in the browser. If you're using Webpack to bundle an app for the server and are having issues, you may need to update import statements to point at an implementation for node like so. +AOConnect by default exposes a file designed to be used in the browser. If +you're using Webpack to bundle an app for the server and are having issues, you +may need to update import statements to point at an implementation for node like +so. ```js -import { createDataItemSigner, message } from "@permaweb/aoconnect/node"; +import { createSigner, message } from "@permaweb/aoconnect/node"; ``` ### API @@ -68,7 +71,6 @@ Read the result of the message evaluation from an `ao` Compute Unit `cu` ```js import { result } from "@permaweb/aoconnect"; - let { Messages, Spawns, Output, Error } = await result({ message: "l3hbt-rIJ_dr9at-eQ3EVajHWMnxPNm9eBtXpzsFWZc", process: "5SGJUlPwlenkyuG9-xWh0Rcf0azm8XEd5RBTiutgWAg", @@ -106,11 +108,11 @@ Parameters send a message to an `ao` Message Unit `mu` targeting an ao `process`. ```js -import { createDataItemSigner, message } from "@permaweb/aoconnect"; +import { createSigner, message } from "@permaweb/aoconnect"; const messageId = await message({ process, - signer: createDataItemSigner(wallet), + signer: createSigner(wallet), anchor, tags, data, @@ -124,12 +126,12 @@ const messageId = await message({ Spawn an `ao` process, assigning the `ao` Scheduler to schedule its messages ```js -import { createDataItemSigner, spawn } from "@permaweb/aoconnect"; +import { createSigner, spawn } from "@permaweb/aoconnect"; const processId = await spawn({ module, scheduler, - signer: createDataItemSigner(wallet), + signer: createSigner(wallet), tags, data, }); @@ -142,9 +144,12 @@ can specify those components by providing their urls to `connect`. You can currently specify: - `GATEWAY_URL`: (currently only used as the default host for `GRAPHQL_URL`) -- `GRAPHQL_URL`: the Arweave GraphQL gateway to use for querying data from Arweave. (defaults to `${GATEWAY_URL}/graphql`) -- `GRAPHQL_MAX_RETRIES`: the number of times to retry querying the gateway, utilizing an exponential backoff (defaults to `0`) -- `GRAPHQL_RETRY_BACKOFF`: the initial backoff, in milliseconds (moot if `GRAPHQL_MAX_RETRIES` is set to `0`) (defaults to `300`) +- `GRAPHQL_URL`: the Arweave GraphQL gateway to use for querying data from + Arweave. (defaults to `${GATEWAY_URL}/graphql`) +- `GRAPHQL_MAX_RETRIES`: the number of times to retry querying the gateway, + utilizing an exponential backoff (defaults to `0`) +- `GRAPHQL_RETRY_BACKOFF`: the initial backoff, in milliseconds (moot if + `GRAPHQL_MAX_RETRIES` is set to `0`) (defaults to `300`) - `MU_URL`: The Messenger Unit URL - `CU_URL`: The Compute Unit URL @@ -161,7 +166,9 @@ const { spawn, message, result } = connect({ }); ``` -> If `GATEWAY_URL` is set but `GRAPHQL_URL` is _not_ set, then the `GATEWAY_URL` provided **MUST** have a `/graphql` endpoint that serves the Arweave Gateway GraphQL Server. ie. `https://arweave.net/graphql` +> If `GATEWAY_URL` is set but `GRAPHQL_URL` is _not_ set, then the `GATEWAY_URL` +> provided **MUST** have a `/graphql` endpoint that serves the Arweave Gateway +> GraphQL Server. ie. `https://arweave.net/graphql` If any value is not provided, a library default will be used. In this sense, invoking `connect()` with no parameters or an empty object is functionally @@ -186,11 +193,11 @@ using this monitor method, ao users can initiate the subscription service for cron messages. ```js -import { createDataItemSigner, monitor } from "@permaweb/aoconnect"; +import { createSigner, monitor } from "@permaweb/aoconnect"; const result = await monitor({ process, - signer: createDataItemSigner(wallet), + signer: createSigner(wallet), }); ``` @@ -202,7 +209,7 @@ create a read message to return the current value of memory. For example, a balance of a token, or a result of a transfer, etc. ```js -import { createDataItemSigner, dryrun } from "@permaweb/aoconnect"; +import { createSigner, dryrun } from "@permaweb/aoconnect"; const result = await dryrun({ process: 'PROCESSID', @@ -223,9 +230,9 @@ Create an Assignment for an `ao` process import { assign } from "@permaweb/aoconnect"; const processId = await assign({ - process: 'process-id', - message: 'message-id', - exclude: ['Data', 'Tags', 'etc.'] // optional list of DataItem fields to exclude + process: "process-id", + message: "message-id", + exclude: ["Data", "Tags", "etc."], // optional list of DataItem fields to exclude }); ``` @@ -235,9 +242,9 @@ Create a Assignment for an `ao` process with an L1 transaction import { assign } from "@permaweb/aoconnect"; const processId = await assign({ - process: 'process-id', - message: 'txid', - baseLayer: true + process: "process-id", + message: "txid", + baseLayer: true, }); ``` @@ -254,13 +261,16 @@ In the browser, you can use `globalThis` to set these values. > module. If this is not possible, consider using [`connect`](#connect) and > passing in values from the environment that way. -#### `createDataItemSigner` +#### `createSigner` + +> `createDataItemSigner` is available for backwards compatibility, but is an +> alias for `createSigner`. -`message` and `spawn` both require signing a DataItem with a wallet. +In `mainnet` mode all HTTP Messages and Data Items must be signed. -`createDataItemSigner` is a convenience api that, given a wallet, returns a -function that can be passed to both `message` and `spawn` in order to properly -sign DataItems. +`createSigner` is a convenience api that, given a wallet, returns a function +that can be passed as `signer` to `connect` in order to properly sign DataItems +and HTTP Signed Messages. The library provides a browser compatible and node compatible version that you can use OOTB. @@ -269,28 +279,117 @@ The `browser` compatible versions expects an instance of `window.arweaveWallet` to be passed to it: ```js -import { createDataItemSigner } from "@permaweb/aoconnect"; +import { createSigner } from "@permaweb/aoconnect"; -const signer = createDataItemSigner(globalThis.arweaveWallet); +const signer = createSigner(globalThis.arweaveWallet); ``` The `node` compatible versions expects a JWK interface to be passed to it: ```js import fs from "node:fs"; -import { createDataItemSigner } from "@permaweb/aoconnect"; +import { createSigner } from "@permaweb/aoconnect"; const wallet = JSON.parse(fs.readFileSync(process.env.PATH_TO_WALLET)); -const signer = createDataItemSigner(wallet); +const signer = createSigner(wallet); ``` -You can also implement your own `createDataItemSigner`, as long as it satisfies -the api. Here is what the API looks like in TypeScript: +You can also implement your own `signer`, as long as it satisfies the api. Here +is what the API looks like in TypeScript: ```ts -type CreateDataItemSigner = (wallet: any): - (args: { data: any, tags?: { name: string, value: string}[], target?: string, anchor?: string }): - Promise<{ id: string, raw: ArrayBuffer }> +type KeyPublicMeta = { + publicKey: string; + alg?: string; + type?: number; +}; + +type CreateSignatureData = (key: KeyPublicMeta): Promise; + +type SignatureDataType = "ans104" | "httpsig"; + +type SignatureResult = { + signature: Uint8Array | string; + address: string; +}; + +type RawSigner = ( + create: CreateSignatureData, + type: SignatureDataType, +): Promise; +``` + +`create` will be a function that expects to receive, as its first arugments, an +object that has the following fields: + +- `publicKey`: the `publicKey` of the credentials, either as a `Uint8Array`, or + **base64 URL encoded** `string`. +- `alg` (optional): the algorithm used for signing. If not provided, the default + is `rsa-pss-sha512` for HTTP Signed Messages and `rsa-v1_5-sha256` for ANS-104 + Data items (these are currently the only supported algorithms) +- `type` (optional): the canonical signature type. If not provided, the default + is `1` which means `arweave` keys and signature type. (Currently only `1` is + supported) + +The second argument, `type`, in most cases can be ignored, but is provided in +cases where different behavior is desired for signing the signature base of +DataItems vs. HTTP Signed Messages. + +In order to construct the data to sign, the `signer` MUST call `create`, +providing it the information described above. `create` will return a Promise +that resolves to a `Uint8Array` that will contain the data to be signed. Once +the signer has signed this data, it should return a `Promise` that resolves to a +`SignatureResult` with the following fields: + +- `signature`: the raw bytes of the signature produced, or a **base64 URL + encoded** `string` +- `address`: be the canonical `address` associated with the credentials. + +Note that, in this signing flow, that the private credentials never need to be +known by `aoconnect`. In this way, the actual signing operation may be an opaque +operation -- a black box. Upon receiving the `signature` and `address` from the +signer, `aoconnect` will handle encoding them, along with associated metadata +such as a public key, `alg` and key type, in the proper context: as the +signature in an ANS-104 Data Item, or as a Signature on an HTTP Signed Message. + +For example, here is a custom signer where the credentials and signing are +stored remotely in a hypothetical vault service: + +```js +import { connect } from 'aoconnect' + +const myAwesomeSigner = async (create) => { + /** + * First retrieve the public key information, + * and invoke create to produce the signature base + */ + const dataToSign = await fetch('https://my.vault.io/.well-known/jwks.json') + .then((res) => res.json()) + .then(async ([firstKey]) => { + const publicKey = Buffer.from(firstKey.n, 'base64url') + return create({ publicKey }) + }) + + /** + * Now sign the data + */ + return fetch( + 'https://my.vault.io', + { + method: 'POST', + body: dataToSign, + ... + } + ).then(async (res) => ({ + signature: res.headers.get('signature'), + address: res.headers.get('address') + })) +} + +/** + * Inject the signer + */ +const { message } = connect({ signer: myAwesomeSigner }) ``` #### `serializeCron` @@ -304,12 +403,12 @@ const cron = { interval: "10-minutes", tags: [ { name: "Foo", value: "bar" }, - { name: "Bar", value: "foo" } - ] -} + { name: "Bar", value: "foo" }, + ], +}; -const tags = serializeCron(cron) -/* +const tags = serializeCron(cron); +/* tags = [ { name: "Cron-Interval", value: "10-minutes" }, { name: "Cron-Tag-Foo", value: "bar" }, diff --git a/connect/esbuild.js b/connect/esbuild.js index 120dcc719..7a952e87e 100644 --- a/connect/esbuild.js +++ b/connect/esbuild.js @@ -54,9 +54,19 @@ await esbuild.build({ * TODO: maybe we can just simply import createSignatureBase * from 'node-http-signatures/cavage/index.js' * and that will treeshake enough? + * + * "events" and "stream" have to be polyfilled for @dha-team/arbundles -- + * really not great. */ plugins: [ - nodeModulesPolyfillPlugin({ modules: { crypto: true, constants: true } }) + nodeModulesPolyfillPlugin({ + modules: { + crypto: true, + constants: true, + events: true, + stream: true + } + }) ], platform: 'browser', format: 'esm', diff --git a/connect/package-lock.json b/connect/package-lock.json index 1eb217b3c..8bbbf8519 100644 --- a/connect/package-lock.json +++ b/connect/package-lock.json @@ -1,22 +1,25 @@ { "name": "@permaweb/aoconnect", - "version": "0.0.65", + "version": "0.0.75", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@permaweb/aoconnect", - "version": "0.0.65", + "version": "0.0.75", "dependencies": { + "@dha-team/arbundles": "^1.0.1", "@permaweb/ao-scheduler-utils": "~0.0.25", "@permaweb/protocol-tag-utils": "~0.0.2", + "axios": "^1.7.9", + "base64url": "^3.0.1", "buffer": "^6.0.3", "debug": "^4.4.0", "http-message-signatures": "^1.0.4", "hyper-async": "^1.1.2", "mnemonist": "^0.39.8", "ramda": "^0.30.1", - "warp-arbundles": "^1.0.4", + "structured-headers": "^2.0.0", "zod": "^3.24.1" }, "devDependencies": { @@ -29,6 +32,33 @@ "node": ">=18" } }, + "node_modules/@dha-team/arbundles": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@dha-team/arbundles/-/arbundles-1.0.1.tgz", + "integrity": "sha512-cgVxhZJLK1HG2+vcRBZ0CYGpxz7mA2QvLaspcw2gOzb2V/ZUxlifUu1aufBK3iz63Ww2OhgO0j9DstRJqrG1uA==", + "license": "Apache-2.0", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/providers": "^5.7.2", + "@ethersproject/signing-key": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/wallet": "^5.7.0", + "@noble/ed25519": "^1.6.1", + "arweave": "^1.15.1", + "base64url": "^3.0.1", + "bs58": "^4.0.1", + "keccak": "^3.0.2", + "secp256k1": "^5.0.0" + }, + "optionalDependencies": { + "@randlabs/myalgo-connect": "^1.1.2", + "algosdk": "^1.13.1", + "arweave-stream-tx": "^1.1.0", + "multistream": "^4.1.0", + "tmp-promise": "^3.0.2" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", @@ -454,170 +484,1020 @@ "node": ">=18" } }, - "node_modules/@fastify/busboy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", - "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==", - "engines": { - "node": ">=14" + "node_modules/@ethersproject/abstract-provider": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz", + "integrity": "sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/networks": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/web": "^5.7.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz", + "integrity": "sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0" } }, - "node_modules/@permaweb/ao-scheduler-utils": { - "version": "0.0.25", - "resolved": "https://registry.npmjs.org/@permaweb/ao-scheduler-utils/-/ao-scheduler-utils-0.0.25.tgz", - "integrity": "sha512-b0UYSTgnLMIYLScrfNBgcqK7ZMmd78L3J0Jz4RIsIq2P5PtkdRqQ7fYqLlltg7bD1f3dvl4TkO1925ED4ei7LA==", + "node_modules/@ethersproject/address": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz", + "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", "dependencies": { - "lru-cache": "^10.2.2", - "ramda": "^0.30.0", - "zod": "^3.23.5" - }, - "engines": { - "node": ">=18" + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/rlp": "^5.7.0" } }, - "node_modules/@permaweb/protocol-tag-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@permaweb/protocol-tag-utils/-/protocol-tag-utils-0.0.2.tgz", - "integrity": "sha512-2IiKu71W7pkHKIzxabCGQ5q8DSppZaE/sPcPF2hn+OWwfe04M7b5X5LHRXQNPRuxHWuioieGdPQb3F7apOlffQ==", - "license": "MIT" + "node_modules/@ethersproject/base64": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.7.0.tgz", + "integrity": "sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0" + } }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, + "node_modules/@ethersproject/basex": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.7.0.tgz", + "integrity": "sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/properties": "^5.7.0" } }, - "node_modules/arconnect": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/arconnect/-/arconnect-0.4.2.tgz", - "integrity": "sha512-Jkpd4QL3TVqnd3U683gzXmZUVqBUy17DdJDuL/3D9rkysLgX6ymJ2e+sR+xyZF5Rh42CBqDXWNMmCjBXeP7Gbw==", + "node_modules/@ethersproject/bignumber": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz", + "integrity": "sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", "dependencies": { - "arweave": "^1.10.13" + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "bn.js": "^5.2.1" } }, - "node_modules/arweave": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/arweave/-/arweave-1.15.5.tgz", - "integrity": "sha512-Zj3b8juz1ZtDaQDPQlzWyk2I4wZPx3RmcGq8pVJeZXl2Tjw0WRy5ueHPelxZtBLqCirGoZxZEAFRs6SZUSCBjg==", + "node_modules/@ethersproject/bignumber/node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "license": "MIT" + }, + "node_modules/@ethersproject/bytes": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz", + "integrity": "sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], "license": "MIT", "dependencies": { - "arconnect": "^0.4.2", - "asn1.js": "^5.4.1", - "base64-js": "^1.5.1", - "bignumber.js": "^9.0.2" - }, - "engines": { - "node": ">=18" + "@ethersproject/logger": "^5.7.0" } }, - "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "node_modules/@ethersproject/constants": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.7.0.tgz", + "integrity": "sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" + "@ethersproject/bignumber": "^5.7.0" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "node_modules/@ethersproject/hash": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.7.0.tgz", + "integrity": "sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==", "funding": [ { - "type": "github", - "url": "https://github.com/sponsors/feross" + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" }, { - "type": "patreon", - "url": "https://www.patreon.com/feross" + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/base64": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@ethersproject/hdnode": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.7.0.tgz", + "integrity": "sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" }, { - "type": "consulting", - "url": "https://feross.org/support" + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" } - ] + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/basex": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/pbkdf2": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/sha2": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0", + "@ethersproject/strings": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/wordlists": "^5.7.0" + } + }, + "node_modules/@ethersproject/json-wallets": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz", + "integrity": "sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hdnode": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/pbkdf2": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/random": "^5.7.0", + "@ethersproject/strings": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz", + "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "js-sha3": "0.8.0" + } }, - "node_modules/base64url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", - "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", - "engines": { - "node": ">=6.0.0" + "node_modules/@ethersproject/logger": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz", + "integrity": "sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT" + }, + "node_modules/@ethersproject/networks": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz", + "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.7.0" } }, - "node_modules/bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", - "engines": { - "node": "*" + "node_modules/@ethersproject/pbkdf2": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz", + "integrity": "sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/sha2": "^5.7.0" } }, - "node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + "node_modules/@ethersproject/properties": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.7.0.tgz", + "integrity": "sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.7.0" + } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "node_modules/@ethersproject/providers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.2.tgz", + "integrity": "sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==", "funding": [ { - "type": "github", - "url": "https://github.com/sponsors/feross" + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" }, { - "type": "patreon", - "url": "https://www.patreon.com/feross" + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/base64": "^5.7.0", + "@ethersproject/basex": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/networks": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/random": "^5.7.0", + "@ethersproject/rlp": "^5.7.0", + "@ethersproject/sha2": "^5.7.0", + "@ethersproject/strings": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/web": "^5.7.0", + "bech32": "1.1.4", + "ws": "7.4.6" + } + }, + "node_modules/@ethersproject/random": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.7.0.tgz", + "integrity": "sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" }, { - "type": "consulting", - "url": "https://feross.org/support" + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0" } }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "node_modules/@ethersproject/rlp": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.7.0.tgz", + "integrity": "sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0" } }, - "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "node_modules/@ethersproject/sha2": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.7.0.tgz", + "integrity": "sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.7.0.tgz", + "integrity": "sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "bn.js": "^5.2.1", + "elliptic": "6.5.4", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/signing-key/node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "license": "MIT" + }, + "node_modules/@ethersproject/strings": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", + "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz", + "integrity": "sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/rlp": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0" + } + }, + "node_modules/@ethersproject/wallet": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.7.0.tgz", + "integrity": "sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/hdnode": "^5.7.0", + "@ethersproject/json-wallets": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/random": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/wordlists": "^5.7.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz", + "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/base64": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@ethersproject/wordlists": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.7.0.tgz", + "integrity": "sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@noble/ed25519": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", + "integrity": "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/@permaweb/ao-scheduler-utils": { + "version": "0.0.25", + "resolved": "https://registry.npmjs.org/@permaweb/ao-scheduler-utils/-/ao-scheduler-utils-0.0.25.tgz", + "integrity": "sha512-b0UYSTgnLMIYLScrfNBgcqK7ZMmd78L3J0Jz4RIsIq2P5PtkdRqQ7fYqLlltg7bD1f3dvl4TkO1925ED4ei7LA==", + "dependencies": { + "lru-cache": "^10.2.2", + "ramda": "^0.30.0", + "zod": "^3.23.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@permaweb/protocol-tag-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@permaweb/protocol-tag-utils/-/protocol-tag-utils-0.0.2.tgz", + "integrity": "sha512-2IiKu71W7pkHKIzxabCGQ5q8DSppZaE/sPcPF2hn+OWwfe04M7b5X5LHRXQNPRuxHWuioieGdPQb3F7apOlffQ==", + "license": "MIT" + }, + "node_modules/@randlabs/communication-bridge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@randlabs/communication-bridge/-/communication-bridge-1.0.1.tgz", + "integrity": "sha512-CzS0U8IFfXNK7QaJFE4pjbxDGfPjbXBEsEaCn9FN15F+ouSAEUQkva3Gl66hrkBZOGexKFEWMwUHIDKpZ2hfVg==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@randlabs/myalgo-connect": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@randlabs/myalgo-connect/-/myalgo-connect-1.4.2.tgz", + "integrity": "sha512-K9hEyUi7G8tqOp7kWIALJLVbGCByhilcy6123WfcorxWwiE1sbQupPyIU5f3YdQK6wMjBsyTWiLW52ZBMp7sXA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@randlabs/communication-bridge": "1.0.1" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", + "license": "MIT" + }, + "node_modules/algo-msgpack-with-bigint": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/algo-msgpack-with-bigint/-/algo-msgpack-with-bigint-2.1.1.tgz", + "integrity": "sha512-F1tGh056XczEaEAqu7s+hlZUDWwOBT70Eq0lfMpBP2YguSQVyxRbprLq5rELXKQOyOaixTWYhMeMQMzP0U5FoQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/algosdk": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/algosdk/-/algosdk-1.24.1.tgz", + "integrity": "sha512-9moZxdqeJ6GdE4N6fA/GlUP4LrbLZMYcYkt141J4Ss68OfEgH9qW0wBuZ3ZOKEx/xjc5bg7mLP2Gjg7nwrkmww==", + "license": "MIT", + "optional": true, + "dependencies": { + "algo-msgpack-with-bigint": "^2.1.1", + "buffer": "^6.0.2", + "cross-fetch": "^3.1.5", + "hi-base32": "^0.5.1", + "js-sha256": "^0.9.0", + "js-sha3": "^0.8.0", + "js-sha512": "^0.8.0", + "json-bigint": "^1.0.0", + "tweetnacl": "^1.0.3", + "vlq": "^2.0.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/arconnect": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/arconnect/-/arconnect-0.4.2.tgz", + "integrity": "sha512-Jkpd4QL3TVqnd3U683gzXmZUVqBUy17DdJDuL/3D9rkysLgX6ymJ2e+sR+xyZF5Rh42CBqDXWNMmCjBXeP7Gbw==", + "dependencies": { + "arweave": "^1.10.13" + } + }, + "node_modules/arweave": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/arweave/-/arweave-1.15.5.tgz", + "integrity": "sha512-Zj3b8juz1ZtDaQDPQlzWyk2I4wZPx3RmcGq8pVJeZXl2Tjw0WRy5ueHPelxZtBLqCirGoZxZEAFRs6SZUSCBjg==", + "license": "MIT", + "dependencies": { + "arconnect": "^0.4.2", + "asn1.js": "^5.4.1", + "base64-js": "^1.5.1", + "bignumber.js": "^9.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/arweave-stream-tx": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/arweave-stream-tx/-/arweave-stream-tx-1.2.2.tgz", + "integrity": "sha512-bNt9rj0hbAEzoUZEF2s6WJbIz8nasZlZpxIw03Xm8fzb9gRiiZlZGW3lxQLjfc9Z0VRUWDzwtqoYeEoB/JDToQ==", + "optional": true, + "dependencies": { + "exponential-backoff": "^3.1.0" + }, + "peerDependencies": { + "arweave": "^1.10.0" + } + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/base-x": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", + "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, + "node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", "dev": true, "hasInstallScript": true, @@ -680,6 +1560,173 @@ "integrity": "sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==", "dev": true }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hi-base32": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/hi-base32/-/hi-base32-0.5.1.tgz", + "integrity": "sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==", + "license": "MIT", + "optional": true + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/http-message-signatures": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/http-message-signatures/-/http-message-signatures-1.0.4.tgz", @@ -689,6 +1736,16 @@ "structured-headers": "^1.0.1" } }, + "node_modules/http-message-signatures/node_modules/structured-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-1.0.1.tgz", + "integrity": "sha512-QYBxdBtA4Tl5rFPuqmbmdrS9kbtren74RTJTcs0VSQNVV5iRhJD4QlYTLD0+81SBwUQctjEQzjTRI3WG4DzICA==", + "license": "MIT", + "engines": { + "node": ">= 14", + "npm": ">=6" + } + }, "node_modules/hyper-async": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/hyper-async/-/hyper-async-1.1.2.tgz", @@ -718,6 +1775,51 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==", + "license": "MIT", + "optional": true + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, + "node_modules/js-sha512": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", + "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==", + "license": "MIT", + "optional": true + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/keccak": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", + "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/local-pkg": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", @@ -741,11 +1843,47 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, "node_modules/mlly": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", @@ -774,11 +1912,84 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multistream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", + "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/obliterator": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/pathe": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", @@ -798,6 +2009,12 @@ "pathe": "^2.0.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/ramda": { "version": "0.30.1", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", @@ -808,6 +2025,20 @@ "url": "https://opencollective.com/ramda" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/resolve.exports": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", @@ -818,21 +2049,126 @@ "node": ">=10" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", + "license": "MIT" + }, + "node_modules/secp256k1": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.1.tgz", + "integrity": "sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "elliptic": "^6.5.7", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/secp256k1/node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/secp256k1/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/structured-headers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-1.0.1.tgz", - "integrity": "sha512-QYBxdBtA4Tl5rFPuqmbmdrS9kbtren74RTJTcs0VSQNVV5iRhJD4QlYTLD0+81SBwUQctjEQzjTRI3WG4DzICA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-2.0.0.tgz", + "integrity": "sha512-qX9utDaDolZWTcAep0EZDSQASTu4mx16Jh5dBSpplfvK3ro1CF8mWqRByAwncRojnlaCCUZA2jPcCqKNnp7uCA==", "license": "MIT", "engines": { - "node": ">= 14", + "node": ">=18", "npm": ">=6" } }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT", + "optional": true + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense", + "optional": true + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -854,39 +2190,63 @@ "dev": true, "license": "MIT" }, - "node_modules/undici": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz", - "integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==", + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vlq": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-2.0.4.tgz", + "integrity": "sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA==", "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, - "engines": { - "node": ">=14.0" - } + "optional": true }, - "node_modules/warp-arbundles": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/warp-arbundles/-/warp-arbundles-1.0.4.tgz", - "integrity": "sha512-KeRac/EJ7VOK+v5+PSMh2SrzpCKOAFnJICLlqZWt6qPkDCzVwcrNE5wFxOlEk5U170ewMDAB3e86UHUblevXpw==", + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "optional": true, "dependencies": { - "arweave": "^1.13.7", - "base64url": "^3.0.1", - "buffer": "^6.0.3", - "warp-isomorphic": "^1.0.7" + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, - "node_modules/warp-isomorphic": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/warp-isomorphic/-/warp-isomorphic-1.0.7.tgz", - "integrity": "sha512-fXHbUXwdYqPm9fRPz8mjv5ndPco09aMQuTe4kXfymzOq8V6F3DLsg9cIafxvjms9/mc6eijzkLBJ63yjEENEjA==", - "dependencies": { - "buffer": "^6.0.3", - "undici": "^5.19.1" - }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "optional": true + }, + "node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "license": "MIT", "engines": { - "node": ">=16.8.0" + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/zod": { @@ -900,6 +2260,30 @@ } }, "dependencies": { + "@dha-team/arbundles": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@dha-team/arbundles/-/arbundles-1.0.1.tgz", + "integrity": "sha512-cgVxhZJLK1HG2+vcRBZ0CYGpxz7mA2QvLaspcw2gOzb2V/ZUxlifUu1aufBK3iz63Ww2OhgO0j9DstRJqrG1uA==", + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/providers": "^5.7.2", + "@ethersproject/signing-key": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/wallet": "^5.7.0", + "@noble/ed25519": "^1.6.1", + "@randlabs/myalgo-connect": "^1.1.2", + "algosdk": "^1.13.1", + "arweave": "^1.15.1", + "arweave-stream-tx": "^1.1.0", + "base64url": "^3.0.1", + "bs58": "^4.0.1", + "keccak": "^3.0.2", + "multistream": "^4.1.0", + "secp256k1": "^5.0.0", + "tmp-promise": "^3.0.2" + } + }, "@esbuild/aix-ppc64": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", @@ -1075,10 +2459,339 @@ "dev": true, "optional": true }, - "@fastify/busboy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", - "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==" + "@ethersproject/abstract-provider": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz", + "integrity": "sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==", + "requires": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/networks": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/web": "^5.7.0" + } + }, + "@ethersproject/abstract-signer": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz", + "integrity": "sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==", + "requires": { + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0" + } + }, + "@ethersproject/address": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz", + "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==", + "requires": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/rlp": "^5.7.0" + } + }, + "@ethersproject/base64": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.7.0.tgz", + "integrity": "sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==", + "requires": { + "@ethersproject/bytes": "^5.7.0" + } + }, + "@ethersproject/basex": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.7.0.tgz", + "integrity": "sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==", + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/properties": "^5.7.0" + } + }, + "@ethersproject/bignumber": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz", + "integrity": "sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==", + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "bn.js": "^5.2.1" + }, + "dependencies": { + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + } + } + }, + "@ethersproject/bytes": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz", + "integrity": "sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==", + "requires": { + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/constants": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.7.0.tgz", + "integrity": "sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==", + "requires": { + "@ethersproject/bignumber": "^5.7.0" + } + }, + "@ethersproject/hash": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.7.0.tgz", + "integrity": "sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==", + "requires": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/base64": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "@ethersproject/hdnode": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.7.0.tgz", + "integrity": "sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==", + "requires": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/basex": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/pbkdf2": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/sha2": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0", + "@ethersproject/strings": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/wordlists": "^5.7.0" + } + }, + "@ethersproject/json-wallets": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz", + "integrity": "sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==", + "requires": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hdnode": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/pbkdf2": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/random": "^5.7.0", + "@ethersproject/strings": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "@ethersproject/keccak256": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz", + "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==", + "requires": { + "@ethersproject/bytes": "^5.7.0", + "js-sha3": "0.8.0" + } + }, + "@ethersproject/logger": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz", + "integrity": "sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==" + }, + "@ethersproject/networks": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz", + "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==", + "requires": { + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/pbkdf2": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz", + "integrity": "sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==", + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/sha2": "^5.7.0" + } + }, + "@ethersproject/properties": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.7.0.tgz", + "integrity": "sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==", + "requires": { + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/providers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.2.tgz", + "integrity": "sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==", + "requires": { + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/base64": "^5.7.0", + "@ethersproject/basex": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/networks": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/random": "^5.7.0", + "@ethersproject/rlp": "^5.7.0", + "@ethersproject/sha2": "^5.7.0", + "@ethersproject/strings": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/web": "^5.7.0", + "bech32": "1.1.4", + "ws": "7.4.6" + } + }, + "@ethersproject/random": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.7.0.tgz", + "integrity": "sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==", + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/rlp": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.7.0.tgz", + "integrity": "sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==", + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/sha2": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.7.0.tgz", + "integrity": "sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==", + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "hash.js": "1.1.7" + } + }, + "@ethersproject/signing-key": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.7.0.tgz", + "integrity": "sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==", + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "bn.js": "^5.2.1", + "elliptic": "6.5.4", + "hash.js": "1.1.7" + }, + "dependencies": { + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + } + } + }, + "@ethersproject/strings": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", + "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==", + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "@ethersproject/transactions": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz", + "integrity": "sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==", + "requires": { + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/rlp": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0" + } + }, + "@ethersproject/wallet": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.7.0.tgz", + "integrity": "sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==", + "requires": { + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/hdnode": "^5.7.0", + "@ethersproject/json-wallets": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/random": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/wordlists": "^5.7.0" + } + }, + "@ethersproject/web": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz", + "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==", + "requires": { + "@ethersproject/base64": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "@ethersproject/wordlists": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.7.0.tgz", + "integrity": "sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==", + "requires": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "@noble/ed25519": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", + "integrity": "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==" }, "@permaweb/ao-scheduler-utils": { "version": "0.0.25", @@ -1095,12 +2808,56 @@ "resolved": "https://registry.npmjs.org/@permaweb/protocol-tag-utils/-/protocol-tag-utils-0.0.2.tgz", "integrity": "sha512-2IiKu71W7pkHKIzxabCGQ5q8DSppZaE/sPcPF2hn+OWwfe04M7b5X5LHRXQNPRuxHWuioieGdPQb3F7apOlffQ==" }, + "@randlabs/communication-bridge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@randlabs/communication-bridge/-/communication-bridge-1.0.1.tgz", + "integrity": "sha512-CzS0U8IFfXNK7QaJFE4pjbxDGfPjbXBEsEaCn9FN15F+ouSAEUQkva3Gl66hrkBZOGexKFEWMwUHIDKpZ2hfVg==", + "optional": true + }, + "@randlabs/myalgo-connect": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@randlabs/myalgo-connect/-/myalgo-connect-1.4.2.tgz", + "integrity": "sha512-K9hEyUi7G8tqOp7kWIALJLVbGCByhilcy6123WfcorxWwiE1sbQupPyIU5f3YdQK6wMjBsyTWiLW52ZBMp7sXA==", + "optional": true, + "requires": { + "@randlabs/communication-bridge": "1.0.1" + } + }, "acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true }, + "aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==" + }, + "algo-msgpack-with-bigint": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/algo-msgpack-with-bigint/-/algo-msgpack-with-bigint-2.1.1.tgz", + "integrity": "sha512-F1tGh056XczEaEAqu7s+hlZUDWwOBT70Eq0lfMpBP2YguSQVyxRbprLq5rELXKQOyOaixTWYhMeMQMzP0U5FoQ==", + "optional": true + }, + "algosdk": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/algosdk/-/algosdk-1.24.1.tgz", + "integrity": "sha512-9moZxdqeJ6GdE4N6fA/GlUP4LrbLZMYcYkt141J4Ss68OfEgH9qW0wBuZ3ZOKEx/xjc5bg7mLP2Gjg7nwrkmww==", + "optional": true, + "requires": { + "algo-msgpack-with-bigint": "^2.1.1", + "buffer": "^6.0.2", + "cross-fetch": "^3.1.5", + "hi-base32": "^0.5.1", + "js-sha256": "^0.9.0", + "js-sha3": "^0.8.0", + "js-sha512": "^0.8.0", + "json-bigint": "^1.0.0", + "tweetnacl": "^1.0.3", + "vlq": "^2.0.4" + } + }, "arconnect": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/arconnect/-/arconnect-0.4.2.tgz", @@ -1120,6 +2877,15 @@ "bignumber.js": "^9.0.2" } }, + "arweave-stream-tx": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/arweave-stream-tx/-/arweave-stream-tx-1.2.2.tgz", + "integrity": "sha512-bNt9rj0hbAEzoUZEF2s6WJbIz8nasZlZpxIw03Xm8fzb9gRiiZlZGW3lxQLjfc9Z0VRUWDzwtqoYeEoB/JDToQ==", + "optional": true, + "requires": { + "exponential-backoff": "^3.1.0" + } + }, "asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -1131,6 +2897,29 @@ "safer-buffer": "^2.1.0" } }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "base-x": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", + "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1141,6 +2930,11 @@ "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" }, + "bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, "bignumber.js": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", @@ -1151,6 +2945,19 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "requires": { + "base-x": "^3.0.2" + } + }, "buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1160,12 +2967,38 @@ "ieee754": "^1.2.1" } }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "confbox": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "dev": true }, + "cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "optional": true, + "requires": { + "node-fetch": "^2.7.0" + } + }, "debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -1174,6 +3007,64 @@ "ms": "^2.1.3" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, "esbuild": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", @@ -1226,12 +3117,123 @@ } } }, + "exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "optional": true + }, + "follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==" + }, + "form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + } + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "requires": { + "has-symbols": "^1.0.3" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "hi-base32": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/hi-base32/-/hi-base32-0.5.1.tgz", + "integrity": "sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==", + "optional": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "http-message-signatures": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/http-message-signatures/-/http-message-signatures-1.0.4.tgz", "integrity": "sha512-gavCQWnxHFg0BVlKs6CmYK7hNSH1o0x0mHTC68yBAHYOYuTVXPv52mEE7QuT5TenfiagTdOa/zPJzen4lEX7Rg==", "requires": { "structured-headers": "^1.0.1" + }, + "dependencies": { + "structured-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-1.0.1.tgz", + "integrity": "sha512-QYBxdBtA4Tl5rFPuqmbmdrS9kbtren74RTJTcs0VSQNVV5iRhJD4QlYTLD0+81SBwUQctjEQzjTRI3WG4DzICA==" + } } }, "hyper-async": { @@ -1249,6 +3251,42 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==", + "optional": true + }, + "js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, + "js-sha512": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", + "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==", + "optional": true + }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "optional": true, + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "keccak": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", + "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", + "requires": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" + } + }, "local-pkg": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", @@ -1264,11 +3302,34 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, "mlly": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", @@ -1294,11 +3355,49 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "multistream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", + "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", + "optional": true, + "requires": { + "once": "^1.4.0", + "readable-stream": "^3.6.0" + } + }, + "node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" + }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==" + }, "obliterator": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "optional": true, + "requires": { + "wrappy": "1" + } + }, "pathe": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", @@ -1316,26 +3415,117 @@ "pathe": "^2.0.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "ramda": { "version": "0.30.1", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", "integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==" }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, "resolve.exports": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "dev": true }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==" + }, + "secp256k1": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.1.tgz", + "integrity": "sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA==", + "requires": { + "elliptic": "^6.5.7", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.2.0" + }, + "dependencies": { + "elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + } + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, "structured-headers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-1.0.1.tgz", - "integrity": "sha512-QYBxdBtA4Tl5rFPuqmbmdrS9kbtren74RTJTcs0VSQNVV5iRhJD4QlYTLD0+81SBwUQctjEQzjTRI3WG4DzICA==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-2.0.0.tgz", + "integrity": "sha512-qX9utDaDolZWTcAep0EZDSQASTu4mx16Jh5dBSpplfvK3ro1CF8mWqRByAwncRojnlaCCUZA2jPcCqKNnp7uCA==" + }, + "tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "optional": true + }, + "tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "optional": true, + "requires": { + "tmp": "^0.2.0" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "optional": true }, "typescript": { "version": "5.7.3", @@ -1349,34 +3539,45 @@ "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", "dev": true }, - "undici": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz", - "integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==", - "requires": { - "@fastify/busboy": "^2.0.0" - } + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "warp-arbundles": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/warp-arbundles/-/warp-arbundles-1.0.4.tgz", - "integrity": "sha512-KeRac/EJ7VOK+v5+PSMh2SrzpCKOAFnJICLlqZWt6qPkDCzVwcrNE5wFxOlEk5U170ewMDAB3e86UHUblevXpw==", - "requires": { - "arweave": "^1.13.7", - "base64url": "^3.0.1", - "buffer": "^6.0.3", - "warp-isomorphic": "^1.0.7" - } + "vlq": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-2.0.4.tgz", + "integrity": "sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA==", + "optional": true }, - "warp-isomorphic": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/warp-isomorphic/-/warp-isomorphic-1.0.7.tgz", - "integrity": "sha512-fXHbUXwdYqPm9fRPz8mjv5ndPco09aMQuTe4kXfymzOq8V6F3DLsg9cIafxvjms9/mc6eijzkLBJ63yjEENEjA==", + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, "requires": { - "buffer": "^6.0.3", - "undici": "^5.19.1" + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "optional": true + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "requires": {} + }, "zod": { "version": "3.24.1", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", diff --git a/connect/package.json b/connect/package.json index fb93d9974..52a7ab0f7 100644 --- a/connect/package.json +++ b/connect/package.json @@ -1,6 +1,6 @@ { "name": "@permaweb/aoconnect", - "version": "0.0.65", + "version": "0.0.75", "repository": { "type": "git", "url": "https://github.com/permaweb/ao.git", @@ -40,20 +40,24 @@ "build": "npm run build:types && npm run build:src", "build:src": "node esbuild.js", "build:types": "tsc src/index.js --skipLibCheck --declaration --allowJs --emitDeclarationOnly --outDir dist", - "test": "node --test ./src/**/*.test.js", + "test": "node --test ./src/**/*.test.js src/index.test.js", "pretest:integration": "npm run build && npm link", - "test:integration": "cd ./test/e2e && npm test" + "test:integration": "cd ./test/e2e && npm test", + "test:m2": "node --test src/index.common.test.js" }, "dependencies": { + "@dha-team/arbundles": "^1.0.1", "@permaweb/ao-scheduler-utils": "~0.0.25", "@permaweb/protocol-tag-utils": "~0.0.2", + "axios": "^1.7.9", + "base64url": "^3.0.1", "buffer": "^6.0.3", "debug": "^4.4.0", "http-message-signatures": "^1.0.4", "hyper-async": "^1.1.2", "mnemonist": "^0.39.8", "ramda": "^0.30.1", - "warp-arbundles": "^1.0.4", + "structured-headers": "^2.0.0", "zod": "^3.24.1" }, "devDependencies": { diff --git a/connect/src/client/ao-mu.js b/connect/src/client/ao-mu.js index c086b17a9..f928fa9ea 100644 --- a/connect/src/client/ao-mu.js +++ b/connect/src/client/ao-mu.js @@ -1,5 +1,7 @@ import { Rejected, fromPromise, of } from 'hyper-async' +import { toDataItemSigner } from './signer.js' + /** * @typedef Env3 * @property {fetch} fetch @@ -22,50 +24,59 @@ export function deployMessageWith ({ fetch, MU_URL, logger: _logger }) { const logger = _logger.child('deployMessage') return (args) => { - return of(args) - /** - * Sign with the provided signer - */ - .chain( - fromPromise(({ processId, data, tags, anchor, signer }) => + return ( + of(args) /** - * The processId is the target set on the data item - * See https://specs.g8way.io/?tx=xwOgX-MmqN5_-Ny_zNu2A8o-PnTGsoRb_3FrtiMAkuw + * Sign with the provided signer */ - signer({ data, tags, target: processId, anchor })) - - ) - .chain(signedDataItem => - of(signedDataItem) - .chain(fromPromise(async (signedDataItem) => - fetch( - MU_URL, - { - method: 'POST', - headers: { - 'Content-Type': 'application/octet-stream', - Accept: 'application/json' - }, - redirect: 'follow', - body: signedDataItem.raw - } + .chain( + fromPromise(({ processId, data, tags, anchor, signer }) => + toDataItemSigner(signer)({ data, tags, target: processId, anchor }) + ) + ) + .map(logger.tap('Successfully built and signed data item')) + .chain((signedDataItem) => + of(signedDataItem) + .chain( + fromPromise(async (signedDataItem) => + fetch(MU_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream', + Accept: 'application/json' + }, + redirect: 'follow', + body: signedDataItem.raw + }) + ) ) - )).bichain( - err => Rejected(new Error(`Error while communicating with MU: ${JSON.stringify(err)}`)), - fromPromise( - async res => { + .bichain( + (err) => { + if (err.name === 'RedirectRequested') { + return Rejected(err) + } else { + return Rejected( + new Error( + `Error while communicating with MU: ${JSON.stringify( + err + )}` + ) + ) + } + }, + fromPromise(async (res) => { if (res.ok) return res.json() throw new Error(`${res.status}: ${await res.text()}`) - } + }) ) - ) - .bimap( - logger.tap('Error encountered when writing message via MU'), - logger.tap('Successfully wrote message via MU') - ) - .map(res => ({ res, messageId: signedDataItem.id })) - ) - .toPromise() + .bimap( + logger.tap('Error encountered when writing message via MU'), + logger.tap('Successfully wrote message via MU') + ) + .map((res) => ({ res, messageId: signedDataItem.id })) + ) + .toPromise() + ) } } @@ -87,42 +98,52 @@ export function deployProcessWith ({ fetch, MU_URL, logger: _logger }) { const logger = _logger.child('deployProcess') return (args) => { - return of(args) - /** - * Sign with the provided signer - */ - .chain(fromPromise(({ data, tags, signer }) => signer({ data, tags }))) - .chain(signedDataItem => - of(signedDataItem) - .chain(fromPromise(async (signedDataItem) => - fetch( - MU_URL, - { - method: 'POST', - headers: { - 'Content-Type': 'application/octet-stream', - Accept: 'application/json' - }, - redirect: 'follow', - body: signedDataItem.raw - } + return ( + of(args) + /** + * Sign with the provided signer + */ + .chain( + fromPromise(({ data, tags, signer }) => + toDataItemSigner(signer)({ data, tags }) + ) + ) + .map(logger.tap('Successfully built and signed data item')) + .chain((signedDataItem) => + of(signedDataItem) + .chain( + fromPromise(async (signedDataItem) => + fetch(MU_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream', + Accept: 'application/json' + }, + redirect: 'follow', + body: signedDataItem.raw + }) + ) ) - )).bichain( - err => Rejected(new Error(`Error while communicating with MU: ${JSON.stringify(err)}`)), - fromPromise( - async res => { + .bichain( + (err) => + Rejected( + new Error( + `Error while communicating with MU: ${JSON.stringify(err)}` + ) + ), + fromPromise(async (res) => { if (res.ok) return res.json() throw new Error(`${res.status}: ${await res.text()}`) - } + }) ) - ) - .bimap( - logger.tap('Error encountered when deploying process via MU'), - logger.tap('Successfully deployed process via MU') - ) - .map(res => ({ res, processId: signedDataItem.id })) - ) - .toPromise() + .bimap( + logger.tap('Error encountered when deploying process via MU'), + logger.tap('Successfully deployed process via MU') + ) + .map((res) => ({ res, processId: signedDataItem.id })) + ) + .toPromise() + ) } } @@ -140,51 +161,54 @@ export function deployProcessWith ({ fetch, MU_URL, logger: _logger }) { export function deployMonitorWith ({ fetch, MU_URL, logger: _logger }) { const logger = _logger.child('deployMonitor') - return (args) => of(args) - /** - * Sign with the provided signer - */ - .chain( - fromPromise(({ processId, data, tags, anchor, signer }) => - /** - * The processId is the target set on the data item - */ - signer({ data, tags, target: processId, anchor })) - ) - .chain((signedDataItem) => - of(signedDataItem) - .chain(fromPromise(async (signedDataItem) => - fetch( - MU_URL + '/monitor/' + args.processId, - { - method: 'POST', - headers: { - 'Content-Type': 'application/octet-stream', - Accept: 'application/json' - }, - redirect: 'follow', - body: signedDataItem.raw - } + return (args) => + of(args) + /** + * Sign with the provided signer + */ + .chain( + fromPromise(({ processId, data, tags, anchor, signer }) => + toDataItemSigner(signer)({ data, tags, target: processId, anchor }) + ) + ) + .map(logger.tap('Successfully built and signed data item')) + .chain((signedDataItem) => + of(signedDataItem) + .chain( + fromPromise(async (signedDataItem) => + fetch(MU_URL + '/monitor/' + args.processId, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream', + Accept: 'application/json' + }, + redirect: 'follow', + body: signedDataItem.raw + }) + ) ) - )).bichain( - err => Rejected(new Error(`Error while communicating with MU: ${JSON.stringify(err)}`)), - fromPromise( - async res => { + .bichain( + (err) => + Rejected( + new Error( + `Error while communicating with MU: ${JSON.stringify(err)}` + ) + ), + fromPromise(async (res) => { /** * Response from MU is not used, so just return some json */ if (res.ok) return { ok: true } throw new Error(`${res.status}: ${await res.text()}`) - } + }) ) - ) - .bimap( - logger.tap('Error encountered when subscribing to process via MU'), - logger.tap('Successfully subscribed to process via MU') - ) - .map(res => ({ res, messageId: signedDataItem.id })) - ) - .toPromise() + .bimap( + logger.tap('Error encountered when subscribing to process via MU'), + logger.tap('Successfully subscribed to process via MU') + ) + .map((res) => ({ res, messageId: signedDataItem.id })) + ) + .toPromise() } /** @@ -201,51 +225,56 @@ export function deployMonitorWith ({ fetch, MU_URL, logger: _logger }) { export function deployUnmonitorWith ({ fetch, MU_URL, logger: _logger }) { const logger = _logger.child('deployUnmonitor') - return (args) => of(args) - /** - * Sign with the provided signer - */ - .chain( - fromPromise(({ processId, data, tags, anchor, signer }) => - /** - * The processId is the target set on the data item - */ - signer({ data, tags, target: processId, anchor })) - ) - .chain((signedDataItem) => - of(signedDataItem) - .chain(fromPromise(async (signedDataItem) => - fetch( - MU_URL + '/monitor/' + args.processId, - { - method: 'DELETE', - headers: { - 'Content-Type': 'application/octet-stream', - Accept: 'application/json' - }, - redirect: 'follow', - body: signedDataItem.raw - } + return (args) => + of(args) + /** + * Sign with the provided signer + */ + .chain( + fromPromise(({ processId, data, tags, anchor, signer }) => + toDataItemSigner(signer)({ data, tags, target: processId, anchor }) + ) + ) + .map(logger.tap('Successfully built and signed data item')) + .chain((signedDataItem) => + of(signedDataItem) + .chain( + fromPromise(async (signedDataItem) => + fetch(MU_URL + '/monitor/' + args.processId, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/octet-stream', + Accept: 'application/json' + }, + redirect: 'follow', + body: signedDataItem.raw + }) + ) ) - )).bichain( - err => Rejected(new Error(`Error while communicating with MU: ${JSON.stringify(err)}`)), - fromPromise( - async res => { + .bichain( + (err) => + Rejected( + new Error( + `Error while communicating with MU: ${JSON.stringify(err)}` + ) + ), + fromPromise(async (res) => { /** * Response from MU is not used, so just return some json */ if (res.ok) return { ok: true } throw new Error(`${res.status}: ${await res.text()}`) - } + }) ) - ) - .bimap( - logger.tap('Error encountered when unsubscribing to process via MU'), - logger.tap('Successfully unsubscribed to process via MU') - ) - .map(res => ({ res, messageId: signedDataItem.id })) - ) - .toPromise() + .bimap( + logger.tap( + 'Error encountered when unsubscribing to process via MU' + ), + logger.tap('Successfully unsubscribed to process via MU') + ) + .map((res) => ({ res, messageId: signedDataItem.id })) + ) + .toPromise() } /** @@ -272,9 +301,12 @@ export function deployAssignWith ({ fetch, MU_URL, logger: _logger }) { return (args) => { return of(args) - .chain(fromPromise(async ({ process, message, baseLayer, exclude }) => - fetch( - `${MU_URL}?process-id=${process}&assign=${message}${baseLayer ? '&base-layer' : ''}${exclude ? '&exclude=' + exclude.join(',') : ''}`, + .chain( + fromPromise(async ({ process, message, baseLayer, exclude }) => + fetch( + `${MU_URL}?process-id=${process}&assign=${message}${ + baseLayer ? '&base-layer' : '' + }${exclude ? '&exclude=' + exclude.join(',') : ''}`, { method: 'POST', headers: { @@ -282,21 +314,26 @@ export function deployAssignWith ({ fetch, MU_URL, logger: _logger }) { Accept: 'application/json' } } + ) ) - )).bichain( - err => Rejected(new Error(`Error while communicating with MU: ${JSON.stringify(err)}`)), - fromPromise( - async res => { - if (res.ok) return res.json() - throw new Error(`${res.status}: ${await res.text()}`) - } - ) + ) + .bichain( + (err) => + Rejected( + new Error( + `Error while communicating with MU: ${JSON.stringify(err)}` + ) + ), + fromPromise(async (res) => { + if (res.ok) return res.json() + throw new Error(`${res.status}: ${await res.text()}`) + }) ) .bimap( logger.tap('Error encountered when writing assignment via MU'), logger.tap('Successfully wrote assignment via MU') ) - .map(res => ({ res, assignmentId: res.id })) + .map((res) => ({ res, assignmentId: res.id })) .toPromise() } } diff --git a/connect/src/client/ao-mu.test.js b/connect/src/client/ao-mu.test.js index 0c022fba4..82d09468b 100644 --- a/connect/src/client/ao-mu.test.js +++ b/connect/src/client/ao-mu.test.js @@ -2,7 +2,7 @@ import { describe, test } from 'node:test' import * as assert from 'node:assert' import { createLogger } from '../logger.js' -import { deployMessageSchema, deployMonitorSchema, deployProcessSchema, signerSchema, deployAssignSchema } from '../dal.js' +import { deployMessageSchema, deployMonitorSchema, deployProcessSchema, deployAssignSchema, signerSchema } from '../dal.js' import { deployMessageWith, deployProcessWith, deployMonitorWith, deployUnmonitorWith, deployAssignWith } from './ao-mu.js' const MU_URL = globalThis.MU_URL || 'https://ao-mu-1.onrender.com' @@ -40,7 +40,8 @@ describe('ao-mu', () => { { name: 'Content-Type', value: 'text/plain' } ], signer: signerSchema.implement( - async ({ data, tags, target }) => { + async (create) => { + const { data, tags, target } = await create({ passthrough: true }) assert.ok(data) assert.deepStrictEqual(tags, [ { name: 'foo', value: 'bar' }, @@ -90,7 +91,8 @@ describe('ao-mu', () => { { name: 'Content-Type', value: 'text/plain' } ], signer: signerSchema.implement( - async ({ data, tags, target }) => { + async (create) => { + const { data, tags, target } = await create({ passthrough: true }) assert.ok(data) assert.deepStrictEqual(tags, [ { name: 'foo', value: 'bar' }, @@ -141,7 +143,8 @@ describe('ao-mu', () => { ], anchor: 'idempotent-123', signer: signerSchema.implement( - async ({ data, tags, target, anchor }) => { + async (create) => { + const { data, tags, target, anchor } = await create({ passthrough: true }) assert.ok(data) assert.deepStrictEqual(tags, [ { name: 'foo', value: 'bar' }, @@ -189,7 +192,8 @@ describe('ao-mu', () => { data: '1234', tags: [{ name: 'foo', value: 'bar' }], signer: signerSchema.implement( - async ({ data, tags }) => { + async (create) => { + const { data, tags } = await create({ passthrough: true }) assert.ok(data) assert.deepStrictEqual(tags, [ { name: 'foo', value: 'bar' } diff --git a/connect/src/client/browser/wallet.js b/connect/src/client/browser/wallet.js index 4bd29b947..648b2fcbc 100644 --- a/connect/src/client/browser/wallet.js +++ b/connect/src/client/browser/wallet.js @@ -1,87 +1,106 @@ import { Buffer } from 'buffer/index.js' -import * as WarpArBundles from 'warp-arbundles' -import { httpbis } from 'http-message-signatures' // eslint-disable-next-line no-unused-vars import { Types } from '../../dal.js' -import { httpSigName } from '../hb.js' +import { DATAITEM_SIGNER_KIND, HTTP_SIGNER_KIND } from '../signer.js' +import { getRawAndId } from '../../lib/data-item.js' if (!globalThis.Buffer) globalThis.Buffer = Buffer -const { DataItem } = WarpArBundles -const { signMessage } = httpbis - -/** - * A function that builds a signer using the global arweaveWallet - * commonly used in browser-based dApps - * - * This is provided as a convenience for consumers of the SDK - * to use, but consumers can also implement their own signer - * - * @param {Object} arweaveWallet - The window.arweaveWallet object - * @returns {Types['signer']} - The signer function - * @example - * const signer = createDataItemSigner(window.arweaveWallet) - */ -export function createDataItemSigner (arweaveWallet) { +function createANS104Signer (arweaveWallet) { /** * createDataItem can be passed here for the purposes of unit testing * with a stub */ - const signer = async ({ data, tags, target, anchor, createDataItem = (buf) => new DataItem(buf) }) => { + const signer = async (create) => arweaveWallet.connect([ + 'SIGN_TRANSACTION' + ]).then(async () => { + /** + * set passthrough in order to receive the arguements as they were passed + * to toDataItemSigner + */ + const { data, tags, target, anchor } = await create({ + alg: 'rsa-v1_5-sha256', + passthrough: true + }) /** - * signDataItem interface according to ArweaveWalletConnector - * - * https://github.com/jfbeats/ArweaveWalletConnector/blob/7c167f79cd0cf72b6e32e1fe5f988a05eed8f794/src/Arweave.ts#L46C23-L46C23 + * https://github.com/wanderwallet/Wander?tab=readme-ov-file#signdataitemdataitem-promiserawdataitem */ const view = await arweaveWallet.signDataItem({ data, tags, target, anchor }) - const dataItem = createDataItem(Buffer.from(view)) - return { - id: await dataItem.id, - raw: await dataItem.getRaw() - } - } + + /** + * Since we passthrough above, just send the precomputed + * shape back, which is then detected in signer wrapper + */ + const res = await getRawAndId(Buffer.from(view)) + return res + }) return signer } -export function createHbSigner (arweaveWallet) { - const params = ['alg', 'keyid'].sort() +function createHttpSigner (arweaveWallet) { + const signer = async (create) => arweaveWallet.connect([ + 'ACCESS_ADDRESS', + 'ACCESS_PUBLIC_KEY', + 'SIGNATURE' + ]).then(async () => { + const [publicKey, address] = await Promise.all([ + arweaveWallet.getActivePublicKey(), + arweaveWallet.getActiveAddress() + ]) + return { publicKey, address } + }).then(async ({ publicKey, address }) => { + const signatureBase = await create({ + type: 1, + publicKey, + address, + alg: 'rsa-pss-sha512' + }) + + const view = await arweaveWallet.signMessage( + signatureBase, + { hashAlgorithm: 'SHA-512' } + ) - let permsP - const signer = async ({ fields, request }) => new Promise((resolve) => { - if (!permsP) { - permsP = arweaveWallet.connect([ - 'ACCESS_ADDRESS', - 'ACCESS_PUBLIC_KEY', - 'SIGNATURE' - ]).then(async () => { - const publicKey = await arweaveWallet.getActivePublicKey() - const address = await arweaveWallet.getActiveAddress() - return { publicKey, address } - }) + return { + signature: Buffer.from(view), + address } + }) - return resolve(permsP) - }).then(({ publicKey, address }) => { - return signMessage({ - key: { - id: publicKey, - alg: 'rsa-pss-sha512', - sign: async (data) => { - const view = await arweaveWallet.signMessage( - data, - { hashAlgorithm: 'SHA-512' } - ) + return signer +} - return Buffer.from(view) - } - }, - fields, - name: httpSigName(address), - params - }, request) - }) +/** + * A function that builds a signer using the global arweaveWallet + * commonly used in browser-based dApps + * + * This is provided as a convenience for consumers of the SDK + * to use, but consumers can also implement their own signer + * + * @param {Object} arweaveWallet - The window.arweaveWallet object + * @returns {Types['signer']} - The signer function + * @example + * const signer = createSigner(window.arweaveWallet) + */ +export function createSigner (wallet) { + const dataItemSigner = createANS104Signer(wallet) + const httpSigner = createHttpSigner(wallet) + + const signer = (create, kind) => { + if (kind === DATAITEM_SIGNER_KIND) return dataItemSigner(create) + if (kind === HTTP_SIGNER_KIND) return httpSigner(create) + throw new Error(`signer kind unknown "${kind}"`) + } return signer } + +/** + * @deprecated - use createSigner() instead + * + * Create a signer that uses the provided arweaveWallet + * commonly used as global in browser-based dApps + */ +export const createDataItemSigner = createSigner diff --git a/connect/src/client/browser/wallet.test.js b/connect/src/client/browser/wallet.test.js index 9f3781f2d..4502f406d 100644 --- a/connect/src/client/browser/wallet.test.js +++ b/connect/src/client/browser/wallet.test.js @@ -2,41 +2,49 @@ import { describe, test } from 'node:test' import * as assert from 'node:assert' import { randomBytes } from 'node:crypto' -import { createDataItemSigner } from './wallet.js' +import Arweave from 'arweave' + +import { createDataItemBytes } from '../../lib/data-item.js' +import { DATAITEM_SIGNER_KIND } from '../signer.js' +import { createSigner } from './wallet.js' describe('browser - wallet', () => { - describe('createDataItemSigner', () => { + describe('createSigner', () => { test('should create and sign the data item', async () => { + const arweave = Arweave.init() + const wallet = await arweave.wallets.generate() + const publicKey = Buffer.from(wallet.n, 'base64url') /** * A mock of an actual arweaveWallet pulled from * globalThis or window * * We only stub signDataItem because it's the only - * api needed for the trimmed down InjectedArweaveSigner + * api needed */ const stubArweaveWallet = { async signDataItem ({ data, tags, target, anchor }) { - return Buffer.from(JSON.stringify({ data, tags, target, anchor })) + /** + * pretend actual signing is done here + */ + const bytes = createDataItemBytes(data, { type: 1, publicKey }, { tags, target, anchor }) + return bytes } } - const sign = createDataItemSigner(stubArweaveWallet) + const sign = createSigner(stubArweaveWallet) - const res = await sign({ - data: 'foobar', - tags: [{ name: 'foo', value: 'bar' }], - target: 'xwOgX-MmqN5_-Ny_zNu2A8o-PnTGsoRb_3FrtiMAkuw', - anchor: randomBytes(32), - /** - * stub createDataItem - */ - createDataItem (buf) { + const res = await sign( + async (args) => { + assert.ok(args.passthrough) return { - id: Promise.resolve('CPUgRQCmNxVnIVyfC69_ypDaMQTYNOU8jsMad4QFAS8'), - getRaw: async () => buf + data: 'foobar', + tags: [{ name: 'foo', value: 'bar' }], + target: 'xwOgX-MmqN5_-Ny_zNu2A8o-PnTGsoRb_3FrtiMAkuw', + anchor: randomBytes(32) } - } - }) + }, + DATAITEM_SIGNER_KIND + ) console.log('signedDataItem', res) assert.ok(res.id) diff --git a/connect/src/client/hb-encode.js b/connect/src/client/hb-encode.js new file mode 100644 index 000000000..b89c41fab --- /dev/null +++ b/connect/src/client/hb-encode.js @@ -0,0 +1,185 @@ +import base64url from 'base64url' +import { Buffer } from 'buffer/index.js' + +/** + * polyfill in Browser + */ +if (!globalThis.Buffer) globalThis.Buffer = Buffer + +/** + * ****** + * HyperBEAM Http Encoding + * + * TODO: bundle into a package with + * + * - export encode() + * - export encodeDataItem() to convert object + * or ans104 to http message + * - exported signers for both node and browser environments + * (currently located in wallet.js modules) + * ****** + */ + +/** + * @param {ArrayBuffer} data + */ +async function sha256 (data) { + return crypto.subtle.digest('SHA-256', data) +} + +function partition (pred, arr) { + return arr.reduce((acc, cur) => { + acc[pred(cur) ? 0 : 1].push(cur) + return acc + }, + [[], []]) +} + +function isBytes (value) { + return value instanceof ArrayBuffer || + ArrayBuffer.isView(value) +} + +function hbEncodeValue (key, value) { + const typeK = `converge-type-${key}` + + if (typeof value === 'string') { + if (value.length === 0) return { [typeK]: 'empty-binary' } + return { [key]: value } + } + + if (Array.isArray(value) && value.length === 0) { + return { [typeK]: 'empty-list' } + } + + if (typeof value === 'number') { + if (!Number.isInteger(value)) return { [typeK]: 'float', [key]: `${value}` } + return { [typeK]: 'integer', [key]: `${value}` } + } + + if (typeof value === 'symbol') { + return { [typeK]: 'atom', [key]: value.description } + } + + throw new Error(`Cannot encode value: ${value.toString()}`) +} + +function hbEncode (obj, parent = '') { + return Object.entries(obj).reduce((acc, [key, value]) => { + const flatK = (parent ? `${parent}/${key}` : key) + .toLowerCase() + + // skip nullish values + if (value == null) return acc + + // binary data + if (isBytes(value)) { + if (value.byteLength === 0) { + return Object.assign(acc, hbEncodeValue(flatK, '')) + } + return Object.assign(acc, { [flatK]: value }) + } + + // first/{idx}/name flatten array + if (Array.isArray(value)) { + if (value.length === 0) { + return Object.assign(acc, hbEncodeValue(flatK, value)) + } + value.forEach((v, i) => + Object.assign(acc, hbEncode(v, `${flatK}/${i}`)) + ) + return acc + } + + // first/second flatten object + if (typeof value === 'object' && value !== null) { + return Object.assign(acc, hbEncode(value, flatK)) + } + + // leaf encode value + Object.assign(acc, hbEncodeValue(flatK, value)) + return acc + }, {}) +} + +async function boundaryFrom (bodyParts = []) { + const base = new Blob( + bodyParts.flatMap((p, i, arr) => + i < arr.length - 1 ? [p, '\r\n'] : [p]) + ) + + const hash = await sha256(await base.arrayBuffer()) + return base64url.encode(Buffer.from(hash)) +} + +/** + * Encode the object as HyperBEAM HTTP multipart + * message. Nested objects are flattened to a single + * depth multipart + */ +export async function encode (obj = {}) { + if (Object.keys(obj) === 0) return + + const flattened = hbEncode(obj) + /** + * Some values may be encoded into headers, + * while others may be encoded into the body + */ + const [bodyKeys, headerKeys] = partition( + (key) => { + if (key.includes('/')) return true + const bytes = Buffer.from(flattened[key]) + /** + * Anything larger than 4k goes into + * the body + */ + return bytes.byteLength > 4096 + }, + Object.keys(flattened).sort() + ) + + const h = new Headers() + headerKeys.forEach((key) => h.append(key, flattened[key])) + /** + * Add headers that indicates and orders body-keys + * for the purpose of determinstically reconstructing + * content-digest on the server + */ + // const bk = hbEncodeValue('body-keys', bodyKeys) + // Object.keys(bk).forEach((key) => h.append(key, bk[key])) + + let body + if (bodyKeys.length) { + const bodyParts = await Promise.all( + bodyKeys.map((name) => new Blob([ + `content-disposition: form-data;name="${name}"\r\n\r\n`, + flattened[name] + ]).arrayBuffer()) + ) + + const boundary = await boundaryFrom(bodyParts) + + /** + * Segment each part with the multipart boundary + */ + const blobParts = bodyParts + .flatMap((p) => [`--${boundary}\r\n`, p, '\r\n']) + + /** + * Add the terminating boundary + */ + blobParts.push(`--${boundary}--`) + + body = new Blob(blobParts) + /** + * calculate the content-digest + */ + const contentDigest = await sha256(await body.arrayBuffer()) + const base64 = base64url.toBase64(base64url.encode(contentDigest)) + + h.set('Content-Type', `multipart/form-data; boundary="${boundary}"`) + h.append('Content-Digest', `sha-256=:${base64}:`) + } + + return { headers: h, body } +} diff --git a/connect/src/client/hb.js b/connect/src/client/hb.js index 5f418716c..a93a7ed43 100644 --- a/connect/src/client/hb.js +++ b/connect/src/client/hb.js @@ -1,17 +1,325 @@ -function base64urlDecodeUTF8 (base64url) { - const binary = atob(base64url.replace(/-/g, '+').replace(/_/g, '/')) - const bytes = new Uint8Array([...binary].map(char => char.charCodeAt(0))) - return new TextDecoder().decode(bytes) +import { Rejected, fromPromise, of } from 'hyper-async' +import base64url from 'base64url' + +import { joinUrl } from '../lib/utils.js' +import { encode } from './hb-encode.js' +import { toHttpSigner } from './signer.js' + +/** + * Map data item members to corresponding HB HTTP message + * shape + */ +export async function encodeDataItem ({ processId, data, tags, anchor }) { + const obj = {} + + if (processId) obj.target = processId + if (anchor) obj.anchor = anchor + if (tags) tags.forEach(t => { obj[t.name] = t.value }) + /** + * Always ensure the variant is mainnet for hyperbeam + * TODO: change default variant to be this eventually + */ + obj.variant = 'ao.N.1' + obj.data = data + + const res = await encode(obj) + if (!res) return { headers: new Headers(), body: undefined } + return res +} + +function toSigBaseArgs ({ url, method, headers, includePath = false }) { + headers = new Headers(headers) + return { + /** + * Always sign all headers, and the path, + * and that there is a deterministic signature + * component ordering + * + * TODO: removing path from signing, for now. + */ + fields: [ + ...headers.keys(), + ...(includePath ? ['@path'] : []) + ].sort(), + request: { url, method, headers: { ...Object.fromEntries(headers) } } + } } export function httpSigName (address) { - const decoded = Buffer.from(base64urlDecodeUTF8(address)) + const decoded = base64url.toBuffer(address) const hexString = [...decoded.subarray(1, 9)] .map(byte => byte.toString(16).padStart(2, '0')) .join('') return `http-sig-${hexString}` } +export function requestWith ({ fetch, logger: _logger, HB_URL, signer }) { + const logger = _logger.child('request') + + return (fields) => { + const { path, method, ...restFields } = fields + + return of({ path, method, fields: restFields }) + .chain(fromPromise(({ path, method, fields }) => + encode(fields).then(({ headers, body }) => ({ + path, + method, + headers, + body + })) + )) + .chain(fromPromise(async ({ path, method, headers, body }) => + toHttpSigner(signer)(toSigBaseArgs({ + url: joinUrl({ url: HB_URL, path }), + method, + headers + // this does not work with hyperbeam + // includePath: true + })).then((req) => ({ ...req, body })) + )) + .map(logger.tap('Sending HTTP signed message to HB: %o')) + .chain((request) => of(request) + .chain(fromPromise(({ url, method, headers, body }) => { + return fetch(url, { method, headers, body, redirect: 'follow' }) + .then(async res => { + if (res.status < 300) { + const contentType = res.headers.get('content-type') + + if (contentType && contentType.includes('multipart/form-data')) { + return res + } else if (contentType && contentType.includes('application/json')) { + const body = await res.json() + return { + headers: res.headers, + body + } + } else { + const body = await res.text() + return { + headers: res.headers, + body + } + } + } + return res + }) + } + )) + ).toPromise() + } +} + +export function deployProcessWith ({ fetch, logger: _logger, HB_URL, signer }) { + const logger = _logger.child('deployProcess') + + return (args) => { + return of(args) + /** + * disregard data item signer passed, as it is not needed + * when the HTTP msg is what is actually signed + */ + .chain(fromPromise(({ processId, data, tags }) => + encodeDataItem({ processId, data, tags }) + )) + .chain(fromPromise(({ headers, body }) => { + return toHttpSigner(signer)(toSigBaseArgs({ + url: `${HB_URL}/schedule`, + method: 'POST', + headers + })).then((req) => ({ ...req, body })) + })) + .map(logger.tap('Sending HTTP signed message to HB MU: %o')) + .chain((request) => of(request) + .chain(fromPromise(({ url, method, headers, body }) => + fetch(url, { method, headers, body, redirect: 'follow' }) + )) + .bichain( + (err) => Rejected(err), + fromPromise(async (res) => { + if (res.ok) return res.headers.get('process') + throw new Error(`${res.status}: ${await res.text()}`) + }) + ) + .bimap( + logger.tap('Error encountered when deploying process via HB MU'), + logger.tap('Successfully deployed process via HB MU') + ) + .map((process) => ({ processId: process })) + ) + .toPromise() + } +} + +export function deployMessageWith ({ fetch, logger: _logger, HB_URL, signer }) { + const logger = _logger.child('deployMessage') + + return (args) => { + return of(args) + /** + * disregard data item signer passed, as it is not needed + * when the HTTP msg is what is actually signed + */ + .chain(fromPromise(({ processId, data, tags, anchor }) => + encodeDataItem({ processId, data, tags, anchor }) + )) + .chain(fromPromise(({ headers, body }) => { + return toHttpSigner(signer)(toSigBaseArgs({ + url: `${HB_URL}/${args.processId}/schedule`, + method: 'POST', + headers + })).then((req) => ({ ...req, body })) + })) + .map(logger.tap('Sending HTTP signed message to HB MU: %o')) + .chain((request) => of(request) + .chain(fromPromise(({ url, method, headers, body }) => + fetch(url, { method, headers, body, redirect: 'follow' }) + )) + .bichain( + (err) => Rejected(err), + fromPromise(async (res) => { + if (res.ok) { + return { + slot: res.headers.get('slot'), + processId: args.processId + } + } + throw new Error(`${res.status}: ${await res.text()}`) + }) + ) + .map(logger.tap('Received slot from HB MU: %o')) + .bichain( + (err) => Rejected(err), + fromPromise(async ({ slot, processId }) => { + const { headers, body } = await encodeDataItem({ processId }) + return toHttpSigner(signer)(toSigBaseArgs({ + url: `${HB_URL}/${processId}/push&slot+integer=${slot}`, + method: 'POST', + headers + })).then((req) => ({ ...req, body })) + .then(req => fetch(req.url, { method: req.method, headers: req.headers, body: req.body, redirect: 'follow' })) + .then(() => slot) + }) + ) + .bimap( + logger.tap('Error encountered when writing message via HB MU'), + logger.tap('Successfully wrote message via HB MU') + ) + .map((slot) => ({ messageId: slot })) + ) + .toPromise() + } +} + +export function loadResultWith ({ fetch, logger: _logger, HB_URL, signer }) { + const logger = _logger.child('loadResult') + + return (args) => { + return of(args) + .chain(fromPromise(async ({ id, processId }) => { + const { headers, body } = await encodeDataItem({ processId }) + headers.append('slot+integer', id) + headers.append('accept', 'application/json') + return toHttpSigner(signer)(toSigBaseArgs({ + url: `${HB_URL}/${processId}/compute&slot+integer=${id}/results/json`, + method: 'POST', + headers + })).then((req) => ({ ...req, body })) + })) + .map(logger.tap('fetching message result from HB CU: %o')) + .chain((request) => of(request) + .chain(fromPromise(({ url, method, headers, body }) => + fetch(url, { method, headers, body, redirect: 'follow' }) + )) + .bichain( + (err) => Rejected(err), + fromPromise(async (res) => { + if (res.ok) return res.json() + throw new Error(`${res.status}: ${await res.text()}`) + }) + ) + .bimap( + logger.tap('Error encountered when loading result via HB CU'), + logger.tap('Successfully loading result via HB CU') + ) + ) + .toPromise() + } +} + +/** + * @typedef Env3 + * @property {fetch} fetch + * @property {string} CU_URL + * + * @typedef QueryResultsArgs + * @property {string} process - the id of the process being read + * @property {string} from - cursor to start the list of results + * @property {string} to - cursor to stop the list of results + * @property {string} sort - "ASC" or "DESC" to describe the order of list + * @property {number} limit - the number of results to return + * + * @callback QueryResults + * @param {QueryResultsArgs} args + * @returns {Promise} + * + * @param {Env3} env + * @returns {QueryResults} + */ +export function queryResultsWith ({ fetch, HB_URL, logger, signer }) { + return ({ process, from, to, sort, limit }) => { + // const target = new URL(`${CU_URL}/results/${process}`) + // const params = new URLSearchParams(target.search) + // if (from) { params.append('from', from) } + // if (to) { params.append('to', to) } + // if (sort) { params.append('sort', sort) } + // if (limit) { params.append('limit', limit) } + // target.search = params + + // m2 does not have a results endpoint, but you can + // access each message by slots, the goal here + // would be to create a range of query requests + // based on the above parameters and then resolve + // them using a promise.all + return of().chain(fromPromise(async () => { + // TODO: need to figure out how best to pass this from client + const processId = 'Vj1efidjweGtv7YVDmD1rUEd7cUK7MCr6OQ6Jv04Qd0' + const { headers, body } = await encodeDataItem({ processId }) + // headers.append('slot+integer', id) + headers.append('accept', 'application/json') + return toHttpSigner(signer)(toSigBaseArgs({ + url: `${HB_URL}/${processId}/state/now`, + method: 'GET', + headers + })).then((req) => ({ ...req, body })) + })) + .chain((request) => of(request) + .chain(fromPromise(({ url, method, headers, body }) => + fetch(url, { method, headers, body, redirect: 'follow' }) + )) + .bichain( + (err) => Rejected(err), + fromPromise(async (res) => { + if (res.ok) return res.json() + throw new Error(`${res.status}: ${await res.text()}`) + }) + ) + .bimap( + logger.tap('Error encountered when loading result via HB CU'), + logger.tap('Successfully loading result via HB CU') + ) + ) + .toPromise() + } +} + +export class InsufficientFunds extends Error { + name = 'InsufficientFunds' +} + +export class RedirectRequested extends Error { + name = 'RedirectRequested' +} + export function relayerWith ({ fetch, logger, HB_URL, signer }) { /** * A fetch wrapper that will sign @@ -39,7 +347,7 @@ export function relayerWith ({ fetch, logger, HB_URL, signer }) { * TODO: should we mimick a fetch error on failure to sign * ie. TypeError? For now, just letting this bubble */ - const { headers: signedHeaders } = await signer({ + const { headers: signedHeaders } = await toHttpSigner(signer)({ fields: [ ...options.headers.keys(), 'relay-path' @@ -51,6 +359,20 @@ export function relayerWith ({ fetch, logger, HB_URL, signer }) { } }) - return fetch(hb, { ...options, headers: signedHeaders }) + return fetch(hb, { ...options, headers: signedHeaders }).then(res => { + if (res.status === 400) { + const err = new InsufficientFunds('Insufficient Funds for request!') + err.price = res.headers.get('price') + throw err + } + if (res.status === 422) { + const err = new RedirectRequested('Redirect with new format!') + err.contentEncoding = res.headers.get('content-encoding') + err.device = res.headers.get('accept-device') + err.location = res.headers.get('location') + throw err + } + return res + }) } } diff --git a/connect/src/client/node/wallet.js b/connect/src/client/node/wallet.js index 249b67e89..bdfb800e1 100644 --- a/connect/src/client/node/wallet.js +++ b/connect/src/client/node/wallet.js @@ -1,18 +1,44 @@ -import { createPrivateKey, createHash } from 'node:crypto' - -import * as WarpArBundles from 'warp-arbundles' -import { httpbis, createSigner } from 'http-message-signatures' +import { constants, createPrivateKey, createHash, createSign } from 'node:crypto' // eslint-disable-next-line no-unused-vars import { Types } from '../../dal.js' -import { httpSigName } from '../hb.js' +import { DATAITEM_SIGNER_KIND, HTTP_SIGNER_KIND } from '../signer.js' -/** - * hack to get module resolution working on node jfc - */ -const pkg = WarpArBundles.default ? WarpArBundles.default : WarpArBundles -const { createData, ArweaveSigner } = pkg -const { signMessage } = httpbis +function createANS104Signer ({ privateKey, publicKey, address }) { + const signer = async (create) => { + const deepHash = await create({ + type: 1, + publicKey, + alg: 'rsa-v1_5-sha256' + }) + + const signature = createSign('sha256') + .update(deepHash) + .sign({ key: privateKey, padding: constants.RSA_PKCS1_PSS_PADDING }) + + return { signature, address } + } + + return signer +} + +function createHttpSigner ({ publicKey, privateKey, address }) { + const signer = async (create) => { + const signatureBase = await create({ + type: 1, + publicKey, + alg: 'rsa-pss-sha512' + }) + + const signature = createSign('sha512') + .update(signatureBase) + .sign({ key: privateKey, padding: constants.RSA_PKCS1_PSS_PADDING }) + + return { signature, address } + } + + return signer +} /** * A function that builds a signer using a wallet jwk interface @@ -23,41 +49,31 @@ const { signMessage } = httpbis * * @returns {Types['signer']} */ -export function createDataItemSigner (wallet) { - const signer = async ({ data, tags, target, anchor }) => { - const signer = new ArweaveSigner(wallet) - const dataItem = createData(data, signer, { tags, target, anchor }) - return dataItem.sign(signer) - .then(async () => ({ - id: await dataItem.id, - raw: await dataItem.getRaw() - })) - } - - return signer -} - -export function createHbSigner (wallet) { +export function createSigner (wallet) { /** * n is the public modulus * (the base64url encoded public key) */ - const publicKey = wallet.n + const publicKey = Buffer.from(wallet.n, 'base64url') const privateKey = createPrivateKey({ key: wallet, format: 'jwk' }) - const address = createHash('sha256') - .update(Buffer.from(publicKey, 'base64url')) - .digest('base64url') + const address = createHash('sha256').update(publicKey).digest('base64url') - const s = createSigner(privateKey, 'rsa-pss-sha512', publicKey) - const params = ['alg', 'keyid'].sort() + const dataItemSigner = createANS104Signer({ wallet, privateKey, publicKey, address }) + const httpSigner = createHttpSigner({ wallet, publicKey, privateKey, address }) - const signer = async ({ fields, request }) => - signMessage({ - key: s, - fields, - name: httpSigName(address), - params - }, request) + const signer = (create, kind) => { + if (kind === DATAITEM_SIGNER_KIND) return dataItemSigner(create) + if (kind === HTTP_SIGNER_KIND) return httpSigner(create) + throw new Error(`signer kind unknown "${kind}"`) + } return signer } + +/** + * @deprecated - use createSigner() instead + * + * Create a signer that uses the provided arweave wallet jwk + * for signing + */ +export const createDataItemSigner = createSigner diff --git a/connect/src/client/node/wallet.test.js b/connect/src/client/node/wallet.test.js index a16995d5b..08db5b740 100644 --- a/connect/src/client/node/wallet.test.js +++ b/connect/src/client/node/wallet.test.js @@ -2,12 +2,17 @@ import { describe, test, before } from 'node:test' import * as assert from 'node:assert' import { tmpdir } from 'node:os' import { writeFileSync, readFileSync } from 'node:fs' -import { randomBytes } from 'node:crypto' +import { createPublicKey, randomBytes } from 'node:crypto' import { join } from 'node:path' import Arweave from 'arweave' +import { createVerifier, httpbis } from 'http-message-signatures' -import { createDataItemSigner } from './wallet.js' +import { createDataItemBytes, verify } from '../../lib/data-item.js' +import { createSigner } from './wallet.js' +import { DATAITEM_SIGNER_KIND, toDataItemSigner, toHttpSigner } from '../signer.js' + +const { verifyMessage } = httpbis describe('node - wallet', () => { /** @@ -24,22 +29,82 @@ describe('node - wallet', () => { ) }) - describe('createDataItemSigner', () => { + describe('createSigner', () => { test('should create and sign the data item with Arweave signer', async () => { const wallet = JSON.parse(readFileSync(tmpWallet).toString()) + const pubKey = Buffer.from(wallet.n, 'base64url') + + const signDataItem = createSigner(wallet) + + const res = await signDataItem(async ({ publicKey, type }) => { + assert.equal(type, 1) + assert.ok(pubKey.equals(publicKey)) - const signDataItem = createDataItemSigner(wallet) + return createDataItemBytes('foobar', { type, publicKey }, { + tags: [{ name: 'foo', value: 'bar' }], + target: 'xwOgX-MmqN5_-Ny_zNu2A8o-PnTGsoRb_3FrtiMAkuw', + anchor: randomBytes(32) + }) + }, DATAITEM_SIGNER_KIND) + + assert.ok(res) + }) - const res = await signDataItem({ - data: 'foobar', - tags: [{ name: 'foo', value: 'bar' }], + test('should create a valid signed data item', async () => { + const wallet = JSON.parse(readFileSync(tmpWallet).toString()) + const signer = toDataItemSigner(createSigner(wallet)) + + const res = await signer({ + data: 'foo', + tags: [ + { name: 'foo', value: 'bar' } + ], target: 'xwOgX-MmqN5_-Ny_zNu2A8o-PnTGsoRb_3FrtiMAkuw', anchor: randomBytes(32) }) - console.log('signedDataItem', res) assert.ok(res.id) assert.ok(res.raw) + + const isValid = await verify(res.raw) + assert.ok(isValid) + }) + + test('should create a valid signed http message', async () => { + const wallet = JSON.parse(readFileSync(tmpWallet).toString()) + const signer = toHttpSigner(createSigner(wallet)) + + const req = await signer({ + request: { + url: 'http://foo.bar/hello/world', + method: 'GET', + headers: { + foo: 'bar', + fizz: 'buzz' + } + }, + fields: [ + 'foo', + 'fizz', + '@path' + ].sort() + }) + + const headers = new Headers(req.headers) + assert.ok(headers.has('signature')) + assert.ok(headers.has('signature-input')) + + const verifier = createVerifier(createPublicKey({ key: wallet, format: 'jwk' }), 'rsa-pss-sha512') + const isValid = await verifyMessage({ + keyLookup: (params) => { + assert.equal(params.keyid, wallet.n) + assert.equal(params.alg, 'rsa-pss-sha512') + + return { verify: verifier } + } + }, req) + + assert.ok(isValid) }) }) }) diff --git a/connect/src/client/signer.js b/connect/src/client/signer.js new file mode 100644 index 000000000..cf02dcde3 --- /dev/null +++ b/connect/src/client/signer.js @@ -0,0 +1,229 @@ +import { Buffer as BufferShim } from 'buffer/index.js' +import base64url from 'base64url' +import { httpbis } from 'http-message-signatures' +import { parseItem, serializeList } from 'structured-headers' + +import { createDataItemBytes, getSignatureData, verify } from '../lib/data-item.js' +import { httpSigName } from './hb.js' + +const { augmentHeaders, createSignatureBase, createSigningParameters, formatSignatureBase } = httpbis + +if (!globalThis.Buffer) globalThis.Buffer = BufferShim + +/** + * Convert the value into a Uint8Array + * @param {string | Uint8Array} value + * @returns {globalThis.Buffer} + */ +const toView = (value) => { + if (ArrayBuffer.isView(value)) value = Buffer.from(value.buffer, value.byteOffset, value.byteLength) + else if (typeof value === 'string') value = base64url.toBuffer(value) + else throw new Error('Unexpected type. Value must be one of Uint8Array, ArrayBuffer, or base64url-encoded string') + return value +} + +export const DATAITEM_SIGNER_KIND = 'ans104' +export const HTTP_SIGNER_KIND = 'httpsig' + +export const toDataItemSigner = (signer) => { + return async ({ data, tags, target, anchor }) => { + let resolveUnsigned + let createCalled + /** + * @type {Promise} + */ + const dataToSign = new Promise((resolve) => { resolveUnsigned = resolve }) + + /** + * receive the signing public credentials and + * extract what we need to construct the unsigned + * data item + */ + const create = async (injected) => { + createCalled = true + /** + * If the signer wishes to receive the arguments + * and skip serialization to a data item, they can provide this argument. + * + * This is useful for signers that internally serialize data items, + * and drive UI off of the provided inputs ie. ArConnect + */ + if (injected.passthrough) return { data, tags, target, anchor } + + /** + * Default the type and alg to be + * - type: arweave + * - alg: RSA PSS SHA256 (default for arweave signing) + */ + // eslint-disable-next-line no-unused-vars + const { publicKey, type = 1, alg = 'rsa-v1_5-sha256' } = injected + + const unsigned = createDataItemBytes( + data, + { type, publicKey: toView(publicKey) }, + { target, tags, anchor } + ) + + /** + * What is actually signed is the DataItem + * deephash, so stash the unsigned bytes, + * and resolve the deepHash. + * + * When the signature is ultimately received, + * we can add it to the unsigned bytes + */ + resolveUnsigned(unsigned) + const deepHash = await getSignatureData(unsigned) + return deepHash + } + + return signer(create, DATAITEM_SIGNER_KIND) + .then((res) => { + /** + * Ensure create was called in order to produce the signature + */ + if (!createCalled) { + throw new Error('create() must be invoked in order to construct the data to sign') + } + + /** + * The signer has done the work + */ + if (typeof res === 'object' && res.id && res.raw) return res + + if (!res.signature || !res.signature) { + throw new Error('signer must return its signature and address') + } + const { signature } = res + return dataToSign.then((unsigned) => { + return Promise.resolve(signature) + .then(toView) + .then(async (rawSig) => { + /** + * Add signature to the data item in the proper + * position: after the first 2 bytes reserved for signature type + */ + const signedBytes = unsigned + signedBytes.set(rawSig, 2) + + const isValid = await verify(signedBytes) + if (!isValid) throw new Error('Data Item signature is not valid') + + return { + /** + * A data item's ID is the base64url encoded + * SHA-256 of the signature + */ + id: await crypto.subtle.digest('SHA-256', rawSig) + .then(raw => base64url.encode(raw)), + raw: signedBytes + } + }) + }) + }) + } +} + +export const toHttpSigner = (signer) => { + const params = ['alg', 'keyid'].sort() + + return async ({ request, fields }) => { + let resolveUnsigned + let createCalled + + const httpSig = {} + /** + * @type {Promise} + */ + const dataToSign = new Promise((resolve) => { resolveUnsigned = resolve }) + /** + * receive the signing public credentials and + * extract what we need to construct the unsigned + * data item + */ + .then() + + const create = (injected) => { + createCalled = true + + /** + * Default the type and alg to be + * - type: arweave + * - alg: RSA PSS SHA512 (default for arweave http signing) + */ + // eslint-disable-next-line no-unused-vars + let { publicKey, type = 1, alg = 'rsa-pss-sha512' } = injected + + /** + * TODO: do we need to check signature type + * and only allow supported keys? + * + * For now allowing through, and letting HyperBEAM + * handle + */ + // const meta = lookupSignatureMeta(type) + // if (meta.type !== 1) { + // throw new Error('HTTP Signed Messages currently can only be signed using Arweave keys') + // } + // if (meta.pubLength !== publicKey.byteLength) { + // throw new Error('HTTP Signed Messages public does not match expected length') + // } + + publicKey = toView(publicKey) + const signingParameters = createSigningParameters({ + params, + paramValues: { + keyid: base64url.encode(publicKey), + alg + } + }) + const signatureBase = createSignatureBase({ fields }, request) + const signatureInput = serializeList([ + [signatureBase.map(([item]) => parseItem(item)), signingParameters] + ]) + signatureBase.push(['"@signature-params"', [signatureInput]]) + const base = formatSignatureBase(signatureBase) + + httpSig.signatureInput = signatureInput + httpSig.signatureBase = base + + const encoded = new TextEncoder().encode(base) + resolveUnsigned(encoded) + return encoded + } + + return signer(create, HTTP_SIGNER_KIND) + .then((res) => { + if (!res.signature || !res.signature) { + throw new Error('signer must return its signature and address') + } + + const { signature, address } = res + /** + * Ensure create was called in order to produce the signature + */ + if (!createCalled) { + throw new Error('create() must be invoked in order to construct the data to sign') + } + + return dataToSign.then(() => { + return Promise.resolve(signature) + .then(toView) + .then((rawSig) => { + const withSignature = augmentHeaders( + request.headers, + rawSig, + httpSig.signatureInput, + httpSigName(address) + ) + + const signedRequest = { ...request, headers: withSignature } + + // TODO: verify the request is properly signed + + return signedRequest + }) + }) + }) + } +} diff --git a/connect/src/dal.js b/connect/src/dal.js index 7fecdf8d4..88530f530 100644 --- a/connect/src/dal.js +++ b/connect/src/dal.js @@ -1,8 +1,8 @@ import { z } from 'zod' -const tagSchema = z.object({ +export const tagSchema = z.object({ name: z.string(), - value: z.string() + value: z.any() }) // CU @@ -53,7 +53,7 @@ export const deployMessageSchema = z.function() data: z.any(), tags: z.array(tagSchema), anchor: z.string().optional(), - signer: z.any() + signer: z.any().nullish() })) .returns(z.promise( z.object({ @@ -65,7 +65,7 @@ export const deployProcessSchema = z.function() .args(z.object({ data: z.any(), tags: z.array(tagSchema), - signer: z.any() + signer: z.any().nullish() })) .returns(z.promise( z.object({ @@ -126,22 +126,6 @@ export const loadTransactionMetaSchema = z.function() )) export const signerSchema = z.function() - .args(z.object({ - data: z.any(), - tags: z.array(tagSchema), - /** - * target must be set with writeMessage, - * but not for createProcess - */ - target: z.string().optional(), - anchor: z.string().optional() - })) - .returns(z.promise( - z.object({ - id: z.string(), - raw: z.any() - }) - )) /** * A hack to get reuse JSDoc types in other files diff --git a/connect/src/hb.playground.js b/connect/src/hb.playground.js new file mode 100644 index 000000000..abbb4f31c --- /dev/null +++ b/connect/src/hb.playground.js @@ -0,0 +1,83 @@ +/* eslint-disable no-unused-vars */ +import { describe, test, before } from 'node:test' +// import * as assert from 'node:assert' +import { tmpdir } from 'node:os' +import { writeFileSync, readFileSync } from 'node:fs' +import { randomBytes } from 'node:crypto' +import { join } from 'node:path' + +import Arweave from 'arweave' +import { tap } from 'ramda' + +import { connect } from './index.js' + +describe('hb playground', () => { + /** + * Generate a wallet in a temporary directory prior to running the tests + */ + let tmpWallet + before(async () => { + const arweave = Arweave.init() + + tmpWallet = join(tmpdir(), `${randomBytes(16).toString('hex')}.json`) + writeFileSync( + tmpWallet, + JSON.stringify(await arweave.wallets.generate()) + ) + }) + + describe('HyperBEAM mode', () => { + test('should relay the message through HyperBEAM', async () => { + const wallet = JSON.parse(readFileSync(tmpWallet).toString()) + + const { request, spawn, message, result, createDataItemSigner } = connect({ + MODE: 'mainnet', + wallet, + device: 'process@1.0', + URL: process.env.HB_URL || 'http://localhost:8734' + }) + + const address = await fetch(process.env.HB_URL + '/~meta@1.0/info/address').then((res) => res.text()) + + console.log(address) + // uncomment examples as needed + + const res = await request(`~simple-pay@1.0/balance?address=${address}`, { + method: 'GET' + }) + + // const p = await spawn({ + // scheduler: address, + // module: 'bkjb55i07GUCUSWROtKK4HU1mBS_X0TyH3M5jMV6aPg', + // data: 'print("Process initialized.")', + // tags: [ + // { name: 'device', value: 'process@1.0' }, + // { name: 'scheduler-device', value: 'scheduler@1.0' }, + // { name: 'execution-device', value: 'compute-lite@1.0' }, + // { name: 'authority', value: address }, + // { name: 'scheduler-location', value: address }, + // { name: 'scheduler', value: address }, + // { name: 'random-seed', value: randomBytes(16).toString('hex') } + // ], + // signer: createDataItemSigner() + // }).then(tap(console.log)) + + // const m = await message({ + // process: p, + // tags: [ + // { name: 'Action', value: 'Eval' }, + // { name: 'Data-Protocol', value: 'ao' }, + // { name: 'Variant', value: 'ao.TN.1' }, + // { name: 'Type', value: 'Message' } + // ], + // data: "Send({ Target = ao.id, Data = 'gday mate' })", + // signer: createDataItemSigner() + // }).then(tap(console.log)) + + // console.log('READY TO LOAD RESULT. m:', m) + + // const r = await result({ message: m, process: p }) + // .then(tap(console.log)).catch(console.error) + }) + }) +}) diff --git a/connect/src/index.browser.js b/connect/src/index.browser.js index 2d056ce9c..8954642c1 100644 --- a/connect/src/index.browser.js +++ b/connect/src/index.browser.js @@ -9,33 +9,37 @@ const GRAPHQL_URL = globalThis.GRAPHQL_URL || undefined const GRAPHQL_MAX_RETRIES = globalThis.GRAPHQL_MAX_RETRIES || undefined const GRAPHQL_RETRY_BACKOFF = globalThis.GRAPHQL_RETRY_BACKOFF || undefined -const HB_URL = globalThis.HB_URL || undefined +const RELAY_URL = globalThis.RELAY_URL || undefined +const AO_URL = globalThis.AO_URL = undefined const connect = connectWith({ createDataItemSigner: WalletClient.createDataItemSigner, - createHbSigner: WalletClient.createHbSigner + createSigner: WalletClient.createSigner }) -const { result, results, message, spawn, monitor, unmonitor, dryrun, assign, createDataItemSigner } = connect({ +const createDataItemSigner = WalletClient.createDataItemSigner +const createSigner = WalletClient.createSigner + +export { createDataItemSigner, createSigner } +export { connect } +export { serializeCron } + +/** + * @deprecated top level exports will soon be removed. Use connect() + * instead. + * + * These will only operate in legacy mode + */ +const { result, results, message, spawn, monitor, unmonitor, dryrun, assign } = connect({ + MODE: 'legacy', GATEWAY_URL, MU_URL, CU_URL, + RELAY_URL, + AO_URL, GRAPHQL_URL, GRAPHQL_MAX_RETRIES, - GRAPHQL_RETRY_BACKOFF + GRAPHQL_RETRY_BACKOFF, + noLog: true }) - -const originalHb = connect.hb -connect.hb = ({ URL = HB_URL, ...rest }) => originalHb({ URL, ...rest }) - export { result, results, message, spawn, monitor, unmonitor, dryrun, assign } -export { connect } -export { serializeCron } -/** - * A function that builds a signer using the global arweaveWallet - * commonly used in browser-based dApps - * - * This is provided as a convenience for consumers of the SDK - * to use, but consumers can also implement their own signer - */ -export { createDataItemSigner } diff --git a/connect/src/index.common.js b/connect/src/index.common.js index 552aa3948..283ae450b 100644 --- a/connect/src/index.common.js +++ b/connect/src/index.common.js @@ -2,11 +2,11 @@ import { connect as schedulerUtilsConnect } from '@permaweb/ao-scheduler-utils' import * as MuClient from './client/ao-mu.js' import * as CuClient from './client/ao-cu.js' -import * as SuClient from './client/ao-su.js' import * as GatewayClient from './client/gateway.js' import * as HbClient from './client/hb.js' import { createLogger } from './logger.js' +import { requestWith } from './lib/request/index.js' import { resultWith } from './lib/result/index.js' import { messageWith } from './lib/message/index.js' import { spawnWith } from './lib/spawn/index.js' @@ -16,174 +16,301 @@ import { resultsWith } from './lib/results/index.js' import { dryrunWith } from './lib/dryrun/index.js' import { assignWith } from './lib/assign/index.js' import { joinUrl } from './lib/utils.js' +import { getOperator, getNodeBalance } from './lib/payments/index.js' + // eslint-disable-next-line no-unused-vars import { Types } from './dal.js' const DEFAULT_GATEWAY_URL = 'https://arweave.net' const DEFAULT_MU_URL = 'https://mu.ao-testnet.xyz' const DEFAULT_CU_URL = 'https://cu.ao-testnet.xyz' -/** - * TODO: set this when we know it - */ -const DEFAULT_HB_URL = 'http://localhost:8734' +const DEFAULT_RELAY_URL = 'http://relay.ao-hb.xyz' +// eslint-disable-next-line no-unused-vars +const DEFAULT_AO_URL = 'http://m2.ao.computer' +const DEFAULT_RELAY_CU_URL = 'http://cu.s451-comm3-main.xyz' +const DEFAULT_RELAY_MU_URL = 'http://mu.s451-comm3-main.xyz' +const DEFAULT_DEVICE = 'relay@1.0' const defaultFetch = fetch export { serializeCron } from './lib/serializeCron/index.js' /** - * @param {{ createDataItemSigner: (wallet:any) => Types['signer'], createHbSigner: any }} + * @param {{ createDataItemSigner: (wallet:any) => Types['signer'], createSigner: any }} */ -export function connectWith ({ createDataItemSigner, createHbSigner }) { - const logger = createLogger() +export function connectWith ({ createDataItemSigner, createSigner }) { + const _logger = createLogger() /** - * Build the sdk apis using the provided ao component urls. You can currently specify - * - * - a GATEWAY_URL - * - a GRAPHQL_URL (defaults to GATEWAY_URL/graphql) - * - a GRAPHQL_MAX_RETRIES. Defaults to 0 - * - a GRAPHQL_RETRY_BACKOFF. Defaults to 300 (moot if GRAPHQL_MAX_RETRIES is set to 0) - * - a Messenger Unit URL - * - a Compute Unit URL - * - * If any value is not provided, an SDK default will be used. - * Invoking connect() with no parameters or an empty object is functionally equivalent - * to using the top-lvl exports of the SDK ie. - * - * @example - * import { - * spawn, - * message, - * result, - * results, - * monitor, - * connect - * } from '@permaweb/ao-sdk'; + * Run connect in HyperBEAM mode, relaying network calls through the configured + * HyperBEAM node. * - * // These are functionally equivalent - * connect() == { spawn, message, result, results, monitor } + * @deprecated - relay mode will instead be triggered by a device passed to + * mainnet mode. This should be removed at earliest convenience. * - * @typedef Services - * @property {string} [GATEWAY_URL] - the url of the desried Gateway. - * @property {string} [GRAPHQL_URL] - the url of the desired Arweave Gateway GraphQL Server - * @property {number} [GRAPHQL_MAX_RETRIES] - the number of times to retry querying the gateway, utilizing an exponential backoff - * @property {number} [GRAPHQL_RETRY_BACKOFF] - the initial backoff, in milliseconds (moot if GRAPHQL_MAX_RETRIES is set to 0) - * @property {string} [MU_URL] - the url of the desried ao Messenger Unit. - * @property {string} [CU_URL] - the url of the desried ao Compute Unit. + * @typedef HyperBeam + * @property {any} wallet - the wallet to use to sign HyperBEAM HTTP messages + * @property {string} [RELAY_URL] the HyperBEAM node * - * @param {Services} [services] + * @param {Services & HyperBeam} args + * @returns {Omit, 'createDataItemSigner'> & { createDataItemSigner: () => Types['signer'] }} */ - function connect ({ + // eslint-disable-next-line no-unused-vars + function relayMode ({ + MODE, + signer, + GRAPHQL_URL, + GRAPHQL_MAX_RETRIES, + GRAPHQL_RETRY_BACKOFF, + MU_URL = DEFAULT_RELAY_MU_URL, + CU_URL = DEFAULT_RELAY_CU_URL, + RELAY_URL = DEFAULT_RELAY_URL, + fetch: originalFetch = defaultFetch + }) { + const logger = _logger.child('relay') + logger('Mode Activated 🔀') + + if (!signer) { throw new Error('relay mode requires providing a signer to connect()') } + + const relayDataItemSigner = signer ? () => signer : createDataItemSigner + + const relaySigner = signer ? () => signer : createSigner + + const fetch = HbClient.relayerWith({ + /** + * Always wrap default fetch with relayer, + * no embellishment beyond the relayer + */ + fetch: defaultFetch, + logger, + HB_URL: RELAY_URL, + signer + }) + + const requestLogger = logger.child('request') + const request = requestWith({ + logger: requestLogger, + request: HbClient.requestWith({ + fetch: defaultFetch, + logger: requestLogger, + HB_URL: RELAY_URL, + signer + }) + }) + + const { validate } = schedulerUtilsConnect({ + cacheSize: 100, + GRAPHQL_URL, + GRAPHQL_MAX_RETRIES, + GRAPHQL_RETRY_BACKOFF + }) + + const resultLogger = logger.child('result') + const result = resultWith({ + signer, + loadResult: CuClient.loadResultWith({ + fetch, + CU_URL, + logger: resultLogger + }), + logger: resultLogger + }) + + const messageLogger = logger.child('message') + const message = messageWith({ + signer, + deployMessage: MuClient.deployMessageWith({ + fetch, + MU_URL, + logger: messageLogger + }), + logger: messageLogger + }) + + const spawnLogger = logger.child('spawn') + const spawn = spawnWith({ + signer, + loadTransactionMeta: GatewayClient.loadTransactionMetaWith({ + fetch: originalFetch, + GRAPHQL_URL, + logger: spawnLogger + }), + validateScheduler: validate, + deployProcess: MuClient.deployProcessWith({ + fetch, + MU_URL, + logger: spawnLogger + }), + logger: spawnLogger + }) + + const monitorLogger = logger.child('monitor') + const monitor = monitorWith({ + signer, + deployMonitor: MuClient.deployMonitorWith({ + fetch, + MU_URL, + logger: monitorLogger + }), + logger: monitorLogger + }) + + const unmonitorLogger = logger.child('unmonitor') + const unmonitor = unmonitorWith({ + signer, + deployUnmonitor: MuClient.deployUnmonitorWith({ + fetch, + MU_URL, + logger: unmonitorLogger + }), + logger: monitorLogger + }) + + const resultsLogger = logger.child('results') + const results = resultsWith({ + signer, + queryResults: CuClient.queryResultsWith({ + fetch, + CU_URL, + logger: resultsLogger + }), + logger: resultsLogger + }) + + const dryrunLogger = logger.child('dryrun') + const dryrun = dryrunWith({ + signer, + dryrunFetch: CuClient.dryrunFetchWith({ + fetch, + CU_URL, + logger: dryrunLogger + }), + logger: dryrunLogger + }) + + const assignLogger = logger.child('assign') + const assign = assignWith({ + signer, + deployAssign: MuClient.deployAssignWith({ + fetch, + MU_URL, + logger: assignLogger + }), + logger: messageLogger + }) + + return { + MODE, + request, + result, + results, + message, + spawn, + monitor, + unmonitor, + dryrun, + assign, + ccreateDataItemSigner: relayDataItemSigner, + createSigner: relaySigner + } + } + + function legacyMode ({ + MODE, GRAPHQL_URL, GRAPHQL_MAX_RETRIES, GRAPHQL_RETRY_BACKOFF, - GATEWAY_URL = DEFAULT_GATEWAY_URL, MU_URL = DEFAULT_MU_URL, CU_URL = DEFAULT_CU_URL, fetch = defaultFetch, - /** - * Only set when augmented with connect.hb(), - * and so default to the provided fetch otherwise. - */ - originalFetch = fetch - } = {}) { - if (!GRAPHQL_URL) GRAPHQL_URL = joinUrl({ url: GATEWAY_URL, path: '/graphql' }) - - const { validate } = schedulerUtilsConnect({ cacheSize: 100, GRAPHQL_URL, GRAPHQL_MAX_RETRIES, GRAPHQL_RETRY_BACKOFF }) + noLog + }) { + const logger = _logger.child('legacy') + if (!noLog) logger('Mode Activated ℹ️') - const processMetaCache = SuClient.createProcessMetaCache({ MAX_SIZE: 25 }) + const { validate } = schedulerUtilsConnect({ + cacheSize: 100, + GRAPHQL_URL, + GRAPHQL_MAX_RETRIES, + GRAPHQL_RETRY_BACKOFF + }) const resultLogger = logger.child('result') const result = resultWith({ - loadResult: CuClient.loadResultWith({ fetch, CU_URL, logger: resultLogger }), + loadResult: CuClient.loadResultWith({ + fetch, + CU_URL, + logger: resultLogger + }), logger: resultLogger }) - /** - * default writeInteraction that works OOTB - * - writes signed data item for message to the MU - */ const messageLogger = logger.child('message') const message = messageWith({ - loadProcessMeta: SuClient.loadProcessMetaWith({ + deployMessage: MuClient.deployMessageWith({ fetch, - cache: processMetaCache, + MU_URL, logger: messageLogger }), - // locateScheduler: locate, - deployMessage: MuClient.deployMessageWith({ fetch, MU_URL, logger: messageLogger }), logger: messageLogger }) - /** - * default spawn that works OOTB - * - Verifies the inputs - * - spawns the process via the MU - */ const spawnLogger = logger.child('spawn') const spawn = spawnWith({ - loadTransactionMeta: GatewayClient.loadTransactionMetaWith({ fetch: originalFetch, GRAPHQL_URL, logger: spawnLogger }), + loadTransactionMeta: GatewayClient.loadTransactionMetaWith({ + fetch, + GRAPHQL_URL, + logger: spawnLogger + }), validateScheduler: validate, - deployProcess: MuClient.deployProcessWith({ fetch, MU_URL, logger: spawnLogger }), + deployProcess: MuClient.deployProcessWith({ + fetch, + MU_URL, + logger: spawnLogger + }), logger: spawnLogger }) - /** - * default monitor that works OOTB - * - Verifies the inputs - * - post a signed message via the MU /monitor/:process endpoint - */ const monitorLogger = logger.child('monitor') const monitor = monitorWith({ - loadProcessMeta: SuClient.loadProcessMetaWith({ + deployMonitor: MuClient.deployMonitorWith({ fetch, - cache: processMetaCache, + MU_URL, logger: monitorLogger }), - // locateScheduler: locate, - deployMonitor: MuClient.deployMonitorWith({ fetch, MU_URL, logger: monitorLogger }), logger: monitorLogger }) - /** - * default unmonitor that works OOTB - * - Verifies the inputs - * - post a signed message via the MU /monitor/:process endpoint - */ const unmonitorLogger = logger.child('unmonitor') const unmonitor = unmonitorWith({ - loadProcessMeta: SuClient.loadProcessMetaWith({ + deployUnmonitor: MuClient.deployUnmonitorWith({ fetch, - cache: processMetaCache, + MU_URL, logger: unmonitorLogger }), - // locateScheduler: locate, - deployUnmonitor: MuClient.deployUnmonitorWith({ fetch, MU_URL, logger: unmonitorLogger }), logger: monitorLogger }) - /** - * results - returns batch of Process Results given a specified range - */ const resultsLogger = logger.child('results') const results = resultsWith({ - queryResults: CuClient.queryResultsWith({ fetch, CU_URL, logger: resultsLogger }), + queryResults: CuClient.queryResultsWith({ + fetch, + CU_URL, + logger: resultsLogger + }), logger: resultsLogger }) - /** - * dryrun - sends a message object to the cu and returns a result - */ const dryrunLogger = logger.child('dryrun') const dryrun = dryrunWith({ - dryrunFetch: CuClient.dryrunFetchWith({ fetch, CU_URL, logger: dryrunLogger }), + dryrunFetch: CuClient.dryrunFetchWith({ + fetch, + CU_URL, + logger: dryrunLogger + }), logger: dryrunLogger }) - /** - * POSTs an Assignment to the MU - */ const assignLogger = logger.child('assign') const assign = assignWith({ deployAssign: MuClient.deployAssignWith({ @@ -194,59 +321,349 @@ export function connectWith ({ createDataItemSigner, createHbSigner }) { logger: messageLogger }) - return { result, results, message, spawn, monitor, unmonitor, dryrun, assign, createDataItemSigner } + return { + MODE, + result, + results, + message, + spawn, + monitor, + unmonitor, + dryrun, + assign, + createDataItemSigner + } } - /** - * Run connect in HyperBEAM mode, relaying network calls through the configured - * HyperBEAM node. - * - * @typedef HyperBeam - * @property {any} wallet - the wallet to use to sign HyperBEAM HTTP messages - * @property {string} [URL] the HyperBEAM node - * - * @param {Services & HyperBeam} args - * @returns {Omit, 'createDataItemSigner'> & { createDataItemSigner: () => Types['signer'] }} - */ - connect.hb = function ({ - wallet, - URL = DEFAULT_HB_URL, - fetch: originalFetch = defaultFetch, - ...rest + function mainnetMode ({ + MODE, + signer, + GRAPHQL_URL, + device = DEFAULT_DEVICE, + URL = DEFAULT_RELAY_URL, + MU_URL = DEFAULT_RELAY_MU_URL, + CU_URL = DEFAULT_RELAY_CU_URL, + fetch = defaultFetch }) { - logger('HyperBEAM mode activated! ⚡️⭐️') + const isRelayMode = device === 'relay@1.0' + const logger = isRelayMode + ? _logger.child('mainnet-relay') + : _logger.child('mainnet-process') + logger('Mode Activated %s', isRelayMode ? '🔀' : '🐲') - const api = connect({ - ...rest, - /** - * Downstream logic, that does not need to be relayed - * may still want the original fetch, so inject it here - */ - originalFetch, + if (!signer) { throw new Error('mainnet mode requires providing a signer to connect()') } + + const mainnetDataItemSigner = signer ? () => signer : createDataItemSigner + + const mainnetSigner = signer ? () => signer : createSigner + + const relayFetch = HbClient.relayerWith({ /** - * Use the HyperBEAM relay as fetch for all downstream - * dependencies + * Always wrap default fetch with relayer, + * no embellishment beyond the relayer */ - fetch: HbClient.relayerWith({ - ...rest, - /** - * Always wrap default fetch with relayer, - * no embellishment beyond the relayer - */ + fetch: defaultFetch, + logger, + HB_URL: URL, + signer + }) + + /** + * TODO: implement validating a scheduler + */ + async function mockValidate (address) { + logger('Mock validation for address "%s"', address) + return true + } + + const resultLogger = logger.child('result') + const loadResult = isRelayMode + ? CuClient.loadResultWith({ + fetch: relayFetch, + logger: resultLogger, + HB_URL: URL, + CU_URL, + signer + }) + : HbClient.loadResultWith({ fetch: defaultFetch, - logger, + logger: resultLogger, HB_URL: URL, - signer: createHbSigner(wallet) + signer }) + const result = resultWith({ + signer, + loadResult, + logger: resultLogger }) - const createDataItemSigner = api.createDataItemSigner - /** - * createDataItemSigner is restricted to only - * use the wallet configured with HyperBEAM - */ - api.createDataItemSigner = () => createDataItemSigner(wallet) - return api + const messageLogger = logger.child('message') + const deployMessage = isRelayMode + ? MuClient.deployMessageWith({ + fetch: isRelayMode ? relayFetch : defaultFetch, + logger: messageLogger, + HB_URL: URL, + MU_URL, + CU_URL, + signer + }) + : HbClient.deployMessageWith({ + fetch: defaultFetch, + logger: messageLogger, + HB_URL: URL, + signer + }) + const message = messageWith({ + signer, + deployMessage, + logger: messageLogger + }) + + const resultsLogger = logger.child('results') + const queryResults = isRelayMode + ? CuClient.queryResultsWith({ + fetch: relayFetch, + CU_URL, + logger: resultsLogger + }) + : HbClient.queryResultsWith({ + fetch: defaultFetch, + HB_URL: URL, + logger: resultsLogger, + signer + }) + + const results = resultsWith({ + signer, + queryResults, + logger: resultsLogger + }) + + const spawnLogger = logger.child('spawn') + const deployProcess = isRelayMode + ? MuClient.deployProcessWith({ + fetch: relayFetch, + HB_URL: URL, + MU_URL, + logger: spawnLogger + }) + : HbClient.deployProcessWith({ + fetch: defaultFetch, + logger: spawnLogger, + HB_URL: URL, + signer + }) + + const spawn = spawnWith({ + signer, + loadTransactionMeta: GatewayClient.loadTransactionMetaWith({ + fetch, + GRAPHQL_URL, + logger: spawnLogger + }), + validateScheduler: mockValidate, + deployProcess, + logger: spawnLogger + }) + + const dryrunLogger = logger.child('dryrun') + const dryrun = dryrunWith({ + signer, + dryrunFetch: CuClient.dryrunFetchWith({ + fetch: isRelayMode ? relayFetch : fetch, + CU_URL, + logger: dryrunLogger + }), + logger: dryrunLogger + }) + + const requestLogger = logger.child('request') + + const request = requestWith({ + signer, + logger: requestLogger, + MODE, + method: 'GET', + device, + dryrun, + message, + result, + spawn, + request: HbClient.requestWith({ + fetch: defaultFetch, + logger: requestLogger, + HB_URL: URL, + signer + }) + }) + + const getLogger = logger.child('get') + const get = requestWith({ + logger: getLogger, + MODE, + method: 'GET', + device, + dryrun, + message, + result, + spawn, + signer, + request: HbClient.requestWith({ + fetch: defaultFetch, + method: 'GET', + logger: getLogger, + HB_URL: URL, + signer + }) + }) + + const postLogger = logger.child('post') + const post = requestWith({ + signer, + logger: postLogger, + MODE, + method: 'POST', + device, + dryrun, + message, + result, + spawn, + request: HbClient.requestWith({ + fetch: defaultFetch, + method: 'GET', + logger: postLogger, + HB_URL: URL, + signer + }) + }) + // const monitorLogger = logger.child('monitor') + // const monitor = monitorWith({ + // deployMonitor: MuClient.deployMonitorWith({ fetch, MU_URL, logger: monitorLogger }), + // logger: monitorLogger + // }) + + // const unmonitorLogger = logger.child('unmonitor') + // const unmonitor = unmonitorWith({ + // deployUnmonitor: MuClient.deployUnmonitorWith({ fetch, MU_URL, logger: unmonitorLogger }), + // logger: monitorLogger + // }) + + // const resultsLogger = logger.child('results') + // const results = resultsWith({ + // queryResults: CuClient.queryResultsWith({ fetch, CU_URL, logger: resultsLogger }), + // logger: resultsLogger + // }) + + // const dryrunLogger = logger.child('dryrun') + // const dryrun = dryrunWith({ + // dryrunFetch: CuClient.dryrunFetchWith({ fetch, CU_URL, logger: dryrunLogger }), + // logger: dryrunLogger + // }) + + // const assignLogger = logger.child('assign') + // const assign = assignWith({ + // deployAssign: MuClient.deployAssignWith({ + // fetch, + // MU_URL, + // logger: assignLogger + // }), + // logger: messageLogger + // }) + + return { + MODE: isRelayMode ? 'relay' : 'mainnet', + request, + get, + post, + result, + message, + spawn, + results, + getOperator: getOperator({ fetch, URL }), + getNodeBalance: getNodeBalance({ + request: HbClient.requestWith({ + fetch: defaultFetch, + method: 'GET', + logger: postLogger, + HB_URL: URL, + signer + }) + }), + createDataItemSigner: mainnetDataItemSigner, + createSigner: mainnetSigner + } + } + + /** + * Build the sdk apis using the provided configuration. You can currently specify + * + * - a GATEWAY_URL + * - a GRAPHQL_URL (defaults to GATEWAY_URL/graphql) + * - a GRAPHQL_MAX_RETRIES. Defaults to 0 + * - a GRAPHQL_RETRY_BACKOFF. Defaults to 300 (moot if GRAPHQL_MAX_RETRIES is set to 0) + * - a Messenger Unit URL + * - a Compute Unit URL + * - a AO Unit URL (in mainnet mode) + * - a Relay Unit URL (in relay mode) + * - a wallet to use for signing HTTP messages (in mainnet or relay mode) + * + * If any value is not provided, an SDK default will be used. + * Invoking connect() with no parameters or an empty object is functionally equivalent + * to using the top-lvl exports of the SDK ie. + * + * @example + * import { + * spawn, + * message, + * result, + * results, + * monitor, + * connect + * } from '@permaweb/ao-sdk'; + * + * // These are functionally equivalent + * connect() == { spawn, message, result, results, monitor } + * + * @typedef {'legacy' | 'mainnet'} ConnectMode + * + * @typedef ConnectArgsShared + * @property {string} [GATEWAY_URL] - the url of the desried Gateway. + * @property {string} [GRAPHQL_URL] - the url of the desired Arweave Gateway GraphQL Server + * @property {number} [GRAPHQL_MAX_RETRIES] - the number of times to retry querying the gateway, utilizing an exponential backoff + * @property {number} [GRAPHQL_RETRY_BACKOFF] - the initial backoff, in milliseconds (moot if GRAPHQL_MAX_RETRIES is set to 0) + * @property {string} [MU_URL] - the url of the desried ao Messenger Unit. Also used as the relay MU in 'relay' mode + * @property {string} [CU_URL] - the url of the desried ao Compute Unit. Also used as the relay CU in 'relay' mode + * + * @typedef ConnectArgsMainnet + * @property {Types['signer']} [signer] - the signer used to sign Data items and HTTP messages. + * @property {string} [URL] - the url of the desried ao Unit. Only applicable in 'mainnet' mode + * @property {string} [device] - the default path either 'relay@1.0' or 'process@1.0' + * + * @typedef ConnectArgs + * @property {ConnectMode} [MODE] - the mode that connect apis will be run in. + * + * @overload + * @param {{ MODE: 'legacy'} & ConnectArgsShared } args + * @returns {ReturnType} + * + * @overload + * @param {{ MODE: 'mainnet'} & ConnectArgsShared & ConnectArgsMainnet } args + * @returns {ReturnType} + * + * @param {ConnectArgs} args + * @returns {ReturnType | ReturnType} + */ + function connect (args = {}) { + let { GRAPHQL_URL, GATEWAY_URL = DEFAULT_GATEWAY_URL, ...restArgs } = args + + if (!GRAPHQL_URL) { GRAPHQL_URL = joinUrl({ url: GATEWAY_URL, path: '/graphql' }) } + + const MODE = args.MODE || 'legacy' + + if (MODE === 'legacy') return legacyMode({ ...restArgs, GRAPHQL_URL }) + if (MODE === 'mainnet') return mainnetMode({ ...restArgs, GRAPHQL_URL }) + + throw new Error(`Unrecognized MODE: ${MODE}`) } return connect diff --git a/connect/src/index.common.test.js b/connect/src/index.common.test.js new file mode 100644 index 000000000..6f622cdf6 --- /dev/null +++ b/connect/src/index.common.test.js @@ -0,0 +1,85 @@ +import { test } from 'node:test' +import * as assert from 'node:assert' + +import { connectWith } from './index.common.js' +import { WalletClient } from './client/node/index.js' + +// this is a test wallet do not use for anything other than testing +const WALLET = { + kty: 'RSA', + n: 'x5wjDziDSmSCvrWTiWk4avD-2DmolJHV6FOqwX8Dv4qvAJUDLq8kGg4r5NGj6yWS_f1N58FyhuAKcNgwBBj5Api5Kx5siL3vvcGywUaIID4wSn0BBjiTOf-IyEp0Ch4esGcf5fxNTzhX5X_2V8uPeXCGI-fbm877swOtgDSPc-ICAbML_CI0lOE60IP-KuWG7N0nsfibfDKs5fDv-JJTl21Dzxdryq44O873Kuyg9uOYIBW454UyBAYNMlSoMO753oLJvQvKi5n5X3ldnexD9bAwSzrjx1uhmtEr98ffxuplwsu0RkBOfolZS7GrD1mA_9zWKba-rreTus69gvbjSnHcQOkn6w11mGlL0_v95CN4kmthxlhuxPQS2R1pdkNYiX9IuULZcet-CO9Cnm6YDsNyvc84ly-iniUUR9Op2DWLzMuwNz9V-qJioKmGWmVE8V3tcu8RheAXnc2EoNwp3nhwV9a0GTcmrH7Dbpk-i60x75raxAYi5w_NAp44jddxueyxthW-3uyQmVtq_t-j1lMmrInTtRoIXi84bfZYzsxDqzx4E56xB2I7wQcOg2JSzLNVmzo6x2gVUSb9K2yCoIgM55eTdSS7sFG_fT60ftvR-RJR4fOpMHbMA8-aOcs6CLgKxFaVrVEWcSKpodKl_wjXXYJnPZE-xrXRyVwtx-c', + e: 'AQAB', + d: 'QMVnkv7Ri4hF7MBa2ZHtHraI230KOPuBn9vWYudmPfcwhk3UNIfcfR_wGlX0jM8qfYR-jR92nFGC0c809X9s_ey02UsMCOspKjf6W0EZ1uyXGvSpKm2dSIqkxely8f2IE9HCxgwywewUiYIWW7LQIaXjwS7xUgO-JP6ihCGKEx5ZbFe_IdevnEfhn9vU0_Ka761rvhJf0dNXy119YPmKam6oPEDrV19lG_MyvRj_ul1r9a1WzLmp2yhji9twoEBLkGp8L_3tgZM7GkH1oNMT0luTsDWxx_Tx4ZzSWZrgAFn7H1SecNVZIzJKaYeDbH2soFn2nJ6SDXvAUjPXxvF6GxCBp5I40mSaQW8CppI-ZWw1dbxMRjv2IQHAxsMN_TM-vk0YmjrzOr5SVWkZ3Eib6nor8kP9g0fdsA7Wlhocop8JhjdQ7XDxm0n-VLg75Wnk0o4hnmvYq5jnJg5k6mE0mBQ15mBgC8MhPZTb9VLySl_CXsXzLdT0UuOKpDGbHiWdVjT3Yo_afZt75vzQ0ys1Y5AILlFlibRyVOLnT2YE6906U3DkQLWYTs6XGWiYYS7rDPnZc24xhTj3WtCubINBOCfbcU558klgxM2Mp-TbFvLu0WPy-neeMopCsLbf-sBjWSdGE3RNSV9IuJWV4rQ2XEvtz4xS_E5-9BGw3IW-eaE', + p: '-3KdF6fI22lJ9Kv0HPMJmB4I3KgHgSpnkx99MirabiEFq3Trd5B_6iIeaCz3IKaPMvpujRTmJatNLaw2KNhtETaHQqJhw-_X82CJaYVJSFrmjnltD7dUIJW3LLokq4F7YiObd_oGWb75YKLx8WobX7E4BJd-8Bbcz2LsReYBLim6KKzzQPIw-uXfMcF_pnWA7W7S5JSBKtp458Mo6MYITlv4iAhU5cgXvZiURa57I7woTuSgl9Pmzn_ZR83tbmXK6EJA5J2nzavUgUTOaBmCkNGWztDvsb3XUwtcW48RQgDoHP6dlS4ZX7qBt5shvkDADj4Rc-X-V0pbqkWLCLKRvw', + q: 'yzlFM4JXVpBEO2s_2fmMDKGP6p-d2pv3M54-hjBHyvYOy8Zs_WrrlRm12zDXvV1YfOpHswKk3SxO9jgTAdHd_QPnXTKpO15IriuKhODPK9zRsM1pEm2tskfdv-KsE2TKULVWxBsS_sMn4pGFgTFIrE4aXezvnWBU_DBO31lRWnBHxflCJ3zl68YxNbn3Er--R4GBpyBle500-tiONDYh4eoY_TKlG3b6x4x4n1u5qz2WGr1VKhUro5GFvTj0WRlZvYGXMfIoTqu5dEApMsfRPq1qAcWU-tzy6FF1nwjypkgbr8lCcDYhNr64eRyCUUuArcVi84iIzxD3KNx2S6UD2Q', + dp: '1f1ej-ks2P2sANvuLkzvl1PIOvGNIXHTH7QmufEaX6sexiIE2oZRNhK_Se0qi9D0AXB7cPmeO-SjFAGBPhWiDIoZZq4HAdc2M1uu1eymxzsFB69zD3L315tSfnAUERlqxcSD5QEVMn-Cf6lsugWRMkkB4XaEgxMR7DTF2165Fm5QjZlSk60J3hyPbCq-1g0eIfK06-8uVigDyUmfoSoXm4hN5IciqUM2YRZe6UZqaakRrMKJoWym-op3gdJRBCkBG9R2oZlCW5imizThbFp5cYHNFElgFCX6ACSk_w6soz1eTH6r4W-QDJYPGxCdEOrOB9DuzXGomOUhSbFRd59wxQ', + dq: 'GmasSM7MDeDcHGQIfYbf3Nw4WCC4XygX60rJkKFBEmr47RwwGJQFWu9mIr2rqVwxHLlK60SSqnERKQeL4JalIjOZoQ_t0FqlUosxiaWzBF3BmBh3Z_97q0eO1VjbRgG4DtggF4X058furI5_K5N9f3T-E-muD2HuaHzWIkn2OauQh3WkVIDzVbf_uJ0aLgNe8ucuMsoQpQh9U4FCqCHIMRM6f9pOfMzuM3JaUUmXS1nK4Fpsb_UkIDHNkBGQHOsgL3BgdgqhlebVRvb24zP2SRA6T1Sd0CFYJTo_75M4AsYnYqTgzrcZhYUtbf54J5uJcgnMmxrHuy7XDSXm8FjVsQ', + qi: 'HJhf2ZP_PczoOoEMAw3cN6wdrZLG9J465tDjZ4HYqL9vrzPs7fPrXWJo4-WA-p_2IDXCkMP_t6H6JFyK1xHmDmjNpP7XlTwBb_hcEgn0W3dvmZ597Ey-B38IZfn0J4Wq3s34kcq3tprB5rG08qTm4d_tG-sln8Z7Ey-bLKTWPL_kIqpTCJ0H7cGvFVRMGN2dc9nPb4MYFRXhxZS7JF4SQJyRwPuHEMsY97Ph2IpNYpxKTGR1LfqWwSwnwrfyY_Y8sgkHMSNDvZcdGmaEYxhzTXa9xFGUdEFn2IAUIdvVz0aCBqC0soyfrkF955SDbCkbD2QxhyLX1DBVBcw_HEUCRA' +} + +test('connectWith use mainnet device=relay@1.0 mode to dryrun on a process', async () => { + const connect = connectWith({ + createDataItemSigner: WalletClient.createDataItemSigner, + createSigner: WalletClient.createSigner + }) + + const { get } = connect({ MODE: 'mainnet', signer: WalletClient.createSigner(WALLET) }) + const Ticker = await get({ + Target: 'WWjq4e5Oigct0QfBPdVF6a-zDKNMRoGPAojgv5yFcLU', + Action: 'Info', + dryrun: true + }).then(map => map.Messages[0].Ticker.text()) + + assert.equal(Ticker, 'PNTS') +}) + +test('connectWith use mainnet device=relay@1.0 mode to message on a process', async () => { + const connect = connectWith({ + createDataItemSigner: WalletClient.createDataItemSigner, + createSigner: WalletClient.createSigner + }) + + const { get } = connect({ MODE: 'mainnet', signer: WalletClient.createSigner(WALLET) }) + const Error = await get({ + Target: 'WWjq4e5Oigct0QfBPdVF6a-zDKNMRoGPAojgv5yFcLU', + Action: 'Transfer', + Quantity: '10000', + Recipient: 'cHencOZC-aCbPCDH2tEZ3Lhw3EM5oRw3kxgj-eEgtNc' + }).then(map => map.Messages[0].Error.text()) + + assert.equal(Error, 'Insufficient Balance!') +}) + +test('connectWith use mainnet device=relay@1.0 mode to spawn a process', async () => { + const connect = connectWith({ + createDataItemSigner: WalletClient.createDataItemSigner, + createSigner: WalletClient.createSigner + }) + + const { get } = connect({ MODE: 'mainnet', signer: WalletClient.createSigner(WALLET) }) + const process = await get({ + Type: 'Process', + Name: 'MyNewProcess', + Module: 'JArYBF-D8q2OmZ4Mok00sD2Y_6SYEQ7Hjx-6VZ_jl3g', + Scheduler: '_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA' + }) + // .then(result => result.Output.text()) + // console.log(result.Output.text()) + // .then(map => map) + assert.equal(process.length, 43) +}) + +test('connectWith should give us legacy connect mode', async () => { + const connect = connectWith({ + createDataItemSigner: WalletClient.createDataItemSigner, + createSigner: WalletClient.createSigner + }) + const { dryrun } = connect({ MODE: 'legacy' }) + const result = await dryrun({ + process: 'xU9zFkq3X2ZQ6olwNVvr1vUWIjc3kXTWr7xKQD6dh10', + tags: [ + { name: 'Action', value: 'Info' } + ] + }).then(res => res.Messages[0].Tags) + assert.equal(result.find(t => t.name === 'Ticker')?.value, 'wAR') +}) diff --git a/connect/src/index.js b/connect/src/index.js index b12ae18df..e551ee8d8 100644 --- a/connect/src/index.js +++ b/connect/src/index.js @@ -1,6 +1,7 @@ import { connectWith, serializeCron } from './index.common.js' import { WalletClient } from './client/node/index.js' + const GATEWAY_URL = process.env.GATEWAY_URL || undefined const MU_URL = process.env.MU_URL || undefined const CU_URL = process.env.CU_URL || undefined @@ -8,33 +9,37 @@ const GRAPHQL_URL = process.env.GRAPHQL_URL || undefined const GRAPHQL_MAX_RETRIES = process.env.GRAPHQL_MAX_RETRIES || undefined const GRAPHQL_RETRY_BACKOFF = process.env.GRAPHQL_RETRY_BACKOFF || undefined -const HB_URL = process.env.HB_URL || undefined +const RELAY_URL = process.env.RELAY_URL || undefined +const AO_URL = process.env.AO_URL = undefined const connect = connectWith({ createDataItemSigner: WalletClient.createDataItemSigner, - createHbSigner: WalletClient.createHbSigner + createSigner: WalletClient.createSigner }) -const { result, results, message, spawn, monitor, unmonitor, dryrun, assign, createDataItemSigner } = connect({ +const createDataItemSigner = WalletClient.createDataItemSigner +const createSigner = WalletClient.createSigner + +export { createDataItemSigner, createSigner } +export { connect } +export { serializeCron } + +/** + * @deprecated top level exports will soon be removed. Use connect() + * instead. + * + * These will only operate in legacy mode + */ +const { result, results, message, spawn, monitor, unmonitor, dryrun, assign } = connect({ + MODE: 'legacy', GATEWAY_URL, MU_URL, CU_URL, + RELAY_URL, + AO_URL, GRAPHQL_URL, GRAPHQL_MAX_RETRIES, - GRAPHQL_RETRY_BACKOFF + GRAPHQL_RETRY_BACKOFF, + noLog: true }) - -const originalHb = connect.hb -connect.hb = ({ URL = HB_URL, ...rest }) => originalHb({ URL, ...rest }) - export { result, results, message, spawn, monitor, unmonitor, dryrun, assign } -export { connect } -export { serializeCron } -/** - * A function that builds a signer using a wallet jwk interface - * commonly used in node-based dApps - * - * This is provided as a convenience for consumers of the SDK - * to use, but consumers can also implement their own signer - */ -export { createDataItemSigner } diff --git a/connect/src/index.test.js b/connect/src/index.test.js new file mode 100644 index 000000000..731293c7a --- /dev/null +++ b/connect/src/index.test.js @@ -0,0 +1,52 @@ +/* eslint-disable no-unused-vars */ +import { describe, test, before, after } from 'node:test' +import * as assert from 'node:assert' +import { tmpdir } from 'node:os' +import { writeFileSync, readFileSync, unlinkSync } from 'node:fs' +import { randomBytes } from 'node:crypto' +import { join } from 'node:path' + +import Arweave from 'arweave' + +import { connect, createDataItemSigner, createSigner } from './index.js' + +describe('index - node', () => { + /** + * Generate a wallet in a temporary directory prior to running the tests + */ + let tmpWallet + before(async () => { + const arweave = Arweave.init() + + tmpWallet = join(tmpdir(), `${randomBytes(16).toString('hex')}.json`) + writeFileSync( + tmpWallet, + JSON.stringify(await arweave.wallets.generate()) + ) + }) + + after(async () => { + unlinkSync(tmpWallet) + }) + + describe('modes', () => { + test('should return apis in the specified mode', async () => { + const wallet = JSON.parse(readFileSync(tmpWallet).toString()) + + assert.equal( + connect({ MODE: 'legacy' }).MODE, + 'legacy' + ) + + assert.equal( + connect({ MODE: 'mainnet', device: 'relay@1.0', signer: createSigner(wallet) }).MODE, + 'relay' + ) + + assert.equal( + connect({ MODE: 'mainnet', device: 'process@1.0', signer: createSigner(wallet) }).MODE, + 'mainnet' + ) + }) + }) +}) diff --git a/connect/src/lib/data-item.js b/connect/src/lib/data-item.js new file mode 100644 index 000000000..f3b45d319 --- /dev/null +++ b/connect/src/lib/data-item.js @@ -0,0 +1,91 @@ +import { Buffer as BufferShim } from 'buffer/index.js' +import base64url from 'base64url' +import * as ArBundles from '@dha-team/arbundles' + +if (!globalThis.Buffer) globalThis.Buffer = BufferShim + +/** + * hack to get module resolution working on node jfc + * + * @type {ArBundles} + */ +const pkg = ArBundles.default ? ArBundles.default : ArBundles +const { createData, DataItem, SIG_CONFIG } = pkg + +/** + * Abstract @dha-team/arbundles away, + * to hedge against rolling needed functionality + * directly into aoconnect, later + * + * TODO: + * + * ANS-104 implictly requires a signature type -> + * signature meta registry. + * + * It is not ideal that this is in code, as the registry + * is effectively hardcoded and esoteric in this way. + * This precedent seems to have been set with the arbundles + * implementation + * + * Perhaps eventually each of these signature types + * would be public somehwere ie. recursively defined Data Items that each implement + * a Data-Protocol for "Data Item Signature Type", on arweave. + */ + +/** + * @typedef DataItemSignerInfo + * @property {SignatureTypes} type + * @property {string} publicKey + * + * @typedef {Object} DataItemCreateOptions + * @property {string} [target] - Optional target value. + * @property {string} [anchor] - Optional anchor value. + * @property {{ name: string; value: string }[]} [tags] - Optional array of tag objects. + * + * @param {string | Uint8Array} data - the data to be encoded into the Data Item + * @param {DataItemSignerInfo} signer - the signature type and corresponding publicKey as a Uint8Array + * @param {DataItemCreateOptions} opts - named data to be encoded into the Data Item + * @returns {Uint8Array} + */ +export function createDataItemBytes (data, signer, opts) { + /** + * Hack to match expected shape for arbundles signer + * meta in order to create the data item, while minimizing + * bleed into other parts of code + */ + const signerMeta = SIG_CONFIG[signer.type] + if (!signerMeta) throw new Error(`Metadata for signature type ${signer.type} not found`) + signerMeta.signatureType = signer.type + signerMeta.ownerLength = signerMeta.pubLength + signerMeta.signatureLength = signerMeta.sigLength + signerMeta.publicKey = signer.publicKey + + const dataItem = createData(data, signerMeta, opts) + return dataItem.getRaw() +} + +export async function getRawAndId (dataItemBytes) { + const dataItem = new DataItem(dataItemBytes) + + /** + * arbundles dataItem.id does not work in browser environments + * so we replicate it's behavior using impl that works + * on node and browser + */ + const rawSignature = dataItem.rawSignature + const rawId = await crypto.subtle.digest('SHA-256', rawSignature) + + return { + id: base64url.encode(Buffer.from(rawId)), + raw: dataItem.getRaw() + } +} + +export function getSignatureData (dataItemBytes) { + const dataItem = new DataItem(dataItemBytes) + return dataItem.getSignatureData() +} + +export function verify (dataItemBytes) { + return DataItem.verify(dataItemBytes) +} diff --git a/connect/src/lib/message/index.js b/connect/src/lib/message/index.js index 1e30bff63..1bcea7edf 100644 --- a/connect/src/lib/message/index.js +++ b/connect/src/lib/message/index.js @@ -14,7 +14,7 @@ import { uploadMessageWith } from './upload-message.js' * @property {string} [data] * @property {{ name: string, value: string }[]} [tags] * @property {string} [anchor] - * @property {Types['signer']} signer + * @property {Types['signer']} [signer] * * @callback SendMessage * @param {SendMessageArgs} args @@ -29,7 +29,7 @@ export function messageWith (env) { return ({ process, data, tags, anchor, signer }) => { return of({ id: process, data, tags, anchor, signer }) .chain(uploadMessage) - .map((ctx) => ctx.messageId) // the id of the data item + .map((ctx) => ctx.messageId) .bimap(errFrom, identity) .toPromise() } diff --git a/connect/src/lib/message/upload-message.js b/connect/src/lib/message/upload-message.js index be22d7818..b0da82668 100644 --- a/connect/src/lib/message/upload-message.js +++ b/connect/src/lib/message/upload-message.js @@ -115,7 +115,7 @@ export function uploadMessageWith (env) { .chain(buildTags) .chain(buildData) .chain(fromPromise(({ id, data, tags, anchor, signer }) => - deployMessage({ processId: id, data, tags, anchor, signer: signerSchema.implement(signer) }) + deployMessage({ processId: id, data, tags, anchor, signer: signerSchema.implement(signer || env.signer) }) )) .map(res => assoc('messageId', res.messageId, ctx)) } diff --git a/connect/src/lib/monitor/index.js b/connect/src/lib/monitor/index.js index 686d42302..c99a69107 100644 --- a/connect/src/lib/monitor/index.js +++ b/connect/src/lib/monitor/index.js @@ -13,7 +13,7 @@ import { uploadMonitorWith } from './upload-monitor.js' * @typedef SendMonitorArgs * @property {string} process * @property {string} [data] - * @property {Types['signer']} signer + * @property {Types['signer']} [signer] * * @callback SendMonitor * @param {SendMonitorArgs} args diff --git a/connect/src/lib/monitor/upload-monitor.js b/connect/src/lib/monitor/upload-monitor.js index e3b5ad33f..70effc7b0 100644 --- a/connect/src/lib/monitor/upload-monitor.js +++ b/connect/src/lib/monitor/upload-monitor.js @@ -11,7 +11,6 @@ import { deployMonitorSchema, signerSchema } from '../../dal.js' * @typedef Context3 * @property {string} id - the transaction id to be verified * @property {any} input - * @property {any} wallet * @property {Tag3[]} tags * * @typedef Env6 @@ -34,7 +33,7 @@ export function uploadMonitorWith (env) { .chain(fromPromise(({ id, signer }) => deployMonitor({ processId: id, - signer: signerSchema.implement(signer), + signer: signerSchema.implement(signer || env.signer), /** * No tags or data can be provided right now, * diff --git a/connect/src/lib/payments/index.js b/connect/src/lib/payments/index.js new file mode 100644 index 000000000..af0cf75b4 --- /dev/null +++ b/connect/src/lib/payments/index.js @@ -0,0 +1,32 @@ +/** + * getOperator - gets the operator of the node + * getNodeBalance - gets the balance of a wallet on the Node + */ +import { of, fromPromise } from 'hyper-async' + +export function getOperator (env) { + return () => of({ URL: env.URL, fetch: env.fetch }) + .chain(getAddress) + .toPromise() +} + +export function getNodeBalance (env) { + return () => of() + .chain(requestBalance(env.request)) + .toPromise() +} + +function requestBalance (request) { + return fromPromise(() => request({ + method: 'GET', + path: '/~simple-pay@1.0/balance' + }) + .then(res => res.body)) +} + +function getAddress ({ URL, fetch }) { + return fromPromise(nodeUrl => fetch(`${nodeUrl}/~meta@1.0/info/address`) + .then(res => res.text()) + .catch(e => 'N/A') + )(URL) +} diff --git a/connect/src/lib/request/dispatch.js b/connect/src/lib/request/dispatch.js new file mode 100644 index 000000000..d0a694a1a --- /dev/null +++ b/connect/src/lib/request/dispatch.js @@ -0,0 +1,93 @@ +import { fromPromise } from 'hyper-async' + +export function dispatch ({ request, spawn, message, result, dryrun, signer }) { + return function (ctx) { + if (ctx.type === 'dryrun' && ctx.dataItem) { + const inputData = { + process: ctx.dataItem.target, + anchor: ctx.dataItem.anchor, + tags: ctx.dataItem.tags, + data: ctx.datatItem?.data ?? '' + } + return fromPromise(() => + dryrun(inputData).catch((err) => { + if (err.message.includes('Insufficient funds')) { + return { error: 'insufficient-funds' } + } + throw err + }).then(res => ({ res })) + )(ctx) + } + + if (ctx.type === 'Message' && ctx.dataItem) { + return fromPromise((ctx) => + message({ + process: ctx.dataItem.target, + anchor: ctx.dataItem.anchor, + tags: ctx.dataItem.tags, + data: ctx.dataItem.data ?? '', + signer + }) + .then((id) => + result({ + process: ctx.dataItem.target, + message: id + }).then(res => ({ res })) + ) + .catch((err) => { + if (err.message.includes('Insufficient funds')) { + return { error: 'insufficient-funds' } + } + throw err + }) + )(ctx) + } + if (ctx.type === 'Process' && ctx.dataItem) { + return fromPromise((ctx) => + spawn({ + tags: ctx.dataItem.tags, + data: ctx.dataItem.data, + scheduler: ctx.dataItem.tags.find((t) => t.name === 'Scheduler') + ?.value, + module: ctx.dataItem.tags.find((t) => t.name === 'Module')?.value, + signer + }) + .then(res => ({ res })) + .catch((err) => { + if (err.message.includes('Insufficient funds')) { + return { error: 'insufficient-funds' } + } + throw err + }) + )(ctx) + } + if (ctx.map) { + return fromPromise((ctx) => { + return request(ctx.map) + .then(res => { + if (res.status === 200) { + const process = res.headers.get('process') + const slot = res.headers.get('slot') + + return request({ + type: 'Message', + path: `${process}/compute&slot+integer=${slot}/results/json`, + method: 'POST', + target: process, + 'slot+integer': slot, + accept: 'application/json' + }) + .then(res2 => { + // if (!res2.process) { + // console.log(process, slot) + // res2.headers.set('process', process) + // } + return ({ res: res2, process, slot }) + }) + } + return ({ res }) + }) + })(ctx) + } + } +} diff --git a/connect/src/lib/request/format.js b/connect/src/lib/request/format.js new file mode 100644 index 000000000..1f261259a --- /dev/null +++ b/connect/src/lib/request/format.js @@ -0,0 +1,64 @@ +import { omit, keys } from 'ramda' +/** +if mode == 'legacy' then request should create an ans-104 from fields +if mode == 'relay' then request should create a hybrid ans-104/httpsig from fields +if mode == 'process' then request should create a pure httpsig from fields + */ +export const handleFormat = (mode) => (fields) => { + if (mode === 'mainnet' && fields.device === 'relay@1.0') { + const dataItem = { + target: fields.Target ?? fields.process, + anchor: fields.Anchor ?? '', + tags: keys( + omit( + [ + 'Target', + 'Anchor', + 'Data', + 'dryrun', + 'Type', + 'Variant', + 'path', + 'method' + ], + fields + ) + ) + .map(function (key) { + return { name: key, value: fields[key] } + }, fields) + .concat([ + { name: 'Data-Protocol', value: 'ao' }, + { name: 'Type', value: fields.Type ?? 'Message' }, + { name: 'Variant', value: fields.Variant ?? 'ao.N.1' } + ]), + data: fields?.data || '' + } + + let _type = fields.Type ?? 'Message' + if (fields.dryrun) { + _type = 'dryrun' + } + return { + type: _type, + dataItem + } + } + + if (mode === 'mainnet') { + const map = fields + + if (!fields.path) { + fields.path = '/schedule' + } + + if (fields.process && !fields.path.includes(fields.process)) { + fields.path = `${fields.process}${fields.path}` + } + + return { + type: fields.Type, + map + } + } +} diff --git a/connect/src/lib/request/index.js b/connect/src/lib/request/index.js new file mode 100644 index 000000000..8ed3bf4f4 --- /dev/null +++ b/connect/src/lib/request/index.js @@ -0,0 +1,111 @@ +import { identity } from 'ramda' +import { z } from 'zod' +import { of, Rejected, fromPromise } from 'hyper-async' + +import { errFrom } from '../utils.js' +import { transformToMap } from './transform.js' +import { dispatch } from './dispatch.js' +import { handleFormat } from './format.js' + +const inputSchema = z + .object({ + path: z.string().min(1, { message: 'path is required' }), + method: z.string() + }) + .passthrough() + +/** + * @callback Request + * @param {Record} fields + * @returns {Promise} result + * + * @returns {Request} + */ +export function requestWith (env) { + /** + * TODO: split into separate modules + * wrap side effect with schema from dal + */ + const logger = env.logger + const request = env.request + const message = env.message + const result = env.result + const dryrun = env.dryrun + const spawn = env.spawn + + const signer = env.signer + const device = env.device + const method = env.method + + const mode = env.MODE + + const verifyInput = (args) => { + if (args.device === 'relay@1.0' && args.Type === 'Process') { + return of( + z + .object({ + path: z.string().min(1, { message: 'path is required' }), + method: z.string(), + Module: z.string(), + Scheduler: z.string() + }) + .passthrough() + .parse(args) + ) + } + return of(inputSchema.parse(args)) + } + + return (fields) => { + const retry = (fn, request, attempts) => + fn(request).bichain(err => { + if (err.name !== 'RedirectRequested') return Rejected(err) + if (attempts <= 0) return Rejected(err) + const newRequest = { + ...request, + device: err.device + } + return retry(fn, newRequest, attempts - 1) + }, of) + + const operation = (ctx) => verifyInput(ctx) + .map(handleFormat(mode)) + .chain(dispatch({ request, spawn, message, result, dryrun, signer })) + .map((_) => { + logger( + 'Received response from message sent to path "%s"', + fields?.path ?? '/' + ) + return _ + }) + // .map(x => (console.log(x.Messages), x)) + // .chain(getResult(request)) + .map(transformToMap(ctx.device)) + .bimap(errFrom, identity) + + return ( + retry(operation, { path: `/~${device}`, device, ...fields, method: fields.method ?? method }, 1) + .toPromise() + ) + } +} + +/** + +*/ +// eslint-disable-next-line no-unused-vars +function getResult (request) { + return (payload) => fromPromise((payload) => { + const process = payload.headers.get('process') + const slot = payload.headers.get('slot') + // return Promise.resolve({slot, process}) + // eslint-disable-next-line no-unused-vars + return request({ + path: `/${process}/compute&slot+integer=${slot}/results/json`, + method: 'POST', + target: process, + 'slot+integer': slot, + accept: 'application/json' + }) + })(payload) +} diff --git a/connect/src/lib/request/transform.js b/connect/src/lib/request/transform.js new file mode 100644 index 000000000..4d9d00508 --- /dev/null +++ b/connect/src/lib/request/transform.js @@ -0,0 +1,70 @@ +export const transformToMap = (mode) => (ctx) => { + const result = ctx.res + const map = {} + + if (mode === 'relay@1.0') { + if (typeof result === 'string') { + return result + } + + if (result.Output && result.Output.data) { + map.Output = result.Output + } + if (result.Messages) { + map.Messages = result.Messages.map((m) => { + const miniMap = {} + m.Tags.forEach((t) => { + miniMap[t.name] = t.value + }) + miniMap.Data = { + text: () => Promise.resolve(m.Data), + json: () => Promise.resolve(JSON.parse(m.Data)) + // binary: () => Promise.resolve(Buffer.from(m.Data)) + } + miniMap.Target = m.Target + miniMap.Anchor = m.Anchor + return miniMap + }) + } + return map + } else { + map.process = ctx.process + map.slot = ctx.slot + const res = result + let body = '' + res.headers.forEach((v, k) => { + map[k] = v + }) + + if (typeof res.body === 'string') { + try { + body = JSON.parse(res.body) + + if (body.Output) { + map.Output = body.Output + } + if (body.Messages) { + map.Messages = body.Messages.map((m) => { + const miniMap = {} + m.Tags.forEach((t) => { + miniMap[t.name] = t.value + }) + miniMap.Data = { + text: () => Promise.resolve(m.Data), + json: () => Promise.resolve(JSON.parse(m.Data)) + // import { Buffer as BufferShim } from 'buffer/index.js' + // if (!globalThis.Buffer) globalThis.Buffer = BufferShim + // binary: () => Promise.resolve(Buffer.from(m.Data)) + } + miniMap.Target = m.Target + miniMap.Anchor = m.Anchor + return miniMap + }) + } + } catch (e) { + map.body = { text: () => Promise.resolve(body) } + } + } + return map + } +} diff --git a/connect/src/lib/spawn/index.js b/connect/src/lib/spawn/index.js index b73685859..8f96bc032 100644 --- a/connect/src/lib/spawn/index.js +++ b/connect/src/lib/spawn/index.js @@ -13,7 +13,7 @@ import { uploadProcessWith } from './upload-process.js' * @typedef SpawnProcessArgs * @property {string} module * @property {string} scheduler - * @property {Types['signer']} signer + * @property {Types['signer']} [signer] * @property {{ name: string, value: string }[]} [tags] * @property {string | ArrayBuffer} [data] * diff --git a/connect/src/lib/spawn/upload-process.js b/connect/src/lib/spawn/upload-process.js index 3bd396348..6c79a3bfe 100644 --- a/connect/src/lib/spawn/upload-process.js +++ b/connect/src/lib/spawn/upload-process.js @@ -3,17 +3,14 @@ import { z } from 'zod' import { __, always, assoc, curry, defaultTo, ifElse, pipe, prop } from 'ramda' import { proto } from '@permaweb/protocol-tag-utils' -import { deployProcessSchema, signerSchema } from '../../dal.js' +import { deployProcessSchema, signerSchema, tagSchema } from '../../dal.js' const aoProto = proto('ao') const removeAoProtoByName = curry(aoProto.removeAllByName) const concatAoProto = curry(aoProto.concat) const concatUnassoc = curry(aoProto.concatUnassoc) -const tagSchema = z.array(z.object({ - name: z.string(), - value: z.string() -})) +const tagsSchema = z.array(tagSchema) /** * @typedef Tag @@ -45,7 +42,7 @@ function buildTagsWith () { { name: 'Module', value: ctx.module }, { name: 'Scheduler', value: ctx.scheduler } ])) - .map(tagSchema.parse) + .map(tagsSchema.parse) .map(assoc('tags', __, ctx)) } } @@ -109,7 +106,7 @@ export function uploadProcessWith (env) { .chain(buildTags) .chain(buildData) .chain(fromPromise(({ data, tags, signer }) => - deployProcess({ data, tags, signer: signerSchema.implement(signer) }) + deployProcess({ data, tags, signer: signerSchema.implement(signer || env.signer) }) )) .map(res => assoc('processId', res.processId, ctx)) } diff --git a/connect/src/lib/spawn/verify-inputs.js b/connect/src/lib/spawn/verify-inputs.js index 86597e614..e39677dcc 100644 --- a/connect/src/lib/spawn/verify-inputs.js +++ b/connect/src/lib/spawn/verify-inputs.js @@ -1,3 +1,4 @@ +/* eslint-disable no-unused-vars */ import { Rejected, Resolved, fromPromise, of } from 'hyper-async' import { isNotNil, prop } from 'ramda' @@ -98,8 +99,8 @@ export function verifyInputsWith (env) { return (ctx) => { return of(ctx) .chain(ctx => verifyModule(ctx.module).map(() => ctx)) - .chain(ctx => verifyScheduler(ctx.scheduler)).map(() => ctx) - .chain(ctx => verifySigner(ctx.signer).map(() => ctx)) + // .chain(ctx => verifyScheduler(ctx.scheduler)).map(() => ctx) + // .chain(ctx => verifySigner(ctx.signer).map(() => ctx)) .bimap( logger.tap('Error when verify input: %s'), logger.tap('Successfully verified inputs') diff --git a/connect/src/lib/unmonitor/index.js b/connect/src/lib/unmonitor/index.js index 912a1266c..ee275106d 100644 --- a/connect/src/lib/unmonitor/index.js +++ b/connect/src/lib/unmonitor/index.js @@ -13,7 +13,7 @@ import { uploadUnmonitorWith } from './upload-unmonitor.js' * @typedef SendMonitorArgs * @property {string} process * @property {string} [data] - * @property {Types['signer']} signer + * @property {Types['signer']} [signer] * * @callback SendMonitor * @param {SendMonitorArgs} args diff --git a/connect/src/lib/unmonitor/upload-unmonitor.js b/connect/src/lib/unmonitor/upload-unmonitor.js index a2c592c1e..23f7f2b73 100644 --- a/connect/src/lib/unmonitor/upload-unmonitor.js +++ b/connect/src/lib/unmonitor/upload-unmonitor.js @@ -34,7 +34,7 @@ export function uploadUnmonitorWith (env) { .chain(fromPromise(({ id, signer }) => deployUnmonitor({ processId: id, - signer: signerSchema.implement(signer), + signer: signerSchema.implement(signer || env.signer), /** * No tags or data can be provided right now, * diff --git a/connect/test/e2e/m0/address.test.js b/connect/test/e2e/m0/address.test.js new file mode 100644 index 000000000..d3e0d009e --- /dev/null +++ b/connect/test/e2e/m0/address.test.js @@ -0,0 +1,21 @@ +import { test } from 'node:test' +import * as assert from 'node:assert' +import { connect, createSigner } from '../../../src/index.js' +// this is a test wallet do not use for anything other than testing +const WALLET = { + kty: 'RSA', + n: 'x5wjDziDSmSCvrWTiWk4avD-2DmolJHV6FOqwX8Dv4qvAJUDLq8kGg4r5NGj6yWS_f1N58FyhuAKcNgwBBj5Api5Kx5siL3vvcGywUaIID4wSn0BBjiTOf-IyEp0Ch4esGcf5fxNTzhX5X_2V8uPeXCGI-fbm877swOtgDSPc-ICAbML_CI0lOE60IP-KuWG7N0nsfibfDKs5fDv-JJTl21Dzxdryq44O873Kuyg9uOYIBW454UyBAYNMlSoMO753oLJvQvKi5n5X3ldnexD9bAwSzrjx1uhmtEr98ffxuplwsu0RkBOfolZS7GrD1mA_9zWKba-rreTus69gvbjSnHcQOkn6w11mGlL0_v95CN4kmthxlhuxPQS2R1pdkNYiX9IuULZcet-CO9Cnm6YDsNyvc84ly-iniUUR9Op2DWLzMuwNz9V-qJioKmGWmVE8V3tcu8RheAXnc2EoNwp3nhwV9a0GTcmrH7Dbpk-i60x75raxAYi5w_NAp44jddxueyxthW-3uyQmVtq_t-j1lMmrInTtRoIXi84bfZYzsxDqzx4E56xB2I7wQcOg2JSzLNVmzo6x2gVUSb9K2yCoIgM55eTdSS7sFG_fT60ftvR-RJR4fOpMHbMA8-aOcs6CLgKxFaVrVEWcSKpodKl_wjXXYJnPZE-xrXRyVwtx-c', + e: 'AQAB', + d: 'QMVnkv7Ri4hF7MBa2ZHtHraI230KOPuBn9vWYudmPfcwhk3UNIfcfR_wGlX0jM8qfYR-jR92nFGC0c809X9s_ey02UsMCOspKjf6W0EZ1uyXGvSpKm2dSIqkxely8f2IE9HCxgwywewUiYIWW7LQIaXjwS7xUgO-JP6ihCGKEx5ZbFe_IdevnEfhn9vU0_Ka761rvhJf0dNXy119YPmKam6oPEDrV19lG_MyvRj_ul1r9a1WzLmp2yhji9twoEBLkGp8L_3tgZM7GkH1oNMT0luTsDWxx_Tx4ZzSWZrgAFn7H1SecNVZIzJKaYeDbH2soFn2nJ6SDXvAUjPXxvF6GxCBp5I40mSaQW8CppI-ZWw1dbxMRjv2IQHAxsMN_TM-vk0YmjrzOr5SVWkZ3Eib6nor8kP9g0fdsA7Wlhocop8JhjdQ7XDxm0n-VLg75Wnk0o4hnmvYq5jnJg5k6mE0mBQ15mBgC8MhPZTb9VLySl_CXsXzLdT0UuOKpDGbHiWdVjT3Yo_afZt75vzQ0ys1Y5AILlFlibRyVOLnT2YE6906U3DkQLWYTs6XGWiYYS7rDPnZc24xhTj3WtCubINBOCfbcU558klgxM2Mp-TbFvLu0WPy-neeMopCsLbf-sBjWSdGE3RNSV9IuJWV4rQ2XEvtz4xS_E5-9BGw3IW-eaE', + p: '-3KdF6fI22lJ9Kv0HPMJmB4I3KgHgSpnkx99MirabiEFq3Trd5B_6iIeaCz3IKaPMvpujRTmJatNLaw2KNhtETaHQqJhw-_X82CJaYVJSFrmjnltD7dUIJW3LLokq4F7YiObd_oGWb75YKLx8WobX7E4BJd-8Bbcz2LsReYBLim6KKzzQPIw-uXfMcF_pnWA7W7S5JSBKtp458Mo6MYITlv4iAhU5cgXvZiURa57I7woTuSgl9Pmzn_ZR83tbmXK6EJA5J2nzavUgUTOaBmCkNGWztDvsb3XUwtcW48RQgDoHP6dlS4ZX7qBt5shvkDADj4Rc-X-V0pbqkWLCLKRvw', + q: 'yzlFM4JXVpBEO2s_2fmMDKGP6p-d2pv3M54-hjBHyvYOy8Zs_WrrlRm12zDXvV1YfOpHswKk3SxO9jgTAdHd_QPnXTKpO15IriuKhODPK9zRsM1pEm2tskfdv-KsE2TKULVWxBsS_sMn4pGFgTFIrE4aXezvnWBU_DBO31lRWnBHxflCJ3zl68YxNbn3Er--R4GBpyBle500-tiONDYh4eoY_TKlG3b6x4x4n1u5qz2WGr1VKhUro5GFvTj0WRlZvYGXMfIoTqu5dEApMsfRPq1qAcWU-tzy6FF1nwjypkgbr8lCcDYhNr64eRyCUUuArcVi84iIzxD3KNx2S6UD2Q', + dp: '1f1ej-ks2P2sANvuLkzvl1PIOvGNIXHTH7QmufEaX6sexiIE2oZRNhK_Se0qi9D0AXB7cPmeO-SjFAGBPhWiDIoZZq4HAdc2M1uu1eymxzsFB69zD3L315tSfnAUERlqxcSD5QEVMn-Cf6lsugWRMkkB4XaEgxMR7DTF2165Fm5QjZlSk60J3hyPbCq-1g0eIfK06-8uVigDyUmfoSoXm4hN5IciqUM2YRZe6UZqaakRrMKJoWym-op3gdJRBCkBG9R2oZlCW5imizThbFp5cYHNFElgFCX6ACSk_w6soz1eTH6r4W-QDJYPGxCdEOrOB9DuzXGomOUhSbFRd59wxQ', + dq: 'GmasSM7MDeDcHGQIfYbf3Nw4WCC4XygX60rJkKFBEmr47RwwGJQFWu9mIr2rqVwxHLlK60SSqnERKQeL4JalIjOZoQ_t0FqlUosxiaWzBF3BmBh3Z_97q0eO1VjbRgG4DtggF4X058furI5_K5N9f3T-E-muD2HuaHzWIkn2OauQh3WkVIDzVbf_uJ0aLgNe8ucuMsoQpQh9U4FCqCHIMRM6f9pOfMzuM3JaUUmXS1nK4Fpsb_UkIDHNkBGQHOsgL3BgdgqhlebVRvb24zP2SRA6T1Sd0CFYJTo_75M4AsYnYqTgzrcZhYUtbf54J5uJcgnMmxrHuy7XDSXm8FjVsQ', + qi: 'HJhf2ZP_PczoOoEMAw3cN6wdrZLG9J465tDjZ4HYqL9vrzPs7fPrXWJo4-WA-p_2IDXCkMP_t6H6JFyK1xHmDmjNpP7XlTwBb_hcEgn0W3dvmZ597Ey-B38IZfn0J4Wq3s34kcq3tprB5rG08qTm4d_tG-sln8Z7Ey-bLKTWPL_kIqpTCJ0H7cGvFVRMGN2dc9nPb4MYFRXhxZS7JF4SQJyRwPuHEMsY97Ph2IpNYpxKTGR1LfqWwSwnwrfyY_Y8sgkHMSNDvZcdGmaEYxhzTXa9xFGUdEFn2IAUIdvVz0aCBqC0soyfrkF955SDbCkbD2QxhyLX1DBVBcw_HEUCRA' +} + +test('get the operator node', async () => { + const { getOperator } = connect({ MODE: 'mainnet', URL: 'https://relay.ao-hb.xyz', signer: createSigner(WALLET) }) + const operator = await getOperator() + assert.equal(operator, 'yTVz5K0XU52lD9O0iHh42wgBGSXgsPcu7wEY8GqWnFY') +}) diff --git a/connect/test/e2e/m0/balance.test.js b/connect/test/e2e/m0/balance.test.js new file mode 100644 index 000000000..426b58cf3 --- /dev/null +++ b/connect/test/e2e/m0/balance.test.js @@ -0,0 +1,21 @@ +import { test } from 'node:test' +import * as assert from 'node:assert' +import { connect, createSigner } from '../../../src/index.js' +// this is a test wallet do not use for anything other than testing +const WALLET = { + kty: 'RSA', + n: 'x5wjDziDSmSCvrWTiWk4avD-2DmolJHV6FOqwX8Dv4qvAJUDLq8kGg4r5NGj6yWS_f1N58FyhuAKcNgwBBj5Api5Kx5siL3vvcGywUaIID4wSn0BBjiTOf-IyEp0Ch4esGcf5fxNTzhX5X_2V8uPeXCGI-fbm877swOtgDSPc-ICAbML_CI0lOE60IP-KuWG7N0nsfibfDKs5fDv-JJTl21Dzxdryq44O873Kuyg9uOYIBW454UyBAYNMlSoMO753oLJvQvKi5n5X3ldnexD9bAwSzrjx1uhmtEr98ffxuplwsu0RkBOfolZS7GrD1mA_9zWKba-rreTus69gvbjSnHcQOkn6w11mGlL0_v95CN4kmthxlhuxPQS2R1pdkNYiX9IuULZcet-CO9Cnm6YDsNyvc84ly-iniUUR9Op2DWLzMuwNz9V-qJioKmGWmVE8V3tcu8RheAXnc2EoNwp3nhwV9a0GTcmrH7Dbpk-i60x75raxAYi5w_NAp44jddxueyxthW-3uyQmVtq_t-j1lMmrInTtRoIXi84bfZYzsxDqzx4E56xB2I7wQcOg2JSzLNVmzo6x2gVUSb9K2yCoIgM55eTdSS7sFG_fT60ftvR-RJR4fOpMHbMA8-aOcs6CLgKxFaVrVEWcSKpodKl_wjXXYJnPZE-xrXRyVwtx-c', + e: 'AQAB', + d: 'QMVnkv7Ri4hF7MBa2ZHtHraI230KOPuBn9vWYudmPfcwhk3UNIfcfR_wGlX0jM8qfYR-jR92nFGC0c809X9s_ey02UsMCOspKjf6W0EZ1uyXGvSpKm2dSIqkxely8f2IE9HCxgwywewUiYIWW7LQIaXjwS7xUgO-JP6ihCGKEx5ZbFe_IdevnEfhn9vU0_Ka761rvhJf0dNXy119YPmKam6oPEDrV19lG_MyvRj_ul1r9a1WzLmp2yhji9twoEBLkGp8L_3tgZM7GkH1oNMT0luTsDWxx_Tx4ZzSWZrgAFn7H1SecNVZIzJKaYeDbH2soFn2nJ6SDXvAUjPXxvF6GxCBp5I40mSaQW8CppI-ZWw1dbxMRjv2IQHAxsMN_TM-vk0YmjrzOr5SVWkZ3Eib6nor8kP9g0fdsA7Wlhocop8JhjdQ7XDxm0n-VLg75Wnk0o4hnmvYq5jnJg5k6mE0mBQ15mBgC8MhPZTb9VLySl_CXsXzLdT0UuOKpDGbHiWdVjT3Yo_afZt75vzQ0ys1Y5AILlFlibRyVOLnT2YE6906U3DkQLWYTs6XGWiYYS7rDPnZc24xhTj3WtCubINBOCfbcU558klgxM2Mp-TbFvLu0WPy-neeMopCsLbf-sBjWSdGE3RNSV9IuJWV4rQ2XEvtz4xS_E5-9BGw3IW-eaE', + p: '-3KdF6fI22lJ9Kv0HPMJmB4I3KgHgSpnkx99MirabiEFq3Trd5B_6iIeaCz3IKaPMvpujRTmJatNLaw2KNhtETaHQqJhw-_X82CJaYVJSFrmjnltD7dUIJW3LLokq4F7YiObd_oGWb75YKLx8WobX7E4BJd-8Bbcz2LsReYBLim6KKzzQPIw-uXfMcF_pnWA7W7S5JSBKtp458Mo6MYITlv4iAhU5cgXvZiURa57I7woTuSgl9Pmzn_ZR83tbmXK6EJA5J2nzavUgUTOaBmCkNGWztDvsb3XUwtcW48RQgDoHP6dlS4ZX7qBt5shvkDADj4Rc-X-V0pbqkWLCLKRvw', + q: 'yzlFM4JXVpBEO2s_2fmMDKGP6p-d2pv3M54-hjBHyvYOy8Zs_WrrlRm12zDXvV1YfOpHswKk3SxO9jgTAdHd_QPnXTKpO15IriuKhODPK9zRsM1pEm2tskfdv-KsE2TKULVWxBsS_sMn4pGFgTFIrE4aXezvnWBU_DBO31lRWnBHxflCJ3zl68YxNbn3Er--R4GBpyBle500-tiONDYh4eoY_TKlG3b6x4x4n1u5qz2WGr1VKhUro5GFvTj0WRlZvYGXMfIoTqu5dEApMsfRPq1qAcWU-tzy6FF1nwjypkgbr8lCcDYhNr64eRyCUUuArcVi84iIzxD3KNx2S6UD2Q', + dp: '1f1ej-ks2P2sANvuLkzvl1PIOvGNIXHTH7QmufEaX6sexiIE2oZRNhK_Se0qi9D0AXB7cPmeO-SjFAGBPhWiDIoZZq4HAdc2M1uu1eymxzsFB69zD3L315tSfnAUERlqxcSD5QEVMn-Cf6lsugWRMkkB4XaEgxMR7DTF2165Fm5QjZlSk60J3hyPbCq-1g0eIfK06-8uVigDyUmfoSoXm4hN5IciqUM2YRZe6UZqaakRrMKJoWym-op3gdJRBCkBG9R2oZlCW5imizThbFp5cYHNFElgFCX6ACSk_w6soz1eTH6r4W-QDJYPGxCdEOrOB9DuzXGomOUhSbFRd59wxQ', + dq: 'GmasSM7MDeDcHGQIfYbf3Nw4WCC4XygX60rJkKFBEmr47RwwGJQFWu9mIr2rqVwxHLlK60SSqnERKQeL4JalIjOZoQ_t0FqlUosxiaWzBF3BmBh3Z_97q0eO1VjbRgG4DtggF4X058furI5_K5N9f3T-E-muD2HuaHzWIkn2OauQh3WkVIDzVbf_uJ0aLgNe8ucuMsoQpQh9U4FCqCHIMRM6f9pOfMzuM3JaUUmXS1nK4Fpsb_UkIDHNkBGQHOsgL3BgdgqhlebVRvb24zP2SRA6T1Sd0CFYJTo_75M4AsYnYqTgzrcZhYUtbf54J5uJcgnMmxrHuy7XDSXm8FjVsQ', + qi: 'HJhf2ZP_PczoOoEMAw3cN6wdrZLG9J465tDjZ4HYqL9vrzPs7fPrXWJo4-WA-p_2IDXCkMP_t6H6JFyK1xHmDmjNpP7XlTwBb_hcEgn0W3dvmZ597Ey-B38IZfn0J4Wq3s34kcq3tprB5rG08qTm4d_tG-sln8Z7Ey-bLKTWPL_kIqpTCJ0H7cGvFVRMGN2dc9nPb4MYFRXhxZS7JF4SQJyRwPuHEMsY97Ph2IpNYpxKTGR1LfqWwSwnwrfyY_Y8sgkHMSNDvZcdGmaEYxhzTXa9xFGUdEFn2IAUIdvVz0aCBqC0soyfrkF955SDbCkbD2QxhyLX1DBVBcw_HEUCRA' +} + +test('get my balance for node', async () => { + const { getNodeBalance } = connect({ MODE: 'mainnet', URL: 'https://relay.ao-hb.xyz', signer: createSigner(WALLET) }) + const balance = await getNodeBalance() + assert.ok(Number(balance) > 0) +}) diff --git a/connect/test/e2e/m0/exch.test.js b/connect/test/e2e/m0/exch.test.js new file mode 100644 index 000000000..7fdabc839 --- /dev/null +++ b/connect/test/e2e/m0/exch.test.js @@ -0,0 +1,111 @@ +import { test } from 'node:test' +import * as assert from 'node:assert' +import { connect, createSigner } from '../../../src/index.js' +// this is a test wallet do not use for anything other than testing +const WALLET = { + kty: 'RSA', + n: 'x5wjDziDSmSCvrWTiWk4avD-2DmolJHV6FOqwX8Dv4qvAJUDLq8kGg4r5NGj6yWS_f1N58FyhuAKcNgwBBj5Api5Kx5siL3vvcGywUaIID4wSn0BBjiTOf-IyEp0Ch4esGcf5fxNTzhX5X_2V8uPeXCGI-fbm877swOtgDSPc-ICAbML_CI0lOE60IP-KuWG7N0nsfibfDKs5fDv-JJTl21Dzxdryq44O873Kuyg9uOYIBW454UyBAYNMlSoMO753oLJvQvKi5n5X3ldnexD9bAwSzrjx1uhmtEr98ffxuplwsu0RkBOfolZS7GrD1mA_9zWKba-rreTus69gvbjSnHcQOkn6w11mGlL0_v95CN4kmthxlhuxPQS2R1pdkNYiX9IuULZcet-CO9Cnm6YDsNyvc84ly-iniUUR9Op2DWLzMuwNz9V-qJioKmGWmVE8V3tcu8RheAXnc2EoNwp3nhwV9a0GTcmrH7Dbpk-i60x75raxAYi5w_NAp44jddxueyxthW-3uyQmVtq_t-j1lMmrInTtRoIXi84bfZYzsxDqzx4E56xB2I7wQcOg2JSzLNVmzo6x2gVUSb9K2yCoIgM55eTdSS7sFG_fT60ftvR-RJR4fOpMHbMA8-aOcs6CLgKxFaVrVEWcSKpodKl_wjXXYJnPZE-xrXRyVwtx-c', + e: 'AQAB', + d: 'QMVnkv7Ri4hF7MBa2ZHtHraI230KOPuBn9vWYudmPfcwhk3UNIfcfR_wGlX0jM8qfYR-jR92nFGC0c809X9s_ey02UsMCOspKjf6W0EZ1uyXGvSpKm2dSIqkxely8f2IE9HCxgwywewUiYIWW7LQIaXjwS7xUgO-JP6ihCGKEx5ZbFe_IdevnEfhn9vU0_Ka761rvhJf0dNXy119YPmKam6oPEDrV19lG_MyvRj_ul1r9a1WzLmp2yhji9twoEBLkGp8L_3tgZM7GkH1oNMT0luTsDWxx_Tx4ZzSWZrgAFn7H1SecNVZIzJKaYeDbH2soFn2nJ6SDXvAUjPXxvF6GxCBp5I40mSaQW8CppI-ZWw1dbxMRjv2IQHAxsMN_TM-vk0YmjrzOr5SVWkZ3Eib6nor8kP9g0fdsA7Wlhocop8JhjdQ7XDxm0n-VLg75Wnk0o4hnmvYq5jnJg5k6mE0mBQ15mBgC8MhPZTb9VLySl_CXsXzLdT0UuOKpDGbHiWdVjT3Yo_afZt75vzQ0ys1Y5AILlFlibRyVOLnT2YE6906U3DkQLWYTs6XGWiYYS7rDPnZc24xhTj3WtCubINBOCfbcU558klgxM2Mp-TbFvLu0WPy-neeMopCsLbf-sBjWSdGE3RNSV9IuJWV4rQ2XEvtz4xS_E5-9BGw3IW-eaE', + p: '-3KdF6fI22lJ9Kv0HPMJmB4I3KgHgSpnkx99MirabiEFq3Trd5B_6iIeaCz3IKaPMvpujRTmJatNLaw2KNhtETaHQqJhw-_X82CJaYVJSFrmjnltD7dUIJW3LLokq4F7YiObd_oGWb75YKLx8WobX7E4BJd-8Bbcz2LsReYBLim6KKzzQPIw-uXfMcF_pnWA7W7S5JSBKtp458Mo6MYITlv4iAhU5cgXvZiURa57I7woTuSgl9Pmzn_ZR83tbmXK6EJA5J2nzavUgUTOaBmCkNGWztDvsb3XUwtcW48RQgDoHP6dlS4ZX7qBt5shvkDADj4Rc-X-V0pbqkWLCLKRvw', + q: 'yzlFM4JXVpBEO2s_2fmMDKGP6p-d2pv3M54-hjBHyvYOy8Zs_WrrlRm12zDXvV1YfOpHswKk3SxO9jgTAdHd_QPnXTKpO15IriuKhODPK9zRsM1pEm2tskfdv-KsE2TKULVWxBsS_sMn4pGFgTFIrE4aXezvnWBU_DBO31lRWnBHxflCJ3zl68YxNbn3Er--R4GBpyBle500-tiONDYh4eoY_TKlG3b6x4x4n1u5qz2WGr1VKhUro5GFvTj0WRlZvYGXMfIoTqu5dEApMsfRPq1qAcWU-tzy6FF1nwjypkgbr8lCcDYhNr64eRyCUUuArcVi84iIzxD3KNx2S6UD2Q', + dp: '1f1ej-ks2P2sANvuLkzvl1PIOvGNIXHTH7QmufEaX6sexiIE2oZRNhK_Se0qi9D0AXB7cPmeO-SjFAGBPhWiDIoZZq4HAdc2M1uu1eymxzsFB69zD3L315tSfnAUERlqxcSD5QEVMn-Cf6lsugWRMkkB4XaEgxMR7DTF2165Fm5QjZlSk60J3hyPbCq-1g0eIfK06-8uVigDyUmfoSoXm4hN5IciqUM2YRZe6UZqaakRrMKJoWym-op3gdJRBCkBG9R2oZlCW5imizThbFp5cYHNFElgFCX6ACSk_w6soz1eTH6r4W-QDJYPGxCdEOrOB9DuzXGomOUhSbFRd59wxQ', + dq: 'GmasSM7MDeDcHGQIfYbf3Nw4WCC4XygX60rJkKFBEmr47RwwGJQFWu9mIr2rqVwxHLlK60SSqnERKQeL4JalIjOZoQ_t0FqlUosxiaWzBF3BmBh3Z_97q0eO1VjbRgG4DtggF4X058furI5_K5N9f3T-E-muD2HuaHzWIkn2OauQh3WkVIDzVbf_uJ0aLgNe8ucuMsoQpQh9U4FCqCHIMRM6f9pOfMzuM3JaUUmXS1nK4Fpsb_UkIDHNkBGQHOsgL3BgdgqhlebVRvb24zP2SRA6T1Sd0CFYJTo_75M4AsYnYqTgzrcZhYUtbf54J5uJcgnMmxrHuy7XDSXm8FjVsQ', + qi: 'HJhf2ZP_PczoOoEMAw3cN6wdrZLG9J465tDjZ4HYqL9vrzPs7fPrXWJo4-WA-p_2IDXCkMP_t6H6JFyK1xHmDmjNpP7XlTwBb_hcEgn0W3dvmZ597Ey-B38IZfn0J4Wq3s34kcq3tprB5rG08qTm4d_tG-sln8Z7Ey-bLKTWPL_kIqpTCJ0H7cGvFVRMGN2dc9nPb4MYFRXhxZS7JF4SQJyRwPuHEMsY97Ph2IpNYpxKTGR1LfqWwSwnwrfyY_Y8sgkHMSNDvZcdGmaEYxhzTXa9xFGUdEFn2IAUIdvVz0aCBqC0soyfrkF955SDbCkbD2QxhyLX1DBVBcw_HEUCRA' +} +/** + * This test should validate the same messages will work for all three ao networks: + * + * 1. legacy + * 2. m0 + * 3. m2 + */ +test.skip('m2: check balance and transfer token', async () => { + const { request, getOperator } = connect({ + MODE: 'mainnet', + device: 'process@1.0', + URL: 'http://localhost:10000', + signer: createSigner(WALLET) + }) + + const address = await getOperator() + + // create a token + // create a process on local cu + const p = await request({ + path: '/schedule', + method: 'POST', + type: 'Process', + scheduler: address, + module: 'JArYBF-D8q2OmZ4Mok00sD2Y_6SYEQ7Hjx-6VZ_jl3g', + device: 'process@1.0', + 'scheduler-device': 'scheduler@1.0', + 'execution-device': 'compute-lite@1.0', + authority: address, + 'scheduler-location': address, + data: 'print("Hello World")', + 'Data-Protocol': 'ao', + Variant: 'ao.N.1', + 'On-Boot': 'USxy_74bsS_yuQtYAqCvn9DtxmBuUZHA-4wNVuQxZHU' + + }) + console.log(p) + + const TOKEN = '8zUUzon7HO2k1E_cXZkEdcI9XGpVmBN7qjEBJ8Ec0e0' + const result = await request({ + type: 'Message', + process: TOKEN, + path: '/schedule', + method: 'POST', + Action: 'Balance', + Recipient: '8zUUzon7HO2k1E_cXZkEdcI9XGpVmBN7qjEBJ8Ec0e0', + 'Data-Protocol': 'ao', + Variant: 'ao.N.1' + }) + + assert.equal(result.Messages[0].Balance, '10000000000000000') +}) + +test.skip('m0: check balance and transfer token', async () => { + const { request } = connect({ + MODE: 'mainnet', + device: 'relay@1.0', + URL: 'https://relay.ao-hb.xyz', + signer: createSigner(WALLET) + }) + + const TOKEN = 'mADRM-jMJ3P3IOo8WelAHAKRDa7paw1kMVw9ivkky1I' + const result = await request({ + type: 'Message', + process: TOKEN, + path: '/schedule', + method: 'POST', + Action: 'Balance', + Recipient: 'mADRM-jMJ3P3IOo8WelAHAKRDa7paw1kMVw9ivkky1I', + 'Data-Protocol': 'ao', + Variant: 'ao.N.1' + }) + const balance = result.Messages[0].Balance + assert.equal(balance, '10000000000000000') + // transfer token + const status = await request({ + type: 'Message', + process: TOKEN, + path: '/schedule', + method: 'POST', + Action: 'Transfer', + Recipient: 'sSTjOLC_7YdQ6ZZ4Otj-QVq4DwZZ9GcoiED8HPLpqwA', + Quantity: '1', + 'Data-Protocol': 'ao', + Variant: 'ao.N.1' + }) + + assert.equal(status.Messages[0].Action, 'Debit-Notice') + assert.equal(status.Messages[1].Action, 'Credit-Notice') + assert.equal(status.Messages[0].Quantity, '1') + assert.equal(status.Messages[1].Quantity, '1') +}) + +test('legacy: check balance and transfer token', () => { + +}) diff --git a/connect/test/e2e/m0/get.test.js b/connect/test/e2e/m0/get.test.js new file mode 100644 index 000000000..a0b3c8ad8 --- /dev/null +++ b/connect/test/e2e/m0/get.test.js @@ -0,0 +1,87 @@ +import { test } from 'node:test' +import * as assert from 'node:assert' +import { connectWith } from '../../../src/index.common.js' +import { WalletClient } from '../../../src/client/node/index.js' +import { createSigner } from '../../../src/client/node/wallet.js' + +// this is a test wallet do not use for anything other than testing +const WALLET = { + kty: 'RSA', + n: 'x5wjDziDSmSCvrWTiWk4avD-2DmolJHV6FOqwX8Dv4qvAJUDLq8kGg4r5NGj6yWS_f1N58FyhuAKcNgwBBj5Api5Kx5siL3vvcGywUaIID4wSn0BBjiTOf-IyEp0Ch4esGcf5fxNTzhX5X_2V8uPeXCGI-fbm877swOtgDSPc-ICAbML_CI0lOE60IP-KuWG7N0nsfibfDKs5fDv-JJTl21Dzxdryq44O873Kuyg9uOYIBW454UyBAYNMlSoMO753oLJvQvKi5n5X3ldnexD9bAwSzrjx1uhmtEr98ffxuplwsu0RkBOfolZS7GrD1mA_9zWKba-rreTus69gvbjSnHcQOkn6w11mGlL0_v95CN4kmthxlhuxPQS2R1pdkNYiX9IuULZcet-CO9Cnm6YDsNyvc84ly-iniUUR9Op2DWLzMuwNz9V-qJioKmGWmVE8V3tcu8RheAXnc2EoNwp3nhwV9a0GTcmrH7Dbpk-i60x75raxAYi5w_NAp44jddxueyxthW-3uyQmVtq_t-j1lMmrInTtRoIXi84bfZYzsxDqzx4E56xB2I7wQcOg2JSzLNVmzo6x2gVUSb9K2yCoIgM55eTdSS7sFG_fT60ftvR-RJR4fOpMHbMA8-aOcs6CLgKxFaVrVEWcSKpodKl_wjXXYJnPZE-xrXRyVwtx-c', + e: 'AQAB', + d: 'QMVnkv7Ri4hF7MBa2ZHtHraI230KOPuBn9vWYudmPfcwhk3UNIfcfR_wGlX0jM8qfYR-jR92nFGC0c809X9s_ey02UsMCOspKjf6W0EZ1uyXGvSpKm2dSIqkxely8f2IE9HCxgwywewUiYIWW7LQIaXjwS7xUgO-JP6ihCGKEx5ZbFe_IdevnEfhn9vU0_Ka761rvhJf0dNXy119YPmKam6oPEDrV19lG_MyvRj_ul1r9a1WzLmp2yhji9twoEBLkGp8L_3tgZM7GkH1oNMT0luTsDWxx_Tx4ZzSWZrgAFn7H1SecNVZIzJKaYeDbH2soFn2nJ6SDXvAUjPXxvF6GxCBp5I40mSaQW8CppI-ZWw1dbxMRjv2IQHAxsMN_TM-vk0YmjrzOr5SVWkZ3Eib6nor8kP9g0fdsA7Wlhocop8JhjdQ7XDxm0n-VLg75Wnk0o4hnmvYq5jnJg5k6mE0mBQ15mBgC8MhPZTb9VLySl_CXsXzLdT0UuOKpDGbHiWdVjT3Yo_afZt75vzQ0ys1Y5AILlFlibRyVOLnT2YE6906U3DkQLWYTs6XGWiYYS7rDPnZc24xhTj3WtCubINBOCfbcU558klgxM2Mp-TbFvLu0WPy-neeMopCsLbf-sBjWSdGE3RNSV9IuJWV4rQ2XEvtz4xS_E5-9BGw3IW-eaE', + p: '-3KdF6fI22lJ9Kv0HPMJmB4I3KgHgSpnkx99MirabiEFq3Trd5B_6iIeaCz3IKaPMvpujRTmJatNLaw2KNhtETaHQqJhw-_X82CJaYVJSFrmjnltD7dUIJW3LLokq4F7YiObd_oGWb75YKLx8WobX7E4BJd-8Bbcz2LsReYBLim6KKzzQPIw-uXfMcF_pnWA7W7S5JSBKtp458Mo6MYITlv4iAhU5cgXvZiURa57I7woTuSgl9Pmzn_ZR83tbmXK6EJA5J2nzavUgUTOaBmCkNGWztDvsb3XUwtcW48RQgDoHP6dlS4ZX7qBt5shvkDADj4Rc-X-V0pbqkWLCLKRvw', + q: 'yzlFM4JXVpBEO2s_2fmMDKGP6p-d2pv3M54-hjBHyvYOy8Zs_WrrlRm12zDXvV1YfOpHswKk3SxO9jgTAdHd_QPnXTKpO15IriuKhODPK9zRsM1pEm2tskfdv-KsE2TKULVWxBsS_sMn4pGFgTFIrE4aXezvnWBU_DBO31lRWnBHxflCJ3zl68YxNbn3Er--R4GBpyBle500-tiONDYh4eoY_TKlG3b6x4x4n1u5qz2WGr1VKhUro5GFvTj0WRlZvYGXMfIoTqu5dEApMsfRPq1qAcWU-tzy6FF1nwjypkgbr8lCcDYhNr64eRyCUUuArcVi84iIzxD3KNx2S6UD2Q', + dp: '1f1ej-ks2P2sANvuLkzvl1PIOvGNIXHTH7QmufEaX6sexiIE2oZRNhK_Se0qi9D0AXB7cPmeO-SjFAGBPhWiDIoZZq4HAdc2M1uu1eymxzsFB69zD3L315tSfnAUERlqxcSD5QEVMn-Cf6lsugWRMkkB4XaEgxMR7DTF2165Fm5QjZlSk60J3hyPbCq-1g0eIfK06-8uVigDyUmfoSoXm4hN5IciqUM2YRZe6UZqaakRrMKJoWym-op3gdJRBCkBG9R2oZlCW5imizThbFp5cYHNFElgFCX6ACSk_w6soz1eTH6r4W-QDJYPGxCdEOrOB9DuzXGomOUhSbFRd59wxQ', + dq: 'GmasSM7MDeDcHGQIfYbf3Nw4WCC4XygX60rJkKFBEmr47RwwGJQFWu9mIr2rqVwxHLlK60SSqnERKQeL4JalIjOZoQ_t0FqlUosxiaWzBF3BmBh3Z_97q0eO1VjbRgG4DtggF4X058furI5_K5N9f3T-E-muD2HuaHzWIkn2OauQh3WkVIDzVbf_uJ0aLgNe8ucuMsoQpQh9U4FCqCHIMRM6f9pOfMzuM3JaUUmXS1nK4Fpsb_UkIDHNkBGQHOsgL3BgdgqhlebVRvb24zP2SRA6T1Sd0CFYJTo_75M4AsYnYqTgzrcZhYUtbf54J5uJcgnMmxrHuy7XDSXm8FjVsQ', + qi: 'HJhf2ZP_PczoOoEMAw3cN6wdrZLG9J465tDjZ4HYqL9vrzPs7fPrXWJo4-WA-p_2IDXCkMP_t6H6JFyK1xHmDmjNpP7XlTwBb_hcEgn0W3dvmZ597Ey-B38IZfn0J4Wq3s34kcq3tprB5rG08qTm4d_tG-sln8Z7Ey-bLKTWPL_kIqpTCJ0H7cGvFVRMGN2dc9nPb4MYFRXhxZS7JF4SQJyRwPuHEMsY97Ph2IpNYpxKTGR1LfqWwSwnwrfyY_Y8sgkHMSNDvZcdGmaEYxhzTXa9xFGUdEFn2IAUIdvVz0aCBqC0soyfrkF955SDbCkbD2QxhyLX1DBVBcw_HEUCRA' +} + +test('get: connectWith use mainnet device=relay@1.0 mode to dryrun on a process', async () => { + const connect = connectWith({ + createDataItemSigner: WalletClient.createSigner, + createSigner: WalletClient.createSigner + }) + + const { get } = connect({ MODE: 'mainnet', signer: createSigner(WALLET) }) + const Ticker = await get({ + Target: 'WWjq4e5Oigct0QfBPdVF6a-zDKNMRoGPAojgv5yFcLU', + Action: 'Info', + dryrun: true + }) + .then(map => map.Messages[0].Ticker) + + assert.equal(Ticker, 'PNTS') +}) + +test('get: connectWith use mainnet device=relay@1.0 mode to message on a process', async () => { + const connect = connectWith({ + createDataItemSigner: WalletClient.createSigner, + createSigner: WalletClient.createSigner + }) + + const { get } = connect({ MODE: 'mainnet', signer: createSigner(WALLET) }) + const Error = await get({ + Target: 'WWjq4e5Oigct0QfBPdVF6a-zDKNMRoGPAojgv5yFcLU', + Action: 'Transfer', + Quantity: '10000', + Recipient: 'cHencOZC-aCbPCDH2tEZ3Lhw3EM5oRw3kxgj-eEgtNc' + }).then(map => map.Messages[0].Error) + + assert.equal(Error, 'Insufficient Balance!') +}) + +test('get: connectWith use mainnet device=relay@1.0 mode to spawn a process', async () => { + const connect = connectWith({ + createDataItemSigner: WalletClient.createSigner, + createSigner: WalletClient.createSigner + }) + + const { get } = connect({ MODE: 'mainnet', signer: createSigner(WALLET) }) + const process = await get({ + Type: 'Process', + Name: 'MyNewProcess', + Module: 'JArYBF-D8q2OmZ4Mok00sD2Y_6SYEQ7Hjx-6VZ_jl3g', + Scheduler: '_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA' + }) + // .then(result => result.Output.text()) + // console.log(result.Output.text()) + // .then(map => map) + assert.equal(process.length, 43) +}) + +test('get: connectWith should give us legacy connect mode', async () => { + const connect = connectWith({ + createDataItemSigner: WalletClient.createSigner, + createSigner: WalletClient.createSigner + }) + + const { dryrun } = connect({ MODE: 'legacy', signer: createSigner(WALLET) }) + const result = await dryrun({ + process: 'xU9zFkq3X2ZQ6olwNVvr1vUWIjc3kXTWr7xKQD6dh10', + tags: [ + { name: 'Action', value: 'Info' } + ] + }).then(res => res.Messages[0].Tags) + assert.equal(result.find(t => t.name === 'Ticker')?.value, 'wAR') +}) diff --git a/connect/test/e2e/m0/nofunds.test.js b/connect/test/e2e/m0/nofunds.test.js new file mode 100644 index 000000000..c0cdc1337 --- /dev/null +++ b/connect/test/e2e/m0/nofunds.test.js @@ -0,0 +1,38 @@ +import { test } from 'node:test' +import * as assert from 'node:assert' +import { connectWith } from '../../../src/index.common.js' +import { WalletClient } from '../../../src/client/node/index.js' +import { createSigner } from '../../../src/client/node/wallet.js' + +const WALLET = { + kty: 'RSA', + n: 't1OhTE-r39pXsrgdsNRXEZyMIAIID2htI6-XmPaRcJXH6vqtbBgW0Vkzx7-bbTiteQdpJiLe6yQM4kT3QyE84L0XnHgHVB0-0QYvOZJIsOTK-7ZTKstHBu0rPNNRpI2XvzKuu2BUDuEYfpGc__1gqi_x7LSyygQuHZZse5MtBPJpHLC5UPOiTYtMxe-NTipM27ctMNFfSIEFw5JOZLKCtFqZpudu0tJ0wMxvV-uE8gXUsvzlDjLUYNM7BenA8otpChnnA-ihjJop_f47JKNbb6WzuNOG7LXHNdC7zcK2MpFTtb387Csu_rJxnZAjwmdlThKHbNkbSHKFeZNxOr_9s2D66hUDLAp0IbwKOJAmIczleRKih_QEytXsJCm3ztkOaGI6xdzjgtaA490zL6jncKeRbss809Ek-iYi9OzurRQXbiJENSMQCzx1RjZG-x4MymyFWK92JsJAx6ux15bW7YI_Fo1u6dELr0c6Id3B3LbkXw3QL3erWaLrucOo0h1f83RDwZrT9GKD9zBnLWXFPyKplyY0PcPjx5AFeFaE1jtP8RZpErqES_v3f5SAZKTx_SQHjs3HGSKkoFL7U457jfT6MSddRPAddzPcRVVJ5TS2spEOyqsD158DUNxa25k42Wg2zmYZ6inxCt6hAUsL7xUpa9A4NmCb4U1n7v7vNqk', + e: 'AQAB', + d: 'IB9vznrRX4aXQlIfpbn3Tbc0GHGyrtx57zxX4G4n-uXL44EsPRoVeXZba99HoUJCp0fkpVGL5QDqnwU9nE4pKCoKBplDk3oSZTbjtH9M7G4SoTuH5CI9XlnVbzGKBz7mPZLoah3NbNQFYjwqmfJwazG-ft8EywAjgny2ooqd41WElInk1unx4QuNFsvMY0tnL0KwEPNG8mHlPCkZQmzDGU4cyVGZKijFW3kW831pzKy34esNSckfTJ4GpBka_YTvEV2LjLRJBmWHASH4hAl5vpz22mZ6KGULtILQPZQmL8MmO1zAgjNo5ykmXlkA4xIR_rWhdDGwpaeneq-ashQtJGLQHpsy32IQq5jqQLZlyFz4U9jZpzIA1mdJImJJBWjIREgwsTv5w7eCz-Bv9kR53CklG5yZuidOIwTqJ0mGnfvUSPSQo9OdmpivZgSbCqOYel9hudkKKKTNVMmulsZ5Qb1B_PvgwY7UWZfuFMJZWT5v_MI9fKfEL1gUW1Ig9zKqW-QNvIxS_MddXJ0YK42VCb2A3DB7dEsMsVmZs_3oBdOS0uSLhtA6s9Hpp2VGZwUVOyAWLraYv2yIW8X5_emShH4AsyUE3nXTlCxx9yaTE9I0YvFQ5DQiDKkzKnrLumHB6ZXi348pN0qUK7AmbBxgDm4jkvq7kubmblGiUKtNgW8', + p: '49cU4_DkYQGLJzSFEe4hk3ITEC4SbLkX91jaUXtk-TO4IdWLgWb0smq8sKzwGEB7EbogaU10Tg4Hr2rGHIY4vNK_9GfO5yKSAWP-dJi1b5u4EzOqDW_FuRWVIRjDEQnoaXSmpKHHk6w9mSAEKNtPmADzOKZfZyzcufvoE4hUG3t07eLhDu9LC_Ib7RjSXMzkUXdUZpg_TuDOzXTBvjBi49kSteWZgP6txd8K6cyo1vlPdeOrfinfR-nKDj-KpvWgbaei7oZqeaRfQdANnoWDd9TLDdNEkSKruzBWd-b_3yVD7FzbGXT4yAT_9iffZlexzdQlwoqu5Dp0_eTZDROJ6w', + q: 'zfwhiaFXrkL9RYJM_LVhmA6xz_66wWa7GpkXiupTsopD_Ly3y6cvn8-tOfh46jBHvtqZjKbZSTuzaEI6FEIqkFqzCtY0UI-r8MbUKwNepXdZCuyOgMOKfYEq9icQdsZWpM6fcNOzTv9Ug8lKAQzy2QR0cZvupQM9SpYPI3mUDVazA_Cp7Xfeu5vmYHspNHNIEWXDmsz3GbByIH67ZY69ve4aTZmRpAxJvbnQ0N84bAWrp9jD9gUh7YzDER6iP0T3DBVTL4gdj1b_Ig1iyGglvdBpjbEKSTlVSD8EM7zxBFD4hjftWK_9dWsBcAiXnYTtV9_SM6cM16ECMfW7KmZouw', + dp: 'VnGHMoh2DbwJMMHDby8bPBb-1pe0U93SEv16L4oSoF4YEXY07M7ELmFC4AFtQrZMO0iT_X_ujaLTFaH8MlxWcIXBYJh2Lsm1_aMTquc5h3PcXiSu81Qh_3wJzbnYYMGSlVg11aav0v55jHxSN7Ilu7U4kpjE_59My5-gyLBjwN2RWqRG4WS243xjTgyp2529To9ozVgcRPAUZm1TCLbQ_kOgzrSjDgcSpU1flZ8ZvB5xIHhV25NFbMJt8AP5tijhSllc_wSupyJPJegrEqbzX2az97XdgPdXkrjfRbS2BdLct3PviPQRY8pJ1vn_z5OcGUzRWsNFYcAwHRhTpZtxKw', + dq: 'x4alDeYFQr2NC9mNrXQc1ufVAX7jdBeIwdjtXxhgfp9D__rlphWKIilOyMqQhXjC5nJlLkMoUrHLtQABtkHpbO8OYz7eafLhQFe4iPm4olsbG2PNHZlL-wo1jT6FyieRIg8rjmTiWXEmcol2b9IVInYYPTwoTBr5MdhtTZWk_c6vlO94CpiEXgJDg82JOk84zfFfKgHEkVPDGQrphxRfYyzSubw5wUhN3cOyK8EtrCVKA0oLd7G9DZe-FEUyMEsjasb1TyNwTrqcZGXG_A02EclhlWiaaBdrLpIsBdnNNLsiIRDnjXLb9vLx743DH0f1h5s8x9_WpsBFmhNHe2i7pQ', + qi: 'G3N8v42TYSw-q3eKTJ4IhcY2IOEGBRpV7CgevtqGlfnCjPUezlwtuqVh3tFIVMryK7HMLsqWdvzRNrOhq3Kg2JrQR1Q-gbDS0brCcGfOPCDKRM1SwC8hzeqLBzRItxj7IoPR3XKBGtUWoR_AKfM0t8ewOzLm5w1OYMAOtfu8inzNU7FHoIK-yc1oB4ox0gHId7-lg0wK3mhCSQxEGO0K6W5f99joHwQ2IQ7_nx_2-s0PEFQ0759LXUkQT3RzWQk__MZ2G2y9dLF7AHJSO4m4XF3uPuH5fKJjjcyKbn8HdsdgIseHEaTr7g00JR1psfOHQqMMR5HPEg73tXgmKy6t7w' +} + +test('get: no funds for message', async () => { + const connect = connectWith({ + createDataItemSigner: WalletClient.createSigner, + createSigner: WalletClient.createSigner + }) + + const { get } = connect({ MODE: 'mainnet', signer: createSigner(WALLET) }) + try { + const Ticker = await get({ + Target: 'WWjq4e5Oigct0QfBPdVF6a-zDKNMRoGPAojgv5yFcLU', + Action: 'Info', + dryrun: true + }).then((map) => map.Messages[0].Ticker.text()) + + assert.equal(Ticker, 'PNTS') + } catch (e) { + assert.equal(e.name, 'InsufficientFunds') + assert.equal(e.price, '3') + } +}) diff --git a/connect/test/e2e/m0/post.test.js b/connect/test/e2e/m0/post.test.js new file mode 100644 index 000000000..cc9d5f62e --- /dev/null +++ b/connect/test/e2e/m0/post.test.js @@ -0,0 +1,70 @@ +import { test } from 'node:test' +import * as assert from 'node:assert' +import { connectWith } from '../../../src/index.common.js' +import { WalletClient } from '../../../src/client/node/index.js' +import { createSigner } from '../../../src/client/node/wallet.js' + +// this is a test wallet do not use for anything other than testing +const WALLET = { + kty: 'RSA', + n: 'x5wjDziDSmSCvrWTiWk4avD-2DmolJHV6FOqwX8Dv4qvAJUDLq8kGg4r5NGj6yWS_f1N58FyhuAKcNgwBBj5Api5Kx5siL3vvcGywUaIID4wSn0BBjiTOf-IyEp0Ch4esGcf5fxNTzhX5X_2V8uPeXCGI-fbm877swOtgDSPc-ICAbML_CI0lOE60IP-KuWG7N0nsfibfDKs5fDv-JJTl21Dzxdryq44O873Kuyg9uOYIBW454UyBAYNMlSoMO753oLJvQvKi5n5X3ldnexD9bAwSzrjx1uhmtEr98ffxuplwsu0RkBOfolZS7GrD1mA_9zWKba-rreTus69gvbjSnHcQOkn6w11mGlL0_v95CN4kmthxlhuxPQS2R1pdkNYiX9IuULZcet-CO9Cnm6YDsNyvc84ly-iniUUR9Op2DWLzMuwNz9V-qJioKmGWmVE8V3tcu8RheAXnc2EoNwp3nhwV9a0GTcmrH7Dbpk-i60x75raxAYi5w_NAp44jddxueyxthW-3uyQmVtq_t-j1lMmrInTtRoIXi84bfZYzsxDqzx4E56xB2I7wQcOg2JSzLNVmzo6x2gVUSb9K2yCoIgM55eTdSS7sFG_fT60ftvR-RJR4fOpMHbMA8-aOcs6CLgKxFaVrVEWcSKpodKl_wjXXYJnPZE-xrXRyVwtx-c', + e: 'AQAB', + d: 'QMVnkv7Ri4hF7MBa2ZHtHraI230KOPuBn9vWYudmPfcwhk3UNIfcfR_wGlX0jM8qfYR-jR92nFGC0c809X9s_ey02UsMCOspKjf6W0EZ1uyXGvSpKm2dSIqkxely8f2IE9HCxgwywewUiYIWW7LQIaXjwS7xUgO-JP6ihCGKEx5ZbFe_IdevnEfhn9vU0_Ka761rvhJf0dNXy119YPmKam6oPEDrV19lG_MyvRj_ul1r9a1WzLmp2yhji9twoEBLkGp8L_3tgZM7GkH1oNMT0luTsDWxx_Tx4ZzSWZrgAFn7H1SecNVZIzJKaYeDbH2soFn2nJ6SDXvAUjPXxvF6GxCBp5I40mSaQW8CppI-ZWw1dbxMRjv2IQHAxsMN_TM-vk0YmjrzOr5SVWkZ3Eib6nor8kP9g0fdsA7Wlhocop8JhjdQ7XDxm0n-VLg75Wnk0o4hnmvYq5jnJg5k6mE0mBQ15mBgC8MhPZTb9VLySl_CXsXzLdT0UuOKpDGbHiWdVjT3Yo_afZt75vzQ0ys1Y5AILlFlibRyVOLnT2YE6906U3DkQLWYTs6XGWiYYS7rDPnZc24xhTj3WtCubINBOCfbcU558klgxM2Mp-TbFvLu0WPy-neeMopCsLbf-sBjWSdGE3RNSV9IuJWV4rQ2XEvtz4xS_E5-9BGw3IW-eaE', + p: '-3KdF6fI22lJ9Kv0HPMJmB4I3KgHgSpnkx99MirabiEFq3Trd5B_6iIeaCz3IKaPMvpujRTmJatNLaw2KNhtETaHQqJhw-_X82CJaYVJSFrmjnltD7dUIJW3LLokq4F7YiObd_oGWb75YKLx8WobX7E4BJd-8Bbcz2LsReYBLim6KKzzQPIw-uXfMcF_pnWA7W7S5JSBKtp458Mo6MYITlv4iAhU5cgXvZiURa57I7woTuSgl9Pmzn_ZR83tbmXK6EJA5J2nzavUgUTOaBmCkNGWztDvsb3XUwtcW48RQgDoHP6dlS4ZX7qBt5shvkDADj4Rc-X-V0pbqkWLCLKRvw', + q: 'yzlFM4JXVpBEO2s_2fmMDKGP6p-d2pv3M54-hjBHyvYOy8Zs_WrrlRm12zDXvV1YfOpHswKk3SxO9jgTAdHd_QPnXTKpO15IriuKhODPK9zRsM1pEm2tskfdv-KsE2TKULVWxBsS_sMn4pGFgTFIrE4aXezvnWBU_DBO31lRWnBHxflCJ3zl68YxNbn3Er--R4GBpyBle500-tiONDYh4eoY_TKlG3b6x4x4n1u5qz2WGr1VKhUro5GFvTj0WRlZvYGXMfIoTqu5dEApMsfRPq1qAcWU-tzy6FF1nwjypkgbr8lCcDYhNr64eRyCUUuArcVi84iIzxD3KNx2S6UD2Q', + dp: '1f1ej-ks2P2sANvuLkzvl1PIOvGNIXHTH7QmufEaX6sexiIE2oZRNhK_Se0qi9D0AXB7cPmeO-SjFAGBPhWiDIoZZq4HAdc2M1uu1eymxzsFB69zD3L315tSfnAUERlqxcSD5QEVMn-Cf6lsugWRMkkB4XaEgxMR7DTF2165Fm5QjZlSk60J3hyPbCq-1g0eIfK06-8uVigDyUmfoSoXm4hN5IciqUM2YRZe6UZqaakRrMKJoWym-op3gdJRBCkBG9R2oZlCW5imizThbFp5cYHNFElgFCX6ACSk_w6soz1eTH6r4W-QDJYPGxCdEOrOB9DuzXGomOUhSbFRd59wxQ', + dq: 'GmasSM7MDeDcHGQIfYbf3Nw4WCC4XygX60rJkKFBEmr47RwwGJQFWu9mIr2rqVwxHLlK60SSqnERKQeL4JalIjOZoQ_t0FqlUosxiaWzBF3BmBh3Z_97q0eO1VjbRgG4DtggF4X058furI5_K5N9f3T-E-muD2HuaHzWIkn2OauQh3WkVIDzVbf_uJ0aLgNe8ucuMsoQpQh9U4FCqCHIMRM6f9pOfMzuM3JaUUmXS1nK4Fpsb_UkIDHNkBGQHOsgL3BgdgqhlebVRvb24zP2SRA6T1Sd0CFYJTo_75M4AsYnYqTgzrcZhYUtbf54J5uJcgnMmxrHuy7XDSXm8FjVsQ', + qi: 'HJhf2ZP_PczoOoEMAw3cN6wdrZLG9J465tDjZ4HYqL9vrzPs7fPrXWJo4-WA-p_2IDXCkMP_t6H6JFyK1xHmDmjNpP7XlTwBb_hcEgn0W3dvmZ597Ey-B38IZfn0J4Wq3s34kcq3tprB5rG08qTm4d_tG-sln8Z7Ey-bLKTWPL_kIqpTCJ0H7cGvFVRMGN2dc9nPb4MYFRXhxZS7JF4SQJyRwPuHEMsY97Ph2IpNYpxKTGR1LfqWwSwnwrfyY_Y8sgkHMSNDvZcdGmaEYxhzTXa9xFGUdEFn2IAUIdvVz0aCBqC0soyfrkF955SDbCkbD2QxhyLX1DBVBcw_HEUCRA' +} + +test('post: connectWith use mainnet device=relay@1.0 mode to dryrun on a process', async () => { + const connect = connectWith({ + createDataItemSigner: WalletClient.createSigner, + createSigner: WalletClient.createSigner + }) + + const { post } = connect({ MODE: 'mainnet', signer: createSigner(WALLET) }) + const Ticker = await post({ + Target: 'WWjq4e5Oigct0QfBPdVF6a-zDKNMRoGPAojgv5yFcLU', + Action: 'Info', + dryrun: true + }).then(map => map.Messages[0].Ticker) + + assert.equal(Ticker, 'PNTS') +}) + +test('post: connectWith use mainnet device=relay@1.0 mode to message on a process', async () => { + const connect = connectWith({ + createDataItemSigner: WalletClient.createSigner, + createSigner: WalletClient.createSigner + }) + + const { post } = connect({ MODE: 'mainnet', signer: createSigner(WALLET) }) + const Error = await post({ + Target: 'WWjq4e5Oigct0QfBPdVF6a-zDKNMRoGPAojgv5yFcLU', + Action: 'Transfer', + Quantity: '10000', + Recipient: 'cHencOZC-aCbPCDH2tEZ3Lhw3EM5oRw3kxgj-eEgtNc' + }).then(map => map.Messages[0].Error) + + assert.equal(Error, 'Insufficient Balance!') +}) + +test('post: connectWith use mainnet device=relay@1.0 mode to spawn a process', async () => { + const connect = connectWith({ + createDataItemSigner: WalletClient.createSigner, + createSigner: WalletClient.createSigner + }) + + const { post } = connect({ MODE: 'mainnet', signer: createSigner(WALLET) }) + const process = await post({ + Type: 'Process', + Name: 'MyNewProcess', + Module: 'JArYBF-D8q2OmZ4Mok00sD2Y_6SYEQ7Hjx-6VZ_jl3g', + Scheduler: '_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA' + }) + // .then(result => result.Output.text()) + // console.log(result.Output.text()) + // .then(map => map) + assert.equal(process.length, 43) +}) diff --git a/connect/test/e2e/m0/redirect.test.js b/connect/test/e2e/m0/redirect.test.js new file mode 100644 index 000000000..e602bb297 --- /dev/null +++ b/connect/test/e2e/m0/redirect.test.js @@ -0,0 +1,55 @@ +import { test } from 'node:test' +import * as assert from 'node:assert' +import { connectWith } from '../../../src/index.common.js' +import { WalletClient } from '../../../src/client/node/index.js' +import http from 'http' + +const WALLET = { + kty: 'RSA', + n: 't1OhTE-r39pXsrgdsNRXEZyMIAIID2htI6-XmPaRcJXH6vqtbBgW0Vkzx7-bbTiteQdpJiLe6yQM4kT3QyE84L0XnHgHVB0-0QYvOZJIsOTK-7ZTKstHBu0rPNNRpI2XvzKuu2BUDuEYfpGc__1gqi_x7LSyygQuHZZse5MtBPJpHLC5UPOiTYtMxe-NTipM27ctMNFfSIEFw5JOZLKCtFqZpudu0tJ0wMxvV-uE8gXUsvzlDjLUYNM7BenA8otpChnnA-ihjJop_f47JKNbb6WzuNOG7LXHNdC7zcK2MpFTtb387Csu_rJxnZAjwmdlThKHbNkbSHKFeZNxOr_9s2D66hUDLAp0IbwKOJAmIczleRKih_QEytXsJCm3ztkOaGI6xdzjgtaA490zL6jncKeRbss809Ek-iYi9OzurRQXbiJENSMQCzx1RjZG-x4MymyFWK92JsJAx6ux15bW7YI_Fo1u6dELr0c6Id3B3LbkXw3QL3erWaLrucOo0h1f83RDwZrT9GKD9zBnLWXFPyKplyY0PcPjx5AFeFaE1jtP8RZpErqES_v3f5SAZKTx_SQHjs3HGSKkoFL7U457jfT6MSddRPAddzPcRVVJ5TS2spEOyqsD158DUNxa25k42Wg2zmYZ6inxCt6hAUsL7xUpa9A4NmCb4U1n7v7vNqk', + e: 'AQAB', + d: 'IB9vznrRX4aXQlIfpbn3Tbc0GHGyrtx57zxX4G4n-uXL44EsPRoVeXZba99HoUJCp0fkpVGL5QDqnwU9nE4pKCoKBplDk3oSZTbjtH9M7G4SoTuH5CI9XlnVbzGKBz7mPZLoah3NbNQFYjwqmfJwazG-ft8EywAjgny2ooqd41WElInk1unx4QuNFsvMY0tnL0KwEPNG8mHlPCkZQmzDGU4cyVGZKijFW3kW831pzKy34esNSckfTJ4GpBka_YTvEV2LjLRJBmWHASH4hAl5vpz22mZ6KGULtILQPZQmL8MmO1zAgjNo5ykmXlkA4xIR_rWhdDGwpaeneq-ashQtJGLQHpsy32IQq5jqQLZlyFz4U9jZpzIA1mdJImJJBWjIREgwsTv5w7eCz-Bv9kR53CklG5yZuidOIwTqJ0mGnfvUSPSQo9OdmpivZgSbCqOYel9hudkKKKTNVMmulsZ5Qb1B_PvgwY7UWZfuFMJZWT5v_MI9fKfEL1gUW1Ig9zKqW-QNvIxS_MddXJ0YK42VCb2A3DB7dEsMsVmZs_3oBdOS0uSLhtA6s9Hpp2VGZwUVOyAWLraYv2yIW8X5_emShH4AsyUE3nXTlCxx9yaTE9I0YvFQ5DQiDKkzKnrLumHB6ZXi348pN0qUK7AmbBxgDm4jkvq7kubmblGiUKtNgW8', + p: '49cU4_DkYQGLJzSFEe4hk3ITEC4SbLkX91jaUXtk-TO4IdWLgWb0smq8sKzwGEB7EbogaU10Tg4Hr2rGHIY4vNK_9GfO5yKSAWP-dJi1b5u4EzOqDW_FuRWVIRjDEQnoaXSmpKHHk6w9mSAEKNtPmADzOKZfZyzcufvoE4hUG3t07eLhDu9LC_Ib7RjSXMzkUXdUZpg_TuDOzXTBvjBi49kSteWZgP6txd8K6cyo1vlPdeOrfinfR-nKDj-KpvWgbaei7oZqeaRfQdANnoWDd9TLDdNEkSKruzBWd-b_3yVD7FzbGXT4yAT_9iffZlexzdQlwoqu5Dp0_eTZDROJ6w', + q: 'zfwhiaFXrkL9RYJM_LVhmA6xz_66wWa7GpkXiupTsopD_Ly3y6cvn8-tOfh46jBHvtqZjKbZSTuzaEI6FEIqkFqzCtY0UI-r8MbUKwNepXdZCuyOgMOKfYEq9icQdsZWpM6fcNOzTv9Ug8lKAQzy2QR0cZvupQM9SpYPI3mUDVazA_Cp7Xfeu5vmYHspNHNIEWXDmsz3GbByIH67ZY69ve4aTZmRpAxJvbnQ0N84bAWrp9jD9gUh7YzDER6iP0T3DBVTL4gdj1b_Ig1iyGglvdBpjbEKSTlVSD8EM7zxBFD4hjftWK_9dWsBcAiXnYTtV9_SM6cM16ECMfW7KmZouw', + dp: 'VnGHMoh2DbwJMMHDby8bPBb-1pe0U93SEv16L4oSoF4YEXY07M7ELmFC4AFtQrZMO0iT_X_ujaLTFaH8MlxWcIXBYJh2Lsm1_aMTquc5h3PcXiSu81Qh_3wJzbnYYMGSlVg11aav0v55jHxSN7Ilu7U4kpjE_59My5-gyLBjwN2RWqRG4WS243xjTgyp2529To9ozVgcRPAUZm1TCLbQ_kOgzrSjDgcSpU1flZ8ZvB5xIHhV25NFbMJt8AP5tijhSllc_wSupyJPJegrEqbzX2az97XdgPdXkrjfRbS2BdLct3PviPQRY8pJ1vn_z5OcGUzRWsNFYcAwHRhTpZtxKw', + dq: 'x4alDeYFQr2NC9mNrXQc1ufVAX7jdBeIwdjtXxhgfp9D__rlphWKIilOyMqQhXjC5nJlLkMoUrHLtQABtkHpbO8OYz7eafLhQFe4iPm4olsbG2PNHZlL-wo1jT6FyieRIg8rjmTiWXEmcol2b9IVInYYPTwoTBr5MdhtTZWk_c6vlO94CpiEXgJDg82JOk84zfFfKgHEkVPDGQrphxRfYyzSubw5wUhN3cOyK8EtrCVKA0oLd7G9DZe-FEUyMEsjasb1TyNwTrqcZGXG_A02EclhlWiaaBdrLpIsBdnNNLsiIRDnjXLb9vLx743DH0f1h5s8x9_WpsBFmhNHe2i7pQ', + qi: 'G3N8v42TYSw-q3eKTJ4IhcY2IOEGBRpV7CgevtqGlfnCjPUezlwtuqVh3tFIVMryK7HMLsqWdvzRNrOhq3Kg2JrQR1Q-gbDS0brCcGfOPCDKRM1SwC8hzeqLBzRItxj7IoPR3XKBGtUWoR_AKfM0t8ewOzLm5w1OYMAOtfu8inzNU7FHoIK-yc1oB4ox0gHId7-lg0wK3mhCSQxEGO0K6W5f99joHwQ2IQ7_nx_2-s0PEFQ0759LXUkQT3RzWQk__MZ2G2y9dLF7AHJSO4m4XF3uPuH5fKJjjcyKbn8HdsdgIseHEaTr7g00JR1psfOHQqMMR5HPEg73tXgmKy6t7w' +} + +test.skip('get: redirect request', async () => { + const server = http.createServer((req, res) => { + res.writeHead(422, { + 'Content-Encoding': 'http', + 'Accept-Device': 'process@1.0', + Location: 'https://10000-permaweb-hbinfra-erkeu448sa9.ws-us117.gitpod.io' + }) + res.end() + }).listen(14000) + + const connect = connectWith({ + createDataItemSigner: WalletClient.createDataItemSigner, + createHbSigner: WalletClient.createHbSigner + }) + + const { get } = connect({ MODE: 'mainnet', URL: 'http://localhost:14000', wallet: WALLET }) + try { + const Ticker = await get({ + Target: 'WWjq4e5Oigct0QfBPdVF6a-zDKNMRoGPAojgv5yFcLU', + Action: 'Info', + Type: 'Message' + }).then((map) => map.Messages[0].Ticker.text()) + + assert.equal(Ticker, 'PNTS') + } catch (e) { + console.log(e) + console.log('handling error') + assert.equal(e.name, 'RedirectRequested') + assert.equal(e.device, 'process@1.0') + assert.equal(e.contentEncoding, 'html') + assert.equal(e.location, 'https://10000-permaweb-hbinfra-erkeu448sa9.ws-us117.gitpod.io') + console.log('tests passed!') + process.exit(0) + return 'ok' + } + server.close() +}) diff --git a/connect/test/e2e/m2/request.test.js b/connect/test/e2e/m2/request.test.js new file mode 100644 index 000000000..20b0d89b8 --- /dev/null +++ b/connect/test/e2e/m2/request.test.js @@ -0,0 +1,83 @@ +import { test } from 'node:test' +import * as assert from 'node:assert' +import { connectWith } from '../../../src/index.common.js' +import { WalletClient } from '../../../src/client/node/index.js' +import { createSigner } from '../../../src/client/node/wallet.js' + +import { randomBytes } from 'node:crypto' +// import { tap } from 'ramda' + +// this is a test wallet do not use for anything other than testing +const WALLET = { + kty: 'RSA', + n: 'x5wjDziDSmSCvrWTiWk4avD-2DmolJHV6FOqwX8Dv4qvAJUDLq8kGg4r5NGj6yWS_f1N58FyhuAKcNgwBBj5Api5Kx5siL3vvcGywUaIID4wSn0BBjiTOf-IyEp0Ch4esGcf5fxNTzhX5X_2V8uPeXCGI-fbm877swOtgDSPc-ICAbML_CI0lOE60IP-KuWG7N0nsfibfDKs5fDv-JJTl21Dzxdryq44O873Kuyg9uOYIBW454UyBAYNMlSoMO753oLJvQvKi5n5X3ldnexD9bAwSzrjx1uhmtEr98ffxuplwsu0RkBOfolZS7GrD1mA_9zWKba-rreTus69gvbjSnHcQOkn6w11mGlL0_v95CN4kmthxlhuxPQS2R1pdkNYiX9IuULZcet-CO9Cnm6YDsNyvc84ly-iniUUR9Op2DWLzMuwNz9V-qJioKmGWmVE8V3tcu8RheAXnc2EoNwp3nhwV9a0GTcmrH7Dbpk-i60x75raxAYi5w_NAp44jddxueyxthW-3uyQmVtq_t-j1lMmrInTtRoIXi84bfZYzsxDqzx4E56xB2I7wQcOg2JSzLNVmzo6x2gVUSb9K2yCoIgM55eTdSS7sFG_fT60ftvR-RJR4fOpMHbMA8-aOcs6CLgKxFaVrVEWcSKpodKl_wjXXYJnPZE-xrXRyVwtx-c', + e: 'AQAB', + d: 'QMVnkv7Ri4hF7MBa2ZHtHraI230KOPuBn9vWYudmPfcwhk3UNIfcfR_wGlX0jM8qfYR-jR92nFGC0c809X9s_ey02UsMCOspKjf6W0EZ1uyXGvSpKm2dSIqkxely8f2IE9HCxgwywewUiYIWW7LQIaXjwS7xUgO-JP6ihCGKEx5ZbFe_IdevnEfhn9vU0_Ka761rvhJf0dNXy119YPmKam6oPEDrV19lG_MyvRj_ul1r9a1WzLmp2yhji9twoEBLkGp8L_3tgZM7GkH1oNMT0luTsDWxx_Tx4ZzSWZrgAFn7H1SecNVZIzJKaYeDbH2soFn2nJ6SDXvAUjPXxvF6GxCBp5I40mSaQW8CppI-ZWw1dbxMRjv2IQHAxsMN_TM-vk0YmjrzOr5SVWkZ3Eib6nor8kP9g0fdsA7Wlhocop8JhjdQ7XDxm0n-VLg75Wnk0o4hnmvYq5jnJg5k6mE0mBQ15mBgC8MhPZTb9VLySl_CXsXzLdT0UuOKpDGbHiWdVjT3Yo_afZt75vzQ0ys1Y5AILlFlibRyVOLnT2YE6906U3DkQLWYTs6XGWiYYS7rDPnZc24xhTj3WtCubINBOCfbcU558klgxM2Mp-TbFvLu0WPy-neeMopCsLbf-sBjWSdGE3RNSV9IuJWV4rQ2XEvtz4xS_E5-9BGw3IW-eaE', + p: '-3KdF6fI22lJ9Kv0HPMJmB4I3KgHgSpnkx99MirabiEFq3Trd5B_6iIeaCz3IKaPMvpujRTmJatNLaw2KNhtETaHQqJhw-_X82CJaYVJSFrmjnltD7dUIJW3LLokq4F7YiObd_oGWb75YKLx8WobX7E4BJd-8Bbcz2LsReYBLim6KKzzQPIw-uXfMcF_pnWA7W7S5JSBKtp458Mo6MYITlv4iAhU5cgXvZiURa57I7woTuSgl9Pmzn_ZR83tbmXK6EJA5J2nzavUgUTOaBmCkNGWztDvsb3XUwtcW48RQgDoHP6dlS4ZX7qBt5shvkDADj4Rc-X-V0pbqkWLCLKRvw', + q: 'yzlFM4JXVpBEO2s_2fmMDKGP6p-d2pv3M54-hjBHyvYOy8Zs_WrrlRm12zDXvV1YfOpHswKk3SxO9jgTAdHd_QPnXTKpO15IriuKhODPK9zRsM1pEm2tskfdv-KsE2TKULVWxBsS_sMn4pGFgTFIrE4aXezvnWBU_DBO31lRWnBHxflCJ3zl68YxNbn3Er--R4GBpyBle500-tiONDYh4eoY_TKlG3b6x4x4n1u5qz2WGr1VKhUro5GFvTj0WRlZvYGXMfIoTqu5dEApMsfRPq1qAcWU-tzy6FF1nwjypkgbr8lCcDYhNr64eRyCUUuArcVi84iIzxD3KNx2S6UD2Q', + dp: '1f1ej-ks2P2sANvuLkzvl1PIOvGNIXHTH7QmufEaX6sexiIE2oZRNhK_Se0qi9D0AXB7cPmeO-SjFAGBPhWiDIoZZq4HAdc2M1uu1eymxzsFB69zD3L315tSfnAUERlqxcSD5QEVMn-Cf6lsugWRMkkB4XaEgxMR7DTF2165Fm5QjZlSk60J3hyPbCq-1g0eIfK06-8uVigDyUmfoSoXm4hN5IciqUM2YRZe6UZqaakRrMKJoWym-op3gdJRBCkBG9R2oZlCW5imizThbFp5cYHNFElgFCX6ACSk_w6soz1eTH6r4W-QDJYPGxCdEOrOB9DuzXGomOUhSbFRd59wxQ', + dq: 'GmasSM7MDeDcHGQIfYbf3Nw4WCC4XygX60rJkKFBEmr47RwwGJQFWu9mIr2rqVwxHLlK60SSqnERKQeL4JalIjOZoQ_t0FqlUosxiaWzBF3BmBh3Z_97q0eO1VjbRgG4DtggF4X058furI5_K5N9f3T-E-muD2HuaHzWIkn2OauQh3WkVIDzVbf_uJ0aLgNe8ucuMsoQpQh9U4FCqCHIMRM6f9pOfMzuM3JaUUmXS1nK4Fpsb_UkIDHNkBGQHOsgL3BgdgqhlebVRvb24zP2SRA6T1Sd0CFYJTo_75M4AsYnYqTgzrcZhYUtbf54J5uJcgnMmxrHuy7XDSXm8FjVsQ', + qi: 'HJhf2ZP_PczoOoEMAw3cN6wdrZLG9J465tDjZ4HYqL9vrzPs7fPrXWJo4-WA-p_2IDXCkMP_t6H6JFyK1xHmDmjNpP7XlTwBb_hcEgn0W3dvmZ597Ey-B38IZfn0J4Wq3s34kcq3tprB5rG08qTm4d_tG-sln8Z7Ey-bLKTWPL_kIqpTCJ0H7cGvFVRMGN2dc9nPb4MYFRXhxZS7JF4SQJyRwPuHEMsY97Ph2IpNYpxKTGR1LfqWwSwnwrfyY_Y8sgkHMSNDvZcdGmaEYxhzTXa9xFGUdEFn2IAUIdvVz0aCBqC0soyfrkF955SDbCkbD2QxhyLX1DBVBcw_HEUCRA' +} + +const URL = 'http://localhost:10000' + +test('request:M2 full stack spawn, message, result', async () => { + const connect = connectWith({ + createDataItemSigner: WalletClient.createSigner, + createHbSigner: WalletClient.createSigner + }) + + const { request } = connect({ MODE: 'mainnet', URL, device: 'process@1.0', signer: createSigner(WALLET) }) + const address = await fetch(`${URL}/~meta@1.0/info/address`).then(res => res.text()) + + // create a process on local cu + const p = await request({ + path: '/schedule', + method: 'POST', + type: 'Process', + scheduler: address, + module: 'JArYBF-D8q2OmZ4Mok00sD2Y_6SYEQ7Hjx-6VZ_jl3g', + device: 'process@1.0', + 'scheduler-device': 'scheduler@1.0', + 'execution-device': 'compute-lite@1.0', + authority: address, + 'scheduler-location': address, + 'random-seed': randomBytes(16).toString('hex'), + data: 'print("Hello World")', + 'Data-Protocol': 'ao', + Variant: 'ao.N.1', + 'On-Boot': 'USxy_74bsS_yuQtYAqCvn9DtxmBuUZHA-4wNVuQxZHU' + + }) + console.log(p) + // .then(tap(console.log)) + const process = p.process + console.log(process) + // message + const m = await request({ + path: `${process}/schedule`, + type: 'Message', + method: 'POST', + action: 'Eval', + data: 'print(Balances)', + 'Data-Protocol': 'ao', + Variant: 'ao.N.1' + + }) + + const slot = m.slot + + // get results + const r = await request({ + path: `/${process}/compute&slot+integer=${slot}/results/json`, + method: 'POST', + target: process, + 'slot+integer': slot, + accept: 'application/json' + }) + console.log(r) + + console.log(r.Output) + assert.ok(true) +}) diff --git a/connect/test/e2e/m2/spawn.test.js b/connect/test/e2e/m2/spawn.test.js new file mode 100644 index 000000000..d5eef808f --- /dev/null +++ b/connect/test/e2e/m2/spawn.test.js @@ -0,0 +1,72 @@ +import { test } from 'node:test' +import * as assert from 'node:assert' +import { connectWith } from '../../../src/index.common.js' +import { WalletClient } from '../../../src/client/node/index.js' +import { createSigner } from '../../../src/client/node/wallet.js' + +import { tap } from 'ramda' +import { randomBytes } from 'node:crypto' + +// this is a test wallet do not use for anything other than testing +const WALLET = { + kty: 'RSA', + n: 'x5wjDziDSmSCvrWTiWk4avD-2DmolJHV6FOqwX8Dv4qvAJUDLq8kGg4r5NGj6yWS_f1N58FyhuAKcNgwBBj5Api5Kx5siL3vvcGywUaIID4wSn0BBjiTOf-IyEp0Ch4esGcf5fxNTzhX5X_2V8uPeXCGI-fbm877swOtgDSPc-ICAbML_CI0lOE60IP-KuWG7N0nsfibfDKs5fDv-JJTl21Dzxdryq44O873Kuyg9uOYIBW454UyBAYNMlSoMO753oLJvQvKi5n5X3ldnexD9bAwSzrjx1uhmtEr98ffxuplwsu0RkBOfolZS7GrD1mA_9zWKba-rreTus69gvbjSnHcQOkn6w11mGlL0_v95CN4kmthxlhuxPQS2R1pdkNYiX9IuULZcet-CO9Cnm6YDsNyvc84ly-iniUUR9Op2DWLzMuwNz9V-qJioKmGWmVE8V3tcu8RheAXnc2EoNwp3nhwV9a0GTcmrH7Dbpk-i60x75raxAYi5w_NAp44jddxueyxthW-3uyQmVtq_t-j1lMmrInTtRoIXi84bfZYzsxDqzx4E56xB2I7wQcOg2JSzLNVmzo6x2gVUSb9K2yCoIgM55eTdSS7sFG_fT60ftvR-RJR4fOpMHbMA8-aOcs6CLgKxFaVrVEWcSKpodKl_wjXXYJnPZE-xrXRyVwtx-c', + e: 'AQAB', + d: 'QMVnkv7Ri4hF7MBa2ZHtHraI230KOPuBn9vWYudmPfcwhk3UNIfcfR_wGlX0jM8qfYR-jR92nFGC0c809X9s_ey02UsMCOspKjf6W0EZ1uyXGvSpKm2dSIqkxely8f2IE9HCxgwywewUiYIWW7LQIaXjwS7xUgO-JP6ihCGKEx5ZbFe_IdevnEfhn9vU0_Ka761rvhJf0dNXy119YPmKam6oPEDrV19lG_MyvRj_ul1r9a1WzLmp2yhji9twoEBLkGp8L_3tgZM7GkH1oNMT0luTsDWxx_Tx4ZzSWZrgAFn7H1SecNVZIzJKaYeDbH2soFn2nJ6SDXvAUjPXxvF6GxCBp5I40mSaQW8CppI-ZWw1dbxMRjv2IQHAxsMN_TM-vk0YmjrzOr5SVWkZ3Eib6nor8kP9g0fdsA7Wlhocop8JhjdQ7XDxm0n-VLg75Wnk0o4hnmvYq5jnJg5k6mE0mBQ15mBgC8MhPZTb9VLySl_CXsXzLdT0UuOKpDGbHiWdVjT3Yo_afZt75vzQ0ys1Y5AILlFlibRyVOLnT2YE6906U3DkQLWYTs6XGWiYYS7rDPnZc24xhTj3WtCubINBOCfbcU558klgxM2Mp-TbFvLu0WPy-neeMopCsLbf-sBjWSdGE3RNSV9IuJWV4rQ2XEvtz4xS_E5-9BGw3IW-eaE', + p: '-3KdF6fI22lJ9Kv0HPMJmB4I3KgHgSpnkx99MirabiEFq3Trd5B_6iIeaCz3IKaPMvpujRTmJatNLaw2KNhtETaHQqJhw-_X82CJaYVJSFrmjnltD7dUIJW3LLokq4F7YiObd_oGWb75YKLx8WobX7E4BJd-8Bbcz2LsReYBLim6KKzzQPIw-uXfMcF_pnWA7W7S5JSBKtp458Mo6MYITlv4iAhU5cgXvZiURa57I7woTuSgl9Pmzn_ZR83tbmXK6EJA5J2nzavUgUTOaBmCkNGWztDvsb3XUwtcW48RQgDoHP6dlS4ZX7qBt5shvkDADj4Rc-X-V0pbqkWLCLKRvw', + q: 'yzlFM4JXVpBEO2s_2fmMDKGP6p-d2pv3M54-hjBHyvYOy8Zs_WrrlRm12zDXvV1YfOpHswKk3SxO9jgTAdHd_QPnXTKpO15IriuKhODPK9zRsM1pEm2tskfdv-KsE2TKULVWxBsS_sMn4pGFgTFIrE4aXezvnWBU_DBO31lRWnBHxflCJ3zl68YxNbn3Er--R4GBpyBle500-tiONDYh4eoY_TKlG3b6x4x4n1u5qz2WGr1VKhUro5GFvTj0WRlZvYGXMfIoTqu5dEApMsfRPq1qAcWU-tzy6FF1nwjypkgbr8lCcDYhNr64eRyCUUuArcVi84iIzxD3KNx2S6UD2Q', + dp: '1f1ej-ks2P2sANvuLkzvl1PIOvGNIXHTH7QmufEaX6sexiIE2oZRNhK_Se0qi9D0AXB7cPmeO-SjFAGBPhWiDIoZZq4HAdc2M1uu1eymxzsFB69zD3L315tSfnAUERlqxcSD5QEVMn-Cf6lsugWRMkkB4XaEgxMR7DTF2165Fm5QjZlSk60J3hyPbCq-1g0eIfK06-8uVigDyUmfoSoXm4hN5IciqUM2YRZe6UZqaakRrMKJoWym-op3gdJRBCkBG9R2oZlCW5imizThbFp5cYHNFElgFCX6ACSk_w6soz1eTH6r4W-QDJYPGxCdEOrOB9DuzXGomOUhSbFRd59wxQ', + dq: 'GmasSM7MDeDcHGQIfYbf3Nw4WCC4XygX60rJkKFBEmr47RwwGJQFWu9mIr2rqVwxHLlK60SSqnERKQeL4JalIjOZoQ_t0FqlUosxiaWzBF3BmBh3Z_97q0eO1VjbRgG4DtggF4X058furI5_K5N9f3T-E-muD2HuaHzWIkn2OauQh3WkVIDzVbf_uJ0aLgNe8ucuMsoQpQh9U4FCqCHIMRM6f9pOfMzuM3JaUUmXS1nK4Fpsb_UkIDHNkBGQHOsgL3BgdgqhlebVRvb24zP2SRA6T1Sd0CFYJTo_75M4AsYnYqTgzrcZhYUtbf54J5uJcgnMmxrHuy7XDSXm8FjVsQ', + qi: 'HJhf2ZP_PczoOoEMAw3cN6wdrZLG9J465tDjZ4HYqL9vrzPs7fPrXWJo4-WA-p_2IDXCkMP_t6H6JFyK1xHmDmjNpP7XlTwBb_hcEgn0W3dvmZ597Ey-B38IZfn0J4Wq3s34kcq3tprB5rG08qTm4d_tG-sln8Z7Ey-bLKTWPL_kIqpTCJ0H7cGvFVRMGN2dc9nPb4MYFRXhxZS7JF4SQJyRwPuHEMsY97Ph2IpNYpxKTGR1LfqWwSwnwrfyY_Y8sgkHMSNDvZcdGmaEYxhzTXa9xFGUdEFn2IAUIdvVz0aCBqC0soyfrkF955SDbCkbD2QxhyLX1DBVBcw_HEUCRA' +} + +const URL = 'http://localhost:10000' + +test('spawn process', async () => { + const connect = connectWith({ + createDataItemSigner: WalletClient.createSigner, + createSigner: WalletClient.createSigner + }) + + const { spawn, message, result } = connect({ MODE: 'mainnet', URL, device: 'process@1.0', signer: createSigner(WALLET) }) + // const address = await request({ + // path: "~meta@1.0/info/address", + // }) + + const address = await fetch(`${URL}/~meta@1.0/info/address`).then(res => res.text()) + const p = await spawn({ + scheduler: address, + module: 'JArYBF-D8q2OmZ4Mok00sD2Y_6SYEQ7Hjx-6VZ_jl3g', + data: 'print("Process initialized.")', + tags: [ + { name: 'device', value: 'process@1.0' }, + { name: 'scheduler-device', value: 'scheduler@1.0' }, + { name: 'execution-device', value: 'compute-lite@1.0' }, + { name: 'authority', value: address }, + { name: 'scheduler-location', value: address }, + { name: 'scheduler', value: address }, + { name: 'random-seed', value: randomBytes(16).toString('hex') }, + { name: 'On-Boot', value: 'USxy_74bsS_yuQtYAqCvn9DtxmBuUZHA-4wNVuQxZHU' } + ] + }).then(tap(console.log)) + // const p = 'PrYCBlk8Qo0N8KdswWkn62Q6ME6LphewHKSWHMqWX9I' + const m = await message({ + process: p, + tags: [ + { name: 'Action', value: 'Transfer' }, + { name: 'Data-Protocol', value: 'ao' }, + { name: 'Variant', value: 'ao.TN.1' }, + { name: 'Type', value: 'Message' }, + { name: 'Quantity', value: '10' }, + { name: 'Recipient', value: 'vh-NTHVvlKZqRxc8LyyTNok65yQ55a_PJ1zWLb9G2JI' } + ], + data: '1984' + }).then(tap(console.log)) + + console.log('READY TO LOAD RESULT. m:', m) + + const r = await result({ message: m, process: p }) + .then(tap(console.log)).catch(console.error) + console.log(r.Output.data) + assert.ok(true) +}) diff --git a/connect/test/e2e/package-lock.json b/connect/test/e2e/package-lock.json index 3110cc7fc..4be4c804c 100644 --- a/connect/test/e2e/package-lock.json +++ b/connect/test/e2e/package-lock.json @@ -1,10 +1,10 @@ { - "name": "@permaweb/ao-sdk-e2e", + "name": "@permaweb/aoconnect-e2e", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@permaweb/ao-sdk-e2e" + "name": "@permaweb/aoconnect-e2e" } } } diff --git a/connect/test/e2e/package.json b/connect/test/e2e/package.json index d8d574b2d..3e5504f53 100644 --- a/connect/test/e2e/package.json +++ b/connect/test/e2e/package.json @@ -3,9 +3,11 @@ "description": "integration test suites for @permaweb/aoconnect", "type": "module", "scripts": { - "test": "npm run test:node-cjs && npm run test:node-esm && npm run test:browser", + "test": "npm run test:node-cjs && npm run test:node-esm && npm run test:browser && npm run test:m0", + "test:browser": "cd ./browser-esm && npm i --legacy-peer-deps && npm start", "test:node-cjs": "cd ./node-cjs && npm i && npm start", - "test:node-esm": "cd ./node-esm && npm i && npm start" + "test:node-esm": "cd ./node-esm && npm i && npm start", + "test:m0": "cd ./m0 && node --test" } } diff --git a/loader/CHANGELOG.md b/loader/CHANGELOG.md index 7da734299..84627ec6a 100644 --- a/loader/CHANGELOG.md +++ b/loader/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## [0.0.48](https://github.com/permaweb/ao/compare/loader@v0.0.47...loader@v0.0.48) (2025-02-23) + + +### Features + +* **loader:** add optional patches return array for mainnet ([ccc6dc9](https://github.com/permaweb/ao/commit/ccc6dc967b0443afef16e8e6e88efd8abcb81d5d)) + + +### Bug Fixes + +* **loader:** improved tests to add host tests ([88d8289](https://github.com/permaweb/ao/commit/88d8289e424ea8e307d2e80dcbcd1724f08d9fe7)) + +## [0.0.47](https://github.com/permaweb/ao/compare/loader@v0.0.46...loader@v0.0.47) (2025-02-15) + + +### Bug Fixes + +* **loader:** stub out getenv ([8002e84](https://github.com/permaweb/ao/commit/8002e84d30c44c43d6c53e00fa0e9560a9756540)) + ## [0.0.46](https://github.com/permaweb/ao/compare/loader@v0.0.45...loader@v0.0.46) (2025-01-29) diff --git a/loader/package-lock.json b/loader/package-lock.json index d04081460..e1cdd6faa 100644 --- a/loader/package-lock.json +++ b/loader/package-lock.json @@ -1,16 +1,17 @@ { "name": "@permaweb/ao-loader", - "version": "0.0.46", + "version": "0.0.48", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@permaweb/ao-loader", - "version": "0.0.46", + "version": "0.0.48", "dependencies": { "@permaweb/wasm-metering": "^0.2.2" }, "devDependencies": { + "@permaweb/loco": "^0.0.17", "esbuild": "^0.19.5", "typescript": "^5.2.2" }, @@ -370,6 +371,30 @@ "node": ">=12" } }, + "node_modules/@permaweb/ao-loader": { + "version": "0.0.47", + "resolved": "https://registry.npmjs.org/@permaweb/ao-loader/-/ao-loader-0.0.47.tgz", + "integrity": "sha512-HdPbjkUzChvWNFCTxRjwt8OCFB4s1DPf8CVsFuiSxMzJuZAKFTtTUTX1GYhuk84Qys/6QAWwcBdn21LUDqkaHA==", + "dev": true, + "dependencies": { + "@permaweb/wasm-metering": "^0.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@permaweb/loco": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@permaweb/loco/-/loco-0.0.17.tgz", + "integrity": "sha512-7q+TT2L2P9jTRt79EHRwAwuUkNq/1yqQXvK3yWkGUlQQY5MacMP2vQZlQiwZMisNf/yiQHX2SwJMbn8ve9qAcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@permaweb/ao-loader": "^0.0.47", + "@permaweb/weavedrive": "^0.0.16", + "hyper-async": "^1.1.2" + } + }, "node_modules/@permaweb/wasm-json-toolkit": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@permaweb/wasm-json-toolkit/-/wasm-json-toolkit-0.2.9.tgz", @@ -398,6 +423,86 @@ "wasm-meter": "bin/wasm-meter" } }, + "node_modules/@permaweb/weavedrive": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@permaweb/weavedrive/-/weavedrive-0.0.16.tgz", + "integrity": "sha512-IUcN9neRtEK158rqJ+NThPAhlI6KSVdb9vlXTt77IgctJV5mj6vFu0leVDlXOMjM/2TYhz18ZrjoCfCYyyVCbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arweave": "^1.15.5" + } + }, + "node_modules/arconnect": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/arconnect/-/arconnect-0.4.2.tgz", + "integrity": "sha512-Jkpd4QL3TVqnd3U683gzXmZUVqBUy17DdJDuL/3D9rkysLgX6ymJ2e+sR+xyZF5Rh42CBqDXWNMmCjBXeP7Gbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "arweave": "^1.10.13" + } + }, + "node_modules/arweave": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/arweave/-/arweave-1.15.5.tgz", + "integrity": "sha512-Zj3b8juz1ZtDaQDPQlzWyk2I4wZPx3RmcGq8pVJeZXl2Tjw0WRy5ueHPelxZtBLqCirGoZxZEAFRs6SZUSCBjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arconnect": "^0.4.2", + "asn1.js": "^5.4.1", + "base64-js": "^1.5.1", + "bignumber.js": "^9.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", @@ -450,6 +555,20 @@ "@esbuild/win32-x64": "0.19.5" } }, + "node_modules/hyper-async": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/hyper-async/-/hyper-async-1.1.2.tgz", + "integrity": "sha512-cnpOgKa+5FZOaccTtjduac1FrZuSc38/ftCp3vYJdUMt+7c+uvGDKLDK4MTNK8D3aFjIeveVrPcSgUPvzZLopg==", + "dev": true, + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, "node_modules/leb128": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/leb128/-/leb128-0.0.4.tgz", @@ -469,6 +588,13 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -489,6 +615,13 @@ ], "license": "MIT" }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", diff --git a/loader/package.json b/loader/package.json index e9371632b..032314c14 100644 --- a/loader/package.json +++ b/loader/package.json @@ -1,6 +1,6 @@ { "name": "@permaweb/ao-loader", - "version": "0.0.46", + "version": "0.0.48", "repository": { "type": "git", "url": "https://github.com/permaweb/ao.git", @@ -24,6 +24,7 @@ "@permaweb/wasm-metering": "^0.2.2" }, "devDependencies": { + "@permaweb/loco": "^0.0.17", "esbuild": "^0.19.5", "typescript": "^5.2.2" }, diff --git a/loader/src/formats/emscripten4.cjs b/loader/src/formats/emscripten4.cjs index fc0e3407d..2279704fb 100644 --- a/loader/src/formats/emscripten4.cjs +++ b/loader/src/formats/emscripten4.cjs @@ -4159,7 +4159,7 @@ var Module = (() => { } getEnvStrings.strings = strings; } - return getEnvStrings.strings; + return []; //getEnvStrings.strings; }; var stringToAscii = (str, buffer) => { diff --git a/loader/src/formats/wasm64-emscripten.cjs b/loader/src/formats/wasm64-emscripten.cjs index ff477dcb2..2bf056f93 100644 --- a/loader/src/formats/wasm64-emscripten.cjs +++ b/loader/src/formats/wasm64-emscripten.cjs @@ -4162,7 +4162,7 @@ var Module = (() => { } getEnvStrings.strings = strings; } - return getEnvStrings.strings; + return []; //getEnvStrings.strings; }; var stringToAscii = (str, buffer) => { diff --git a/loader/src/index.cjs b/loader/src/index.cjs index acb860bce..34404cdc1 100644 --- a/loader/src/index.cjs +++ b/loader/src/index.cjs @@ -60,6 +60,7 @@ const metering = require('@permaweb/wasm-metering') * @property {DataItem} Output * @property {Message[]} Messages * @property {Message[]} Spawns + * @property {Patches[]} Patches * @property {AssignmentTypes.Assignment[]} Assignments */ @@ -262,6 +263,7 @@ module.exports = async function (binary, options) { Messages: response.Messages, Spawns: response.Spawns, Assignments: response.Assignments, + Patches: response.Patches || [], GasUsed: instance.gas.used } } finally { diff --git a/loader/test/host.test.js b/loader/test/host.test.js new file mode 100644 index 000000000..c7ee2f092 --- /dev/null +++ b/loader/test/host.test.js @@ -0,0 +1,12 @@ +import { aoslocal } from '@permaweb/loco' +import { test } from 'node:test' +import * as assert from 'node:assert' + +// skip test first then add back next pr +test('genEnv should return nil', async () => { + const aos = await aoslocal() + + const result = await aos.eval('if not os.getenv("PATH") then print([[2]]) else print([[1]]) end') + // const result = await aos.eval('print(os.getenv([[PATH]]))') + assert.equal(result.Output.data, '2') +}) diff --git a/loader/test/loader.test.js b/loader/test/loader.test.js index a349e93ab..eb1022c98 100644 --- a/loader/test/loader.test.js +++ b/loader/test/loader.test.js @@ -79,13 +79,13 @@ describe('AoLoader Functionality Tests', () => { it('load wasm and evaluate message', async () => { const handle = await AoLoader(wasmBinary, options) const mainResult = await handle(null, getMsg('return \'Hello World\''), getEnv()) - + //const mainResult = await handle(null, getMsg("print(os.getenv(\"PATH\"))"),getEnv()) // Check basic properties of the result assert.ok(mainResult.Memory) assert.ok(mainResult.hasOwnProperty('Messages')) assert.ok(mainResult.hasOwnProperty('Spawns')) assert.ok(mainResult.hasOwnProperty('Error')) - assert.equal(mainResult.Output.data, 'Hello World') // Expect 'Hello World' in output + assert.equal(mainResult.Output.data, 'Hello World') // Expect 'nil' in output }) it('should use separately instantiated WebAssembly.Instance', async () => { diff --git a/loader/test/metering.test.js b/loader/test/metering.test.js index 3033c1467..946441808 100644 --- a/loader/test/metering.test.js +++ b/loader/test/metering.test.js @@ -99,7 +99,8 @@ describe('AoLoader Metering Tests', () => { // Check that gas usage difference is within an acceptable range const gasDiff = Math.abs(result.GasUsed - result2.GasUsed) - assert.ok(gasDiff < 900000) + console.log(gasDiff) + assert.ok(gasDiff < 1900000) }) it('should run out of gas', async () => { diff --git a/servers/cu/README.md b/servers/cu/README.md index ca272f30c..44b157a11 100644 --- a/servers/cu/README.md +++ b/servers/cu/README.md @@ -14,9 +14,10 @@ This is an spec compliant `ao` Compute Unit, implemented using NodeJS - [Project Structure](#project-structure) - [Business Logic](#business-logic) - [Driven Adapters](#driven-adapters) + - [Driving Adapter](#driving-adapter) + - [Routes](#routes) + - [Route Middleware](#route-middleware) - [Entrypoint](#entrypoint) - - [Routes](#routes) - - [Middleware](#middleware) - [System Requirements](#system-requirements) @@ -123,6 +124,9 @@ There are a few environment variables that you can set. Besides - `EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD`: If a process uses this amount of gas, then it will immediately create a Checkpoint at the end of the evaluation stream. +- `EAGER_CHECKPOINT_EVAL_TIME_THRESHOLD`: If a process has this amount of eval + time, then it will immediately create a Checkpoint at the end of the + evaluation stream. - `MEM_MONITOR_INTERVAL`: The interval, in milliseconds, at which to log memory usage on this CU. - `BUSY_THRESHOLD`: The amount of time, in milliseconds, the CU should wait for @@ -140,7 +144,8 @@ There are a few environment variables that you can set. Besides - `PROCESS_CHECKPOINT_TRUSTED_OWNERS`: A list of wallets whose checkpoints are trusted and the CU can start from - `DEFAULT_LOG_LEVEL`: the logging level to use (defaults to `debug`) -- `LOG_CONFIG_PATH`: the path to the file used to dynamically set the logging level (see [here](#dynamically-change-the-log-level)) +- `LOG_CONFIG_PATH`: the path to the file used to dynamically set the logging + level (see [here](#dynamically-change-the-log-level)) ## Tests @@ -177,11 +182,11 @@ If you'd like to dynamically change the log level on a running CU, you may set the desired level in a `.loglevel` file in the working directory. The CU will automatically adjust its logging level accordingly, for all new logs. -If the `.loglevel` file does not exist, is empty, the logging level will be reset -to the `DEFAULT_LOG_LEVEL`. +If the `.loglevel` file does not exist, is empty, the logging level will be +reset to the `DEFAULT_LOG_LEVEL`. -> You can also specify the `LOG_CONFIG_PATH` environment variable to configure -> a different file to use for dynamically setting the log level +> You can also specify the `LOG_CONFIG_PATH` environment variable to configure a +> different file to use for dynamically setting the log level ## Manually Trigger Checkpointing @@ -232,31 +237,33 @@ API. Thus our unit tests are simoultaneously contract tests. All driven adapters are located in `effects` -`effects` contains implementations, of the contracts in `dal.js`, for -various platforms. The unit tests for the implementations in `effects` also -import contracts from `domain/dal.js` to help ensure that the implementation properly +`effects` contains implementations, of the contracts in `dal.js`, for various +platforms. The unit tests for the implementations in `effects` also import +contracts from `domain/dal.js` to help ensure that the implementation properly satisfies the API. -#### Entrypoint +#### Driving Adapter -Finally, the entrypoint `bootstrap.js` sets up the appropriate -implementations from `effects` and injects them into the public apis from `domain/apis`. +All driving adapters, which is to say the public API, are also located in +effects. -Anything outside of domain should only ever import from the top level of `domain/*` or -`domain/apis/*`. +The driving adapter is responsible for receving `domain` and returning an +interface to `start` and `stop` the application. The driving adapter is +responsible for injecting the `domain` into whereever it needs to, from the +public API it exposes. -### Routes +##### Routes -All public routes exposed by the `ao` Compute Unit can be found in `/routes`. -Each route is composed in `/route/index.js`, which is then composed further in -`app.js`, the Fastify server. This is the Driving Adapter. +All public routes are provided by the driving Adapter. For example, the +`effects/ao-http` adapter exposes a public API that can be consumed over HTTP +and whose interface is understood by other AO units. -#### Middleware +###### Route Middleware -This `ao` Compute Unit uses simple function composition to achieve middleware -behavior on routes. This allows for a more idiomatic developer experience -- if -an error occurs, it can simply be thrown, which bubbles and is caught by a -middleware that is composed at the top (see `withErrorHandler.js`). +This `ao-http` driving adapter uses simple function composition to achieve middleware +behavior on Driving adapter routes. This allows for a more idiomatic developer +experience -- if an error occurs, it can simply be thrown, which bubbles and is +caught by a middleware that is composed at the top. In fact, our routes don't event import `fastify`, and instead are injected an instance of `fastify` to mount routes onto. @@ -264,10 +271,12 @@ instance of `fastify` to mount routes onto. > `fastify` middleware is still leveraged, it is abstracted away from the > majority of the developer experience, only existing in `app.js` -Business logic is injected into routes via a composed middleware `withDomain.js` -that attached `config` and business logic apis to `req.domain`. This is how -routes call into business logic, thus completing the Ports and Adapters -Architecture. +#### Entrypoint + +Finally, the entrypoint `app.js` builds the appropriate effects based on the +`UNIT_MODE`, then passes those effects into `domain/index.js` that will then +bootstrap the application. `app.js` then starts the application, thus completing +the Ports and Adapters Architecture. ## System Requirements diff --git a/servers/cu/package-lock.json b/servers/cu/package-lock.json index 8f90460ab..be847a081 100644 --- a/servers/cu/package-lock.json +++ b/servers/cu/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@fastify/middie": "^9.0.3", - "@permaweb/ao-loader": "^0.0.44", + "@permaweb/ao-loader": "^0.0.48", "@permaweb/ao-scheduler-utils": "^0.0.25", "@permaweb/weavedrive": "^0.0.18", "arweave": "^1.15.5", @@ -193,9 +193,9 @@ } }, "node_modules/@permaweb/ao-loader": { - "version": "0.0.44", - "resolved": "https://registry.npmjs.org/@permaweb/ao-loader/-/ao-loader-0.0.44.tgz", - "integrity": "sha512-O/5XuwqxCD9dTIN/jZ6x4rmqIA/Css0bqaXScOrXc0xTz7VjYseM+PNXFf8vAXiOgnNFmrZzDJ0or94cjmqhZA==", + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@permaweb/ao-loader/-/ao-loader-0.0.48.tgz", + "integrity": "sha512-X03O9WvvxN6Nk6/N1K6Nie+46fZFFv8B++LS+ppa+dciNCynVH3fbvv0aMWDK1wwWVqTl7QOhrAIA23vCA/s1Q==", "dependencies": { "@permaweb/wasm-metering": "^0.2.2" }, diff --git a/servers/cu/package.json b/servers/cu/package.json index 3b27b5d90..80b179126 100644 --- a/servers/cu/package.json +++ b/servers/cu/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@fastify/middie": "^9.0.3", - "@permaweb/ao-loader": "^0.0.44", + "@permaweb/ao-loader": "^0.0.48", "@permaweb/ao-scheduler-utils": "^0.0.25", "@permaweb/weavedrive": "^0.0.18", "arweave": "^1.15.5", diff --git a/servers/cu/src/app.js b/servers/cu/src/app.js index 6cc1faf28..bd25fe796 100644 --- a/servers/cu/src/app.js +++ b/servers/cu/src/app.js @@ -1,82 +1,55 @@ -import { unapply, pipeWith } from 'ramda' -import ms from 'ms' +/** + * The dirty component that knows all the details about which + * effects, and which configuration should be passed where. + */ -import Fastify from 'fastify' -import FastifyMiddie from '@fastify/middie' -import cors from 'cors' -import helmet from 'helmet' +import { setGlobalDispatcher, Agent, fetch } from 'undici' -import { logger as _logger } from './logger.js' import { config } from './config.js' -import { withRoutes } from './routes/index.js' +import { logger } from './logger.js' -import { domain } from './routes/middleware/withDomain.js' +import { bootstrap } from './domain/index.js' -const logger = _logger.child('app') +import { createEffects as createCuEffects } from './effects/main.cu.js' +import { createEffects as createHbEffects } from './effects/main.hb.js' -const pipeP = unapply(pipeWith((fn, p) => Promise.resolve(p).then(fn))) +setGlobalDispatcher(new Agent({ + /** the timeout, in milliseconds, after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server */ + keepAliveTimeout: 30 * 1000, // 30 seconds + /** the maximum allowed `idleTimeout`, in milliseconds, when overridden by *keep-alive* hints from the server. */ + keepAliveMaxTimeout: 10 * 60 * 1000 // 10 mins +})) -export const server = pipeP( - /** - * Allow us to use some simple express middleware - */ - (app) => app.register(FastifyMiddie).then(() => app), - (app) => app.use(helmet()), - (app) => app.use(cors()), - withRoutes, - (app) => { - /** - * See https://github.com/fastify/fastify?tab=readme-ov-file#note - */ - const server = app.listen({ port: config.port, host: '0.0.0.0' }, () => { - logger(`Server is running on http://localhost:${config.port}`) - logger(`Server in unit mode: "${config.UNIT_MODE}"`) - }) +function chooseEffects (ctx) { + if (['cu', 'ru'].includes(ctx.UNIT_MODE)) { + return createCuEffects({ ...config, logger, fetch }) + } - const memMonitor = setInterval(async () => { - logger('Stats Usage: %j', await domain.apis.stats()) - logger('Currently Pending readState operations: %j', domain.apis.pendingReadStates()) - }, config.MEM_MONITOR_INTERVAL) - memMonitor.unref() + if (ctx.UNIT_MODE === 'hbu') { + return createHbEffects({ ...config, logger, fetch }) + } - if (config.PROCESS_MEMORY_CACHE_CHECKPOINT_INTERVAL) { - logger('Setting up Interval to Checkpoint all Processes every %s', ms(config.PROCESS_MEMORY_CACHE_CHECKPOINT_INTERVAL)) - const cacheCheckpointInterval = setInterval(async () => { - logger('Checkpoint Interval Reached. Attempting to Checkpoint all Processes currently in WASM heap cache...') - await domain.apis.checkpointWasmMemoryCache().toPromise() - logger('Interval Checkpoint Done. Done checkpointing all processes in WASM heap cache.') - }, config.PROCESS_MEMORY_CACHE_CHECKPOINT_INTERVAL) - cacheCheckpointInterval.unref() - } + throw new Error(`Unknown effects for UNIT_MODE: ${ctx.UNIT_MODE}`) +} - process.on('SIGTERM', async () => { - logger('Received SIGTERM. Gracefully shutting down server...') - app.close( - () => logger('Server shut down.'), - (e) => logger('Failed to shut down server!', e) - ) +export const cu = async ({ config, logger }) => { + const effects = { + ...(await chooseEffects({ ...config, logger, fetch })), + logger + } - logger('Received SIGTERM. Attempting to Checkpoint all Processes currently in WASM heap cache...') - await domain.apis.checkpointWasmMemoryCache().toPromise() - logger('Done checkpointing all processes in WASM heap cache. Exiting...') - process.exit() - }) + const application = await bootstrap({ config, effects }) - process.on('SIGUSR2', async () => { - logger('Received SIGUSR2. Manually Attempting to Checkpoint all Processes currently in WASM heap cache...') - await domain.apis.checkpointWasmMemoryCache().toPromise() - logger('SIGUSR2 Done. Done checkpointing all processes in WASM heap cache.') - }) + process.on('uncaughtException', (err) => { + console.trace('Uncaught Exception:', err) + process.exit(1) + }) - process.on('uncaughtException', (err) => { - console.trace('Uncaught Exception:', err) - process.exit(1) - }) + process.on('unhandledRejection', (reason, promise) => { + console.trace('Unhandled Rejection at:', promise, 'reason:', reason) + }) - process.on('unhandledRejection', (reason, promise) => { - console.trace('Unhandled Rejection at:', promise, 'reason:', reason) - }) + return application.start() +} - return server - } -)(Fastify()) +export const app = cu({ config, logger }) diff --git a/servers/cu/src/config.js b/servers/cu/src/config.js index 70f668554..2927ffaa2 100644 --- a/servers/cu/src/config.js +++ b/servers/cu/src/config.js @@ -43,9 +43,9 @@ const serverConfigSchema = domainConfigSchema.extend({ MODE: z.enum(['development', 'production']), /** * Whether the unit is operating as a Compute Unit - * or Read Unit. Defaults to 'cu'. + * or Read Unit or HyperBEAM Unit. Defaults to 'cu'. */ - UNIT_MODE: z.enum(['cu', 'ru']), + UNIT_MODE: z.enum(['cu', 'ru', 'hbu']), port: positiveIntSchema, ENABLE_METRICS_ENDPOINT: z.preprocess((val) => !!val, z.boolean()) }) @@ -59,20 +59,22 @@ const serverConfigSchema = domainConfigSchema.extend({ const preprocessUnitMode = (envConfig) => { const { UNIT_MODE } = envConfig - if (UNIT_MODE === 'cu') return envConfig - /** * A Read Unit's primary concern is serving dry-runs, * and so does not create checkpoints and does not cache evaluation * results to be served later. */ - return { - ...envConfig, - DISABLE_PROCESS_EVALUATION_CACHE: true, - DISABLE_PROCESS_CHECKPOINT_CREATION: true, - DISABLE_PROCESS_FILE_CHECKPOINT_CREATION: true, - PROCESS_MEMORY_CACHE_CHECKPOINT_INTERVAL: 0 + if (UNIT_MODE === 'ru') { + return { + ...envConfig, + DISABLE_PROCESS_EVALUATION_CACHE: true, + DISABLE_PROCESS_CHECKPOINT_CREATION: true, + DISABLE_PROCESS_FILE_CHECKPOINT_CREATION: true, + PROCESS_MEMORY_CACHE_CHECKPOINT_INTERVAL: 0 + } } + + return envConfig } /** @@ -136,6 +138,7 @@ const CONFIG_ENVS = { CHECKPOINT_GRAPHQL_URL: process.env.CHECKPOINT_GRAPHQL_URL, ARWEAVE_URL: process.env.ARWEAVE_URL, UPLOADER_URL: process.env.UPLOADER_URL || 'https://up.arweave.net', + HB_URL: process.env.HB_URL, DB_URL: process.env.DB_URL || 'ao-cache', WALLET: process.env.WALLET, WALLET_FILE: process.env.WALLET_FILE, @@ -151,6 +154,7 @@ const CONFIG_ENVS = { * catching up to a previous checkpoint. */ EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD: process.env.EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD || 300_000_000_000_000, + EAGER_CHECKPOINT_EVAL_TIME_THRESHOLD: process.env.EAGER_CHECKPOINT_EVAL_TIME_THRESHOLD || ms('15m'), PROCESS_WASM_MEMORY_MAX_LIMIT: process.env.PROCESS_WASM_MEMORY_MAX_LIMIT || bytes('1gb'), PROCESS_WASM_COMPUTE_MAX_LIMIT: process.env.PROCESS_WASM_COMPUTE_MAX_LIMIT || 9_000_000_000_000, // 9t PROCESS_WASM_SUPPORTED_FORMATS: process.env.PROCESS_WASM_SUPPORTED_FORMATS || DEFAULT_PROCESS_WASM_MODULE_FORMATS, @@ -188,6 +192,7 @@ const CONFIG_ENVS = { CHECKPOINT_GRAPHQL_URL: process.env.CHECKPOINT_GRAPHQL_URL, ARWEAVE_URL: process.env.ARWEAVE_URL, UPLOADER_URL: process.env.UPLOADER_URL || 'https://up.arweave.net', + HB_URL: process.env.HB_URL, DB_URL: process.env.DB_URL || 'ao-cache', WALLET: process.env.WALLET, WALLET_FILE: process.env.WALLET_FILE, @@ -203,6 +208,7 @@ const CONFIG_ENVS = { * This is the baseline for checkpointing as no process should need to spend more than two hours catching up to a previous checkpoint. */ EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD: process.env.EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD || 300_000_000_000_000, + EAGER_CHECKPOINT_EVAL_TIME_THRESHOLD: process.env.EAGER_CHECKPOINT_EVAL_TIME_THRESHOLD || ms('15m'), PROCESS_WASM_MEMORY_MAX_LIMIT: process.env.PROCESS_WASM_MEMORY_MAX_LIMIT || bytes('1gb'), // 1GB PROCESS_WASM_COMPUTE_MAX_LIMIT: process.env.PROCESS_WASM_COMPUTE_MAX_LIMIT || 9_000_000_000_000, // 9t PROCESS_WASM_SUPPORTED_FORMATS: process.env.PROCESS_WASM_SUPPORTED_FORMATS || DEFAULT_PROCESS_WASM_MODULE_FORMATS, diff --git a/servers/cu/src/domain/api/dryRun.js b/servers/cu/src/domain/api/dryRun.js index bb5e413f6..71ec4462e 100644 --- a/servers/cu/src/domain/api/dryRun.js +++ b/servers/cu/src/domain/api/dryRun.js @@ -48,7 +48,7 @@ const TtlCache = ({ setTimeout, clearTimeout }) => { * @typedef Result * * @typedef ReadResultArgs - * @property {string} messageTxId + * @property {string} messageUid * * @callback ReadResult * @param {ReadResultArgs} args @@ -73,16 +73,16 @@ export function dryRunWith (env) { const readStateCache = TtlCache(env) - function loadMessageCtx ({ messageTxId, processId }) { + function loadMessageCtx ({ messageUid, processId }) { /** * Load the metadata associated with the messageId ie. * it's timestamp and ordinate, so readState can evaluate * up to that point (if it hasn't already) */ - if (messageTxId) return loadMessageMeta({ processId, messageTxId }) + if (messageUid) return loadMessageMeta({ processId, messageUid }) /** - * No messageTxId provided so evaluate up to latest + * No messageUid provided so evaluate up to latest */ return Resolved({ processId, @@ -161,8 +161,8 @@ export function dryRunWith (env) { return Resolved(ctx) } - return ({ processId, messageTxId, maxProcessAge = DEFAULT_MAX_PROCESS_AGE, dryRun }) => { - return of({ processId, messageTxId }) + return ({ processId, messageUid, maxProcessAge = DEFAULT_MAX_PROCESS_AGE, dryRun }) => { + return of({ processId, messageUid }) .chain(loadMessageCtx) .chain(ensureProcessLoaded({ maxProcessAge })) .chain(ensureModuleLoaded) diff --git a/servers/cu/src/domain/api/readResult.js b/servers/cu/src/domain/api/readResult.js index fc8c3579f..d71e2c572 100644 --- a/servers/cu/src/domain/api/readResult.js +++ b/servers/cu/src/domain/api/readResult.js @@ -10,7 +10,7 @@ import { loadMessageMetaWith } from '../lib/loadMessageMeta.js' * @typedef Result * * @typedef ReadResultArgs - * @property {string} messageTxId + * @property {string} messageUid * * @callback ReadResult * @param {ReadResultArgs} args @@ -23,12 +23,12 @@ export function readResultWith (env) { const loadMessageMeta = loadMessageMetaWith(env) const readState = readStateWith(env) - return ({ processId, messageTxId }) => { - return of({ processId, messageTxId }) + return ({ processId, messageUid, body }) => { + return of({ processId, messageUid, body }) .chain(loadMessageMeta) .chain(res => readState({ processId: res.processId, - messageId: messageTxId, + messageId: messageUid, to: res.timestamp, /** * The ordinate for a scheduled message is it's nonce @@ -41,9 +41,28 @@ export function readResultWith (env) { * So we explicitly set cron to undefined, for posterity */ cron: undefined, - needsOnlyMemory: false + needsOnlyMemory: false, + body: body + })) + .map((res) => ({ + output: omit(['Memory'], res.output), + last: res.exact + /** + * The message was cached, and so not eval stream + * was spun up, so map the cached value to the + * shape of 'last' + */ + ? { + id: res.messageId, + timestamp: res.from, + blockHeight: res.fromBlockHeight, + ordinate: res.ordinate, + cron: res.fromCron + } + : res.last, + isColdStart: res.isColdStart, + exact: res.exact, + stats: res.stats })) - .map((res) => res.output) - .map(omit(['Memory'])) } } diff --git a/servers/cu/src/domain/api/readState.js b/servers/cu/src/domain/api/readState.js index 17901c22d..6cb33058d 100644 --- a/servers/cu/src/domain/api/readState.js +++ b/servers/cu/src/domain/api/readState.js @@ -37,7 +37,7 @@ export function readStateWith (env) { const loadModule = loadModuleWith(env) const evaluate = evaluateWith(env) - return ({ processId, messageId, to, ordinate, cron, needsOnlyMemory }) => { + return ({ processId, messageId, to, ordinate, cron, needsOnlyMemory, body }) => { messageId = messageId || [to, ordinate, cron].filter(isNotNil).join(':') || 'latest' const stats = { @@ -84,7 +84,7 @@ export function readStateWith (env) { * there is only one instance of the work used to resolve each Async, * every time, thus preventing duplication of work */ - pending = of({ id: processId, messageId, to, ordinate, cron, stats, needsOnlyMemory }) + pending = of({ id: processId, messageId, to, ordinate, cron, stats, needsOnlyMemory, body }) .chain(loadProcessMeta) .chain(loadProcess) .chain((ctx) => { diff --git a/servers/cu/src/domain/dal.js b/servers/cu/src/domain/dal.js index c5e026867..73133ff98 100644 --- a/servers/cu/src/domain/dal.js +++ b/servers/cu/src/domain/dal.js @@ -1,3 +1,13 @@ +/** + * The business logic is encapsulated away from the effects + * using simple zod schemas. + * + * All calls entering and exiting the business logic, into an effect, + * ought to pass through a schema specified here. This ensures the + * business logic is always dealing with the shapes that it expects + * from the "outside" effect world. + */ + import { z } from 'zod' import { blockSchema, evaluationSchema, processSchema, moduleSchema, rawTagSchema, processCheckpointSchema, bufferSchema } from './model.js' @@ -76,7 +86,8 @@ export const saveLatestProcessMemorySchema = z.function() cron: z.string().nullish(), blockHeight: z.coerce.number().nullish(), Memory: bufferSchema, - gasUsed: z.bigint().nullish() + gasUsed: z.bigint().nullish(), + evalTime: z.number().nullish() })) .returns(z.promise(z.any())) @@ -175,9 +186,13 @@ export const loadMessagesSchema = z.function() moduleTags: z.array(rawTagSchema), moduleOwner: z.string(), from: z.coerce.number().nullish(), + fromOrdinate: z.coerce.number().nullish(), to: z.coerce.number().nullish(), + toOrdinate: z.coerce.number().nullish(), assignmentId: z.string().nullish(), - hashChain: z.string().nullish() + hashChain: z.string().nullish(), + isColdStart: z.boolean(), + body: z.any().optional() }) ) /** @@ -211,7 +226,8 @@ export const loadMessageMetaSchema = z.function() .args(z.object({ suUrl: z.string().url(), processId: z.string().min(1), - messageTxId: z.string().min(1) + messageUid: z.string().min(1), + body: z.any().optional() })) .returns(z.promise( z.object({ @@ -231,3 +247,10 @@ export const locateProcessSchema = z.function() url: z.string() }) )) + +// Application + +export const applicationSchema = z.object({ + start: z.function().returns(z.promise(z.any())), + stop: z.function().returns(z.promise(z.any())) +}) diff --git a/servers/cu/src/domain/index.js b/servers/cu/src/domain/index.js new file mode 100644 index 000000000..e954c218a --- /dev/null +++ b/servers/cu/src/domain/index.js @@ -0,0 +1,154 @@ +import { fromPromise } from 'hyper-async' + +import { applicationSchema } from './dal.js' +import { domainConfigSchema } from './model.js' + +import { readResultWith } from './api/readResult.js' +import { readStateWith, pendingReadStates } from './api/readState.js' +import { readCronResultsWith } from './api/readCronResults.js' +import { healthcheckWith } from './api/healthcheck.js' +import { readResultsWith } from './api/readResults.js' +import { dryRunWith } from './api/dryRun.js' +import { statsWith } from './api/perf.js' + +export const bootstrap = async ({ config, effects }) => { + const logger = effects.logger + + logger('Creating business logic apis') + + const domainConfig = domainConfigSchema.parse(config) + + const commonDeps = (logger) => ({ + /** + * TODO: not sure if I like this or not, having a 'common' + * set of effects. For now, keeping to reduce deltas in the rejig + * to full ports and adapters. + */ + ...effects.common(logger), + MODULE_MODE: domainConfig.MODULE_MODE, + logger + }) + + const stats = statsWith({ + gauge: effects.gauge, + loadWorkerStats: effects.loadWorkerStats, + loadMemoryUsage: effects.loadMemoryUsage, + loadProcessCacheUsage: effects.loadProcessCacheUsage + }) + + /** + * TODO: Should this just be a field on stats to call? + */ + const metrics = { + contentType: effects.metrics.contentType, + compute: fromPromise(() => effects.metrics.metrics()) + } + + const readStateLogger = logger.child('readState') + const readState = readStateWith(commonDeps(readStateLogger)) + + const dryRunLogger = logger.child('dryRun') + const dryRun = dryRunWith({ + ...commonDeps(dryRunLogger), + setTimeout: effects.setTimeout, + clearTimeout: effects.clearTimeout, + loadMessageMeta: effects.loadMessageMeta, + /** + * Dry-runs use a separate worker thread pool, so as to not block + * primary evaluations + */ + loadDryRunEvaluator: effects.loadDryRunEvaluator + }) + + /** + * default readResult that works OOTB + * - Uses PouchDB to cache evaluations and processes + * - Uses ao Sequencer Unit + * - Use arweave.net gateway + */ + const readResultLogger = logger.child('readResult') + const readResult = readResultWith({ + ...commonDeps(readResultLogger), + loadMessageMeta: effects.loadMessageMeta + }) + + const readCronResultsLogger = logger.child('readCronResults') + const readCronResults = readCronResultsWith({ + ...commonDeps(readCronResultsLogger), + findEvaluations: effects.findEvaluations + }) + + const readResultsLogger = logger.child('readResults') + const readResults = readResultsWith({ + ...commonDeps(readResultsLogger), + findEvaluations: effects.findEvaluations + }) + + const healthcheck = healthcheckWith({ walletAddress: effects.address }) + + /** + * The CU domain -- the business logic of the unit + */ + const domain = { + ...domainConfig, + logger, + apis: { + metrics, + stats, + pendingReadStates, + readState, + dryRun, + readResult, + readResults, + readCronResults, + healthcheck + } + } + + /** + * Inject the business logic into the app effect, + * which will expose an api for starting and stopping + * the CU application, in whatever form it takes + * + * { start, stop } + */ + const cu = applicationSchema.parse(await effects.app(domain)) + + const memMonitor = setInterval(async () => { + logger('Stats Usage: %j', await domain.apis.stats()) + logger('Currently Pending readState operations: %j', domain.apis.pendingReadStates()) + }, config.MEM_MONITOR_INTERVAL) + memMonitor.unref() + + // DEPRECATED: Checkpointing is no longer done on an interval, as we are using the eager checkpointing feature + // if (config.PROCESS_MEMORY_CACHE_CHECKPOINT_INTERVAL) { + // logger('Setting up Interval to Checkpoint all Processes every %s', ms(domainConfig.PROCESS_MEMORY_CACHE_CHECKPOINT_INTERVAL)) + // const cacheCheckpointInterval = setInterval(async () => { + // logger('Checkpoint Interval Reached. Attempting to Checkpoint all Processes currently in WASM heap cache...') + // await domain.apis.checkpointProcesses().toPromise() + // logger('Interval Checkpoint Done. Done checkpointing all processes in WASM heap cache.') + // }, config.PROCESS_MEMORY_CACHE_CHECKPOINT_INTERVAL) + // cacheCheckpointInterval.unref() + // } + + // process.on('SIGUSR2', async () => { + // logger('Received SIGUSR2. Manually Attempting to Checkpoint all Processes currently in WASM heap cache...') + // await domain.apis.checkpointProcesses().toPromise() + // logger('SIGUSR2 Done. Done checkpointing all processes in WASM heap cache.') + // }) + + process.on('SIGTERM', async () => { + logger('Received SIGTERM. Gracefully shutting down server...') + // logger('Received SIGTERM. Attempting to Checkpoint all Processes currently in WASM heap cache...') + + await Promise.all([ + cu.stop() + // domain.apis.checkpointWasmMemoryCache().toPromise() + ]) + + // logger('Done checkpointing all processes. Exiting...') + process.exit() + }) + + return cu +} diff --git a/servers/cu/src/domain/lib/evaluate.js b/servers/cu/src/domain/lib/evaluate.js index 9f28bbe60..d94145b2e 100644 --- a/servers/cu/src/domain/lib/evaluate.js +++ b/servers/cu/src/domain/lib/evaluate.js @@ -26,7 +26,7 @@ const ctxSchema = z.object({ timestamp: z.coerce.number(), blockHeight: z.coerce.number(), ordinate: z.coerce.string() - }) + }).passthrough() }).passthrough() const toEvaledCron = ({ timestamp, cron }) => `${timestamp}-${cron}` @@ -105,6 +105,7 @@ export function evaluateWith (env) { Assignments: pathOr([], ['result', 'Assignments']), Spawns: pathOr([], ['result', 'Spawns']), Output: pathOr('', ['result', 'Output']), + Patches: pathOr([], ['result', 'Patches']), GasUsed: pathOr(undefined, ['result', 'GasUsed']), noSave: always(true) })(ctx) @@ -318,6 +319,10 @@ export function evaluateWith (env) { const { noSave, cron, ordinate, message } = prev if (!noSave && prev.Memory) { + const now = new Date() + // If there is no startTime, then we use the current time which will make evalTime = 0 + const startTime = pathOr(now, ['stats', 'startTime'], ctx) + await saveLatestProcessMemory({ processId: ctx.id, moduleId: ctx.moduleId, @@ -331,16 +336,23 @@ export function evaluateWith (env) { ordinate, cron, Memory: prev.Memory, - gasUsed: totalGasUsed + gasUsed: totalGasUsed, + evalTime: now.getTime() - startTime.getTime() // The eval time in ms: currTime - startTime }) } return { output: prev, last: { + /** + * id could be undefined, since the latest message evaluated + * could have been a Cron Message, and so will not have an 'id'. + */ + id: pathOr(undefined, ['message', 'Id'], prev), timestamp: pathOr(ctx.from, ['message', 'Timestamp'], prev), blockHeight: pathOr(ctx.fromBlockHeight, ['message', 'Block-Height'], prev), - ordinate: pathOr(ctx.ordinate, ['ordinate'], prev) + ordinate: pathOr(ctx.ordinate, ['ordinate'], prev), + cron: pathOr(ctx.fromCron, ['cron'], prev) } } })) diff --git a/servers/cu/src/domain/lib/evaluate.test.js b/servers/cu/src/domain/lib/evaluate.test.js index cc453088b..b6ed113b5 100644 --- a/servers/cu/src/domain/lib/evaluate.test.js +++ b/servers/cu/src/domain/lib/evaluate.test.js @@ -63,6 +63,7 @@ describe('evaluate', () => { moduleId: 'foo-module', moduleOptions, stats: { + startTime: new Date(), messages: { scheduled: 0, cron: 0, @@ -198,6 +199,7 @@ describe('evaluate', () => { moduleId: 'foo-module', moduleOptions, stats: { + startTime: new Date(), messages: { scheduled: 0, cron: 0, @@ -313,6 +315,7 @@ describe('evaluate', () => { moduleId: 'foo-module', moduleOptions, stats: { + startTime: new Date(), messages: { scheduled: 0, cron: 0, @@ -460,6 +463,7 @@ describe('evaluate', () => { moduleId: 'foo-module', moduleOptions, stats: { + startTime: new Date(), messages: { scheduled: 0, cron: 0, @@ -570,6 +574,7 @@ describe('evaluate', () => { moduleId: 'foo-module', moduleOptions, stats: { + startTime: new Date(), messages: { scheduled: 0, cron: 0, @@ -708,6 +713,7 @@ describe('evaluate', () => { mostRecentHashChain: 'init-hashchain-123', moduleOptions, stats: { + startTime: new Date(), messages: { scheduled: 0, cron: 0, diff --git a/servers/cu/src/domain/lib/gatherResults.js b/servers/cu/src/domain/lib/gatherResults.js index 91235851c..7ae8d9572 100644 --- a/servers/cu/src/domain/lib/gatherResults.js +++ b/servers/cu/src/domain/lib/gatherResults.js @@ -18,7 +18,7 @@ const ctxSchema = z.object({ /** * @typedef Args - * @property {string} messageTxId - the transaction id of the message + * @property {string} messageUid - the transaction id of the message * * @typedef Result * @property {any[]} messages - the array of cron messages over the interval @@ -115,6 +115,7 @@ export function gatherResultsWith (env) { Assignments: pathOr([], ['Assignments']), Spawns: pathOr([], ['Spawns']), Output: pathOr(undefined, ['Output']), + Patches: pathOr([], ['Patches']), Error: pathOr(undefined, ['Error']) }) ) diff --git a/servers/cu/src/domain/lib/loadMessageMeta.js b/servers/cu/src/domain/lib/loadMessageMeta.js index 796cde6ea..9832ac7ca 100644 --- a/servers/cu/src/domain/lib/loadMessageMeta.js +++ b/servers/cu/src/domain/lib/loadMessageMeta.js @@ -49,7 +49,7 @@ function maybeCachedWith ({ locateProcess, findProcess }) { /** * @typedef Args - * @property {string} messageTxId - the transaction id of the message + * @property {string} messageUid - the transaction id of the message * * @typedef Result * @property {string} processId - the id of the process that the message is for @@ -81,11 +81,10 @@ export function loadMessageMetaWith (env) { * itself as the first Message without there necessarily being any more * Messages on the Process. */ - if (ctx.processId === ctx.messageTxId) { + if (ctx.processId === ctx.messageUid) { return loadProcess({ suUrl: trimSlash(url), - processId: ctx.processId, - messageTxId: ctx.messageTxId + processId: ctx.processId }) } @@ -96,7 +95,8 @@ export function loadMessageMetaWith (env) { return loadMessageMeta({ suUrl: trimSlash(url), processId: ctx.processId, - messageTxId: ctx.messageTxId + messageUid: ctx.messageUid, + body: ctx.body }) }) .map(ctxSchema.parse) diff --git a/servers/cu/src/domain/lib/loadMessageMeta.test.js b/servers/cu/src/domain/lib/loadMessageMeta.test.js index 43a0d48c1..7f3a4b0a2 100644 --- a/servers/cu/src/domain/lib/loadMessageMeta.test.js +++ b/servers/cu/src/domain/lib/loadMessageMeta.test.js @@ -21,7 +21,8 @@ describe('loadMessageMeta', () => { assert.deepStrictEqual(args, { suUrl: 'https://foo.bar', processId: 'process-123', - messageTxId: 'message-tx-123' + messageUid: 'message-tx-123', + body: undefined }) return { processId: 'process-123', timestamp: 1697574792000, nonce: 1 } }, @@ -40,7 +41,7 @@ describe('loadMessageMeta', () => { logger }) - const res = await loadMessageMeta({ processId: 'process-123', messageTxId: 'message-tx-123' }) + const res = await loadMessageMeta({ processId: 'process-123', messageUid: 'message-tx-123', body: undefined }) .toPromise() assert.deepStrictEqual(res, { processId: 'process-123', timestamp: 1697574792000, nonce: 1 }) @@ -69,7 +70,8 @@ describe('loadMessageMeta', () => { assert.deepStrictEqual(args, { suUrl: 'https://from.cache', processId: 'process-123', - messageTxId: 'message-tx-123' + messageUid: 'message-tx-123', + body: undefined }) return { processId: 'process-123', timestamp: 1697574792000, nonce: 1 } }, @@ -88,7 +90,7 @@ describe('loadMessageMeta', () => { logger }) - await loadMessageMeta({ processId: 'process-123', messageTxId: 'message-tx-123' }) + await loadMessageMeta({ processId: 'process-123', messageUid: 'message-tx-123', body: undefined }) .toPromise() }) @@ -122,7 +124,7 @@ describe('loadMessageMeta', () => { logger }) - const res = await loadMessageMeta({ processId: 'process-123', messageTxId: 'process-123' }) + const res = await loadMessageMeta({ processId: 'process-123', messageUid: 'process-123' }) .toPromise() assert.deepStrictEqual(res, { diff --git a/servers/cu/src/domain/lib/loadMessages.js b/servers/cu/src/domain/lib/loadMessages.js index 60f1ebef5..e088b1c53 100644 --- a/servers/cu/src/domain/lib/loadMessages.js +++ b/servers/cu/src/domain/lib/loadMessages.js @@ -487,9 +487,13 @@ function loadScheduledMessagesWith ({ loadMessages, logger }) { moduleOwner: ctx.moduleOwner, moduleTags: ctx.moduleTags, from: ctx.from, // could be undefined + fromOrdinate: ctx.ordinate, // could be undefined to: ctx.to, // could be undefined + toOrdinate: ctx.toOrdinate, // could be undefined assignmentId: ctx.mostRecentAssignmentId, - hashChain: ctx.mostRecentHashChain + hashChain: ctx.mostRecentHashChain, + isColdStart: ctx.isColdStart, + body: ctx.body }) ) } diff --git a/servers/cu/src/domain/lib/loadProcess.js b/servers/cu/src/domain/lib/loadProcess.js index 637c3e5ef..434bc2534 100644 --- a/servers/cu/src/domain/lib/loadProcess.js +++ b/servers/cu/src/domain/lib/loadProcess.js @@ -12,6 +12,12 @@ import { findEvaluationSchema, findLatestProcessMemorySchema, saveLatestProcessM * is always added to context */ const ctxSchema = z.object({ + isColdStart: z.boolean(), + /** + * The ordinate of the scheduled message we are + * evaluating up to. + */ + toOrdinate: z.coerce.number().nullish(), /** * The most recent result. This could be the most recent * cached result, or potentially initial cold start state @@ -87,12 +93,14 @@ function loadLatestEvaluationWith ({ findEvaluation, findLatestProcessMemory, sa ) return { + toOrdinate: ctx.ordinate, result: evaluation.output, from: evaluation.timestamp, ordinate: evaluation.ordinate, fromBlockHeight: evaluation.blockHeight, fromCron: evaluation.cron, - exact: true + exact: true, + isColdStart: false } }) .bimap(() => ctx, identity) @@ -126,13 +134,15 @@ function loadLatestEvaluationWith ({ findEvaluation, findLatestProcessMemory, sa found.ordinate === ctx.ordinate && found.cron === ctx.cron + const isColdStart = ['cold_start'].includes(found.src) + return of() .chain(() => { /** * Nothing to backfill in-memory cache with, * so simply noop */ - if (['cold_start'].includes(found.src)) return Resolved(found) + if (isColdStart) return Resolved(found) if (['memory'].includes(found.src) && !found.fromFile) return Resolved(found) /** @@ -164,6 +174,7 @@ function loadLatestEvaluationWith ({ findEvaluation, findLatestProcessMemory, sa .map(() => found) }) .map(() => ({ + toOrdinate: ctx.ordinate, result: { Memory: found.Memory }, @@ -180,7 +191,8 @@ function loadLatestEvaluationWith ({ findEvaluation, findLatestProcessMemory, sa * So we still signal to the caller whether an exact match * was found, for potential optimizations. */ - exact + exact, + isColdStart })) }) } diff --git a/servers/cu/src/domain/lib/loadProcess.test.js b/servers/cu/src/domain/lib/loadProcess.test.js index b71b2740a..ea81e7e4b 100644 --- a/servers/cu/src/domain/lib/loadProcess.test.js +++ b/servers/cu/src/domain/lib/loadProcess.test.js @@ -29,11 +29,13 @@ describe('loadProcess', () => { logger }) - const res = await loadProcess({ id: PROCESS, to: '1697574792000' }).toPromise() + const res = await loadProcess({ id: PROCESS, ordinate: '10', to: '1697574792000' }).toPromise() assert.deepStrictEqual(res.result, { Memory: null }) assert.equal(res.ordinate, '0') assert.equal(res.id, PROCESS) + assert.equal(res.toOrdinate, 10) + assert.ok(res.isColdStart) }) test('use exact match from db', async () => { @@ -68,7 +70,7 @@ describe('loadProcess', () => { logger }) - const res = await loadProcess({ id: PROCESS, to: 1697574792000 }).toPromise() + const res = await loadProcess({ id: PROCESS, ordinate: '10', to: 1697574792000 }).toPromise() assert.deepStrictEqual(res.result, cachedEvaluation.output) assert.deepStrictEqual(res.from, cachedEvaluation.timestamp) assert.deepStrictEqual(res.ordinate, cachedEvaluation.ordinate) @@ -77,6 +79,7 @@ describe('loadProcess', () => { assert.equal(res.mostRecentAssignmentId, undefined) assert.equal(res.mostRecentHashChain, undefined) assert.equal(res.id, PROCESS) + assert.equal(res.toOrdinate, 10) }) test('use latest in cache', async () => { @@ -101,7 +104,7 @@ describe('loadProcess', () => { logger }) - const res = await loadProcess({ id: PROCESS, to: 1697574792000 }).toPromise() + const res = await loadProcess({ id: PROCESS, ordinate: '10', to: 1697574792000 }).toPromise() assert.deepStrictEqual(res.from, cached.timestamp) assert.deepStrictEqual(res.ordinate, cached.ordinate) assert.deepStrictEqual(res.fromCron, cached.cron) @@ -109,6 +112,7 @@ describe('loadProcess', () => { assert.deepStrictEqual(res.mostRecentAssignmentId, cached.assignmentId) assert.deepStrictEqual(res.mostRecentHashChain, cached.hashChain) assert.equal(res.id, PROCESS) + assert.equal(res.toOrdinate, 10) }) test('backfill cache if latest from arweave or file', async () => { @@ -147,10 +151,10 @@ describe('loadProcess', () => { }) cached.src = 'record' - await loadProcess({ id: PROCESS, to: 1697574792000 }).toPromise() + await loadProcess({ id: PROCESS, ordinate: '10', to: 1697574792000 }).toPromise() cached.src = 'arweave' - await loadProcess({ id: PROCESS, to: 1697574792000 }).toPromise() + await loadProcess({ id: PROCESS, ordinate: '10', to: 1697574792000 }).toPromise() }) test('backfill cache if latest from memory drained to file', async () => { @@ -190,7 +194,7 @@ describe('loadProcess', () => { cached.src = 'memory' cached.fromFile = 'state-process-123.dat' - await loadProcess({ id: PROCESS, to: 1697574792000 }).toPromise() + await loadProcess({ id: PROCESS, ordinate: '10', to: 1697574792000 }).toPromise() }) test('bubble 425 if latestProcessMemory is later than requested message', async () => { diff --git a/servers/cu/src/domain/model.js b/servers/cu/src/domain/model.js index c8d77addf..bca646615 100644 --- a/servers/cu/src/domain/model.js +++ b/servers/cu/src/domain/model.js @@ -79,6 +79,7 @@ export const domainConfigSchema = z.object({ * The connection string to the database */ DB_URL: z.string().min(1, 'DB_URL must be set to the database connection string'), + HB_URL: z.string().url().nullish(), /** * The wallet for the CU */ @@ -114,6 +115,11 @@ export const domainConfigSchema = z.object({ * evaluation stream. */ EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD: positiveIntSchema, + /** + * If a process has this amount of eval time, then it will immediately create a Checkpoint at the end of the + * evaluation stream. + */ + EAGER_CHECKPOINT_EVAL_TIME_THRESHOLD: positiveIntSchema, /** * The number of workers to use for evaluating messages */ @@ -242,7 +248,7 @@ export const processSchema = z.object({ signature: z.string().nullish(), data: z.any().nullish(), anchor: z.string().nullish(), - owner: ownerSchema, + owner: ownerSchema.partial({ key: true }), tags: z.array(rawTagSchema), block: blockSchema }) @@ -454,6 +460,7 @@ export const evaluationSchema = z.object({ Assignments: z.array(z.any()).nullish(), Spawns: z.array(z.any()).nullish(), Output: z.any().nullish(), + Patches: z.array(z.any()).nullish(), GasUsed: z.number().nullish(), Error: z.any().nullish() }) diff --git a/servers/cu/src/domain/utils.js b/servers/cu/src/domain/utils.js index 671fdabaf..17043cc62 100644 --- a/servers/cu/src/domain/utils.js +++ b/servers/cu/src/domain/utils.js @@ -281,32 +281,53 @@ export async function busyIn (millis, p, busyFn) { } export function isLaterThan (eval1, eval2) { - const t1 = `${eval1.timestamp || 0}` - const t2 = `${eval2.timestamp || 0}` /** - * timestamps are equal some might be two crons on overlapping interval, - * so compare the crons + * One of the messages is a cron produced + * message, so we must compare timestamps + * instead */ - if (t2 === t1) return (eval2.cron || '') > (eval1.cron || '') + if (eval2.cron || eval1.cron) { + const t1 = parseInt(`${eval1.timestamp || 0}`) + const t2 = parseInt(`${eval2.timestamp || 0}`) + /** + * timestamps are equal some might be two crons on overlapping interval, + * so compare the crons + */ + if (t2 === t1) return (eval2.cron || '') > (eval1.cron || '') + return t2 > t1 + } - return t2 > t1 + const o1 = parseInt(eval1.ordinate) + const o2 = parseInt(eval2.ordinate) + return o2 > o1 } export function isEarlierThan (eval1, eval2) { - const t1 = `${eval1.timestamp || 0}` - const t2 = `${eval2.timestamp || 0}` /** - * timestamps are equal some might be two crons on overlapping interval, - * so compare the crons + * One of the messages is a cron produced + * message, so we must compare timestamps + * instead */ - if (t2 === t1) return (eval2.cron || '') < (eval1.cron || '') + if (eval2.cron || eval1.cron) { + const t1 = parseInt(`${eval1.timestamp || 0}`) + const t2 = parseInt(`${eval2.timestamp || 0}`) + /** + * timestamps are equal some might be two crons on overlapping interval, + * so compare the crons + */ + if (t2 === t1) return (eval2.cron || '') < (eval1.cron || '') + return t2 > t1 + } - return t2 < t1 + const o1 = parseInt(eval1.ordinate) + const o2 = parseInt(eval2.ordinate) + + return o2 < o1 } export function isEqualTo (eval1, eval2) { - const t1 = `${eval1.timestamp}` - const t2 = `${eval2.timestamp}` + const t1 = parseInt(`${eval1.timestamp}`) + const t2 = parseInt(`${eval2.timestamp}`) return t2 === t1 && (eval1.ordinate === eval2.ordinate) && @@ -535,6 +556,12 @@ const cache = new LRUCache({ max: 1000 }) * @returns {string} */ export const addressFrom = ({ address, key }) => { + /** + * If we do not have the key, then the address + * MUST be what we use + */ + if (!key) return address + const cKey = `${address}-${key}` if (cache.has(cKey)) return cache.get(cKey) @@ -554,9 +581,3 @@ export const addressFrom = ({ address, key }) => { return _address } - -export function readableToAsyncGenerator (stream) { - return (async function * () { - for await (const d of stream) yield d - })() -} diff --git a/servers/cu/src/domain/utils.test.js b/servers/cu/src/domain/utils.test.js index d2fd3874c..83837dffa 100644 --- a/servers/cu/src/domain/utils.test.js +++ b/servers/cu/src/domain/utils.test.js @@ -225,19 +225,19 @@ describe('utils', () => { }) describe('isLaterThan', () => { - const now = new Date() + const now = new Date().getTime() const tenSecondsAgo = now - 10000 test('should return whether the right is later than the left', () => { - // later timestamp - assert.ok(isLaterThan({ timestamp: tenSecondsAgo }, { timestamp: now })) + // later ordinate + assert.ok(isLaterThan({ ordinate: '9' }, { ordinate: '10' })) // later cron (when left has no cron) assert.ok(isLaterThan({ timestamp: tenSecondsAgo }, { timestamp: tenSecondsAgo, cron: '0-1-minute' })) // later cron assert.ok(isLaterThan({ timestamp: tenSecondsAgo, cron: '0-1-minute' }, { timestamp: tenSecondsAgo, cron: '1-1-minute' })) - // earlier timestamp - assert.ok(!isLaterThan({ timestamp: now }, { timestamp: tenSecondsAgo })) + // earlier ordinate + assert.ok(!isLaterThan({ ordinate: '10' }, { ordinate: '9' })) // earlier cron (when right has no cron) assert.ok(!isLaterThan({ timestamp: tenSecondsAgo, cron: '0-1-minute' }, { timestamp: tenSecondsAgo })) // earlier cron @@ -246,19 +246,19 @@ describe('utils', () => { }) describe('isEarlierThan', () => { - const now = new Date() + const now = new Date().getTime() const tenSecondsAgo = now - 10000 test('should return whether the right is later than the left', () => { - // later timestamp - assert.ok(!isEarlierThan({ timestamp: tenSecondsAgo }, { timestamp: now })) + // later ordinate + assert.ok(!isEarlierThan({ ordinate: '9' }, { ordinate: '10' })) // later cron (when left has no cron) assert.ok(!isEarlierThan({ timestamp: tenSecondsAgo }, { timestamp: tenSecondsAgo, cron: '0-1-minute' })) // later cron assert.ok(!isEarlierThan({ timestamp: tenSecondsAgo, cron: '0-1-minute' }, { timestamp: tenSecondsAgo, cron: '1-1-minute' })) - // earlier timestamp - assert.ok(isEarlierThan({ timestamp: now }, { timestamp: tenSecondsAgo })) + // earlier ordinate + assert.ok(isEarlierThan({ ordinate: '10' }, { ordinate: '9' })) // earlier cron (when right has no cron) assert.ok(isEarlierThan({ timestamp: tenSecondsAgo, cron: '0-1-minute' }, { timestamp: tenSecondsAgo })) // earlier cron @@ -266,8 +266,8 @@ describe('utils', () => { }) }) - describe('isEarlierThan', () => { - const now = new Date() + describe('isEqualTo', () => { + const now = new Date().getTime() const tenSecondsAgo = now - 10000 test('should return whether the right is equal to the left', () => { diff --git a/servers/cu/src/effects/ao-http/index.js b/servers/cu/src/effects/ao-http/index.js new file mode 100644 index 000000000..2f226f852 --- /dev/null +++ b/servers/cu/src/effects/ao-http/index.js @@ -0,0 +1,94 @@ +import { pipe, pipeWith, unapply } from 'ramda' + +import Fastify from 'fastify' +import FastifyMiddie from '@fastify/middie' +import cors from 'cors' +import helmet from 'helmet' + +import { withMetricRoutes } from './routes/metrics.js' +import { withStateRoutes } from './routes/state.js' +import { withResultRoutes } from './routes/result.js' +import { withDryRunRoutes } from './routes/dryRun.js' +import { withResultsRoutes } from './routes/results.js' +import { withCronRoutes } from './routes/cron.js' +import { withHealthcheckRoutes } from './routes/healthcheck.js' + +const pipeP = unapply(pipeWith((fn, p) => Promise.resolve(p).then(fn))) + +const withRoutes = pipe( + withHealthcheckRoutes, + withStateRoutes, + withResultRoutes, + withDryRunRoutes, + withResultsRoutes, + withCronRoutes, + withMetricRoutes +) + +export const createAoHttp = async ({ logger: _logger, domain, ...config }) => { + const logger = _logger.child('ao-http') + + /** + * Fastify wraps the Node native req and res with Request and Reply + * only after Middleware (added using use()) is ran. Ergo, Middleware can + * not be used to make things accessible on the top level Request and Reply. + * + * In order to do that, Fastify recommends using a preHandler hook, which + * fires after the wrapping by Fastify has occurred, allowing us to augment + * the top level objects received by routes. + * + * See https://fastify.dev/docs/v3.29.x/Reference/Middleware/#middleware + * + * So we use a preHandler hook to inject the domain, config, and logger + * to all routes. + */ + const withDeps = (app) => app.addHook('preHandler', (req, _res, next) => { + req.domain = domain + req.config = config + req.logger = logger + next() + }) + + return pipeP( + /** + * Allow us to use some simple express middleware + */ + (app) => app.register(FastifyMiddie).then(() => app), + (app) => app.use(helmet()), + (app) => app.use(cors()), + withDeps, + withRoutes, + (app) => { + async function start () { + return new Promise((resolve) => { + /** + * See https://github.com/fastify/fastify?tab=readme-ov-file#note + */ + app.listen({ port: config.port, host: '0.0.0.0' }, () => { + logger(`Server is running on http://localhost:${config.port}`) + logger(`Server in unit mode: "${config.UNIT_MODE}"`) + resolve() + }) + }) + } + + async function stop () { + return new Promise((resolve, reject) => { + logger('Received SIGTERM. Gracefully shutting down server...') + app.close( + () => { + logger('Server shut down.') + resolve() + }, + (e) => { + logger('Failed to shut down server!', e) + reject(e) + } + ) + }) + } + + return { start, stop } + } + )(Fastify({ bodyLimit: 10485760 })) +} diff --git a/servers/cu/src/routes/cron.js b/servers/cu/src/effects/ao-http/routes/cron.js similarity index 90% rename from servers/cu/src/routes/cron.js rename to servers/cu/src/effects/ao-http/routes/cron.js index f79be4b2d..b65728af8 100644 --- a/servers/cu/src/routes/cron.js +++ b/servers/cu/src/effects/ao-http/routes/cron.js @@ -1,8 +1,10 @@ import { always, compose, identity } from 'ramda' import { z } from 'zod' -import { withMetrics, withMiddleware, withProcessRestrictionFromPath } from './middleware/index.js' +import { withErrorHandler } from './middleware/withErrorHandler.js' import { withCuMode } from './middleware/withCuMode.js' +import { withMetrics } from './middleware/withMetrics.js' +import { withProcessRestrictionFromPath } from './middleware/withProcessRestriction.js' /** * TODO: could be moved into a route utils or middleware @@ -37,7 +39,7 @@ export const withCronRoutes = app => { app.get( '/cron/:processId', compose( - withMiddleware, + withErrorHandler, withCuMode, withProcessRestrictionFromPath, withMetrics({ tracesFrom: (req) => ({ process_id: req.params.processId }) }), diff --git a/servers/cu/src/routes/dryRun.js b/servers/cu/src/effects/ao-http/routes/dryRun.js similarity index 69% rename from servers/cu/src/routes/dryRun.js rename to servers/cu/src/effects/ao-http/routes/dryRun.js index a694391b8..89496db68 100644 --- a/servers/cu/src/routes/dryRun.js +++ b/servers/cu/src/effects/ao-http/routes/dryRun.js @@ -1,12 +1,15 @@ import { always, compose } from 'ramda' import { z } from 'zod' -import { busyIn } from '../domain/utils.js' -import { withMetrics, withMiddleware, withProcessRestrictionFromQuery } from './middleware/index.js' +import { busyIn } from '../../../domain/utils.js' + +import { withErrorHandler } from './middleware/withErrorHandler.js' +import { withMetrics } from './middleware/withMetrics.js' +import { withProcessRestrictionFromQuery } from './middleware/withProcessRestriction.js' const inputSchema = z.object({ processId: z.string().min(1, 'a process-id query parameter is required'), - messageTxId: z.string().min(1, 'to must be a transaction id').optional(), + messageUid: z.string().min(1, 'to must be a transaction id').optional(), maxProcessAge: z.coerce.number().nullish(), dryRun: z.object({ Id: z.string().nullish(), @@ -28,25 +31,25 @@ export const withDryRunRoutes = app => { app.post( '/dry-run', compose( - withMiddleware, + withErrorHandler, withMetrics({ tracesFrom: (req) => ({ process_id: req.query['process-id'] }) }), withProcessRestrictionFromQuery, always(async (req, res) => { const { headers: { 'x-max-age': maxProcessAge }, - query: { 'process-id': processId, to: messageTxId }, + query: { 'process-id': processId, to: messageUid }, body, domain: { BUSY_THRESHOLD, apis: { dryRun } } } = req - const input = inputSchema.parse({ processId, messageTxId, maxProcessAge, dryRun: body }) + const input = inputSchema.parse({ processId, messageUid, maxProcessAge, dryRun: body }) await busyIn( BUSY_THRESHOLD, dryRun(input).toPromise(), () => { res.status(202) - return { message: `Evaluation of process "${input.processId}" to "${input.messageTxId || 'latest'}" is in progress.` } + return { message: `Evaluation of process "${input.processId}" to "${input.messageUid || 'latest'}" is in progress.` } } ).then((output) => res.send(output)) }) diff --git a/servers/cu/src/routes/healthcheck.js b/servers/cu/src/effects/ao-http/routes/healthcheck.js similarity index 79% rename from servers/cu/src/routes/healthcheck.js rename to servers/cu/src/effects/ao-http/routes/healthcheck.js index db62ef8e3..05cf287cb 100644 --- a/servers/cu/src/routes/healthcheck.js +++ b/servers/cu/src/effects/ao-http/routes/healthcheck.js @@ -1,13 +1,13 @@ import { always, compose } from 'ramda' -import { withMiddleware } from './middleware/index.js' +import { withErrorHandler } from './middleware/withErrorHandler.js' export const withHealthcheckRoutes = (app) => { // healthcheck app.get( '/', compose( - withMiddleware, + withErrorHandler, always(async (req, res) => { const { domain: { apis: { healthcheck } } } = req diff --git a/servers/cu/src/routes/metrics.js b/servers/cu/src/effects/ao-http/routes/metrics.js similarity index 69% rename from servers/cu/src/routes/metrics.js rename to servers/cu/src/effects/ao-http/routes/metrics.js index d75fe8273..f4f5fd5a0 100644 --- a/servers/cu/src/routes/metrics.js +++ b/servers/cu/src/effects/ao-http/routes/metrics.js @@ -1,20 +1,20 @@ import { always, compose } from 'ramda' -import { withMiddleware } from './middleware/index.js' -import { config } from '../config.js' +import { withErrorHandler } from './middleware/withErrorHandler.js' export const withMetricRoutes = (app) => { - if (!config.ENABLE_METRICS_ENDPOINT) return app - app.get( '/metrics', compose( - withMiddleware, + withErrorHandler, always(async (req, res) => { const { + config, domain: { apis: { metrics } } } = req + if (!config.ENABLE_METRICS_ENDPOINT) return res.status(404).send('Not Found') + await metrics.compute() .toPromise() .then((output) => { diff --git a/servers/cu/src/effects/ao-http/routes/middleware/withCuMode.js b/servers/cu/src/effects/ao-http/routes/middleware/withCuMode.js new file mode 100644 index 000000000..7f026ad7e --- /dev/null +++ b/servers/cu/src/effects/ao-http/routes/middleware/withCuMode.js @@ -0,0 +1,10 @@ +const withUnitMode = (modes) => (handler) => (req, res, next) => { + const { UNIT_MODE } = req.config + + if (!modes.includes(UNIT_MODE)) return res.status(404).send('Not Found') + + return handler(req, res, next) +} + +export const withCuMode = withUnitMode(['cu', 'hbu']) +export const withRuMode = withUnitMode(['ru']) diff --git a/servers/cu/src/routes/middleware/withErrorHandler.js b/servers/cu/src/effects/ao-http/routes/middleware/withErrorHandler.js similarity index 88% rename from servers/cu/src/routes/middleware/withErrorHandler.js rename to servers/cu/src/effects/ao-http/routes/middleware/withErrorHandler.js index 0c522c6cb..f6048fa9a 100644 --- a/servers/cu/src/routes/middleware/withErrorHandler.js +++ b/servers/cu/src/effects/ao-http/routes/middleware/withErrorHandler.js @@ -1,4 +1,4 @@ -import { errFrom } from '../../domain/utils.js' +import { errFrom } from '../../../../domain/utils.js' /** * A middleware that simply calls the next handler in the chain. @@ -13,9 +13,10 @@ export const withErrorHandler = (handler) => (req, res) => { return Promise.resolve() .then(() => handler(req, res)) .catch((err) => { - const { domain: { logger } } = req + const { logger } = req logger('An error bubbled to the top handler. Formatting and relaying to client:', err) + console.error(err) const formatted = errFrom(err) if (res.raw.writableEnded) return diff --git a/servers/cu/src/routes/middleware/withInMemoryCache.js b/servers/cu/src/effects/ao-http/routes/middleware/withInMemoryCache.js similarity index 70% rename from servers/cu/src/routes/middleware/withInMemoryCache.js rename to servers/cu/src/effects/ao-http/routes/middleware/withInMemoryCache.js index 1c232a5e1..b79a49712 100644 --- a/servers/cu/src/routes/middleware/withInMemoryCache.js +++ b/servers/cu/src/effects/ao-http/routes/middleware/withInMemoryCache.js @@ -1,7 +1,5 @@ import { LRUCache } from 'lru-cache' -import { logger as _logger } from '../../logger.js' - /** * A middleware that provides a response from an In-Memory LRUCache. * @@ -33,9 +31,26 @@ export const withInMemoryCache = ({ sizeCalculation: (value) => JSON.stringify(value).length }) - const logger = _logger.child('InMemoryCache') + let logger + + const response = (res, [result, headers = []], isCached = false) => { + if (headers) { + headers.forEach(([name, value]) => res.header(name, value)) + } + + /** + * cached on the endpoint edge + */ + if (isCached) res.header('cu-cached', true) + /** + * TODO: maybe add Cache-Control headers as well, + * so that clients can also cache the request + */ + res.send(result) + } return async (req, res) => { + logger = logger || req.logger.child('InMemoryCache') const key = keyer(req) /** @@ -47,16 +62,12 @@ export const withInMemoryCache = ({ const cached = cache.get(key, { status }) logger(`"%s" for key ${key}`, status.get.toUpperCase()) - if (cached) return res.send(cached) + if (cached) return response(res, cached, true) await loader({ req, res }) - .then(([result, noCache]) => { - if (!noCache) cache.set(key, result) - /** - * TODO: maybe add Cache-Control headers as well, - * so that clients can also cache the request - */ - res.send(result) + .then(([result, headers, noCache = false]) => { + if (!noCache) cache.set(key, [result, headers]) + response(res, [result, headers]) }) .catch((err) => { if (evict(req, err)) { diff --git a/servers/cu/src/routes/middleware/withMetrics.js b/servers/cu/src/effects/ao-http/routes/middleware/withMetrics.js similarity index 96% rename from servers/cu/src/routes/middleware/withMetrics.js rename to servers/cu/src/effects/ao-http/routes/middleware/withMetrics.js index 99dd085b8..b9d107027 100644 --- a/servers/cu/src/routes/middleware/withMetrics.js +++ b/servers/cu/src/effects/ao-http/routes/middleware/withMetrics.js @@ -1,6 +1,6 @@ import { always } from 'ramda' -import { histogramWith, summaryWith } from '../../effects/metrics.js' +import { histogramWith, summaryWith } from '../../../metrics.js' const histogram = histogramWith()({ name: 'http_request_duration_seconds', diff --git a/servers/cu/src/routes/middleware/withProcessRestriction.js b/servers/cu/src/effects/ao-http/routes/middleware/withProcessRestriction.js similarity index 100% rename from servers/cu/src/routes/middleware/withProcessRestriction.js rename to servers/cu/src/effects/ao-http/routes/middleware/withProcessRestriction.js diff --git a/servers/cu/src/effects/ao-http/routes/result.js b/servers/cu/src/effects/ao-http/routes/result.js new file mode 100644 index 000000000..d4ec4c837 --- /dev/null +++ b/servers/cu/src/effects/ao-http/routes/result.js @@ -0,0 +1,111 @@ +import { compose, pipe, prop, sum, values } from 'ramda' +import { z } from 'zod' + +import { busyIn } from '../../../domain/utils.js' + +import { withInMemoryCache } from './middleware/withInMemoryCache.js' +import { withErrorHandler } from './middleware/withErrorHandler.js' +import { withCuMode } from './middleware/withCuMode.js' +import { withMetrics } from './middleware/withMetrics.js' +import { withProcessRestrictionFromQuery } from './middleware/withProcessRestriction.js' + +const inputSchema = z.object({ + messageUid: z.string().min(1, 'a message unique identifier is required'), + processId: z.string().min(1, 'a process-id query parameter is required') +}) + +const gatherStatCounts = pipe(prop('messages'), values, sum) + +export const withResultRoutes = app => { + const route = + compose( + withErrorHandler, + withCuMode, + withProcessRestrictionFromQuery, + withMetrics({ tracesFrom: (req) => ({ process_id: req.query['process-id'] }) }), + withInMemoryCache({ + keyer: (req) => { + const { + params: { messageUid }, + query: { 'process-id': processId } + } = req + return `${processId}:${messageUid}` + }, + loader: async ({ req, res }) => { + const { + params: { messageUid }, + /** + * Client may set the 'no-busy' query parameter to any value + * to disable the busy response. + * + * Useful for MUs that would rather wait on the evaluation response + */ + query: { 'process-id': processId, 'no-busy': noBusy }, + domain: { BUSY_THRESHOLD, apis: { readResult } }, + body + } = req + + const input = inputSchema.parse({ messageUid, processId }) + + return busyIn( + noBusy ? 0 : BUSY_THRESHOLD, + readResult({ ...input, body }) + .map((result) => { + const { output, last, isColdStart, exact, stats } = result + + /** + * Set various headers that contain metadata concerning the eval + * stream + */ + const cacheableHeaders = [] + + if (last.id) cacheableHeaders.push(['Message', last.id]) + cacheableHeaders.push(['timestamp', last.timestamp]) + cacheableHeaders.push(['block-height', last.blockHeight]) + cacheableHeaders.push(['slot', last.ordinate]) + + const aoTypes = [ + ['timestamp', 'integer'], + ['block-height', 'integer'], + ['slot', 'integer'], + ['cold-start', 'atom'], + ['cu-cached', 'atom'], + ['eval-stream-length', 'integer'] + ].map(t => t.join('=')) + cacheableHeaders.push(['ao-types', aoTypes.join(',')]) + + /** + * Some headers ought not be cached on the edge, + * as they have to do with this specific evaluation stream + * -- this specific request + * + * So we set them here, so that they are not included as part + * of subsequent cached responses, but still included + * on this specific response + */ + res.header('cold-start', isColdStart) + res.header('cu-cached', exact) + res.header('eval-stream-length', gatherStatCounts(stats)) + + return [output, cacheableHeaders] + }) + .toPromise(), + () => { + res.status(202) + return [ + { message: `Evaluation of process "${input.processId}" to "${input.messageUid || 'latest'}" is in progress.` }, + /** + * Don't store the busy message in the In-Memory cache + */ + true // noCache + ] + } + ) + } + }) + ) + + app.post('/result/:messageUid', route()) + app.get('/result/:messageUid', route()) + return app +} diff --git a/servers/cu/src/routes/results.js b/servers/cu/src/effects/ao-http/routes/results.js similarity index 86% rename from servers/cu/src/routes/results.js rename to servers/cu/src/effects/ao-http/routes/results.js index 9f5a8d456..548d57f73 100644 --- a/servers/cu/src/routes/results.js +++ b/servers/cu/src/effects/ao-http/routes/results.js @@ -1,7 +1,10 @@ import { always, compose, identity } from 'ramda' import { z } from 'zod' -import { withMetrics, withMiddleware, withProcessRestrictionFromPath, withCuMode } from './middleware/index.js' +import { withErrorHandler } from './middleware/withErrorHandler.js' +import { withCuMode } from './middleware/withCuMode.js' +import { withProcessRestrictionFromPath } from './middleware/withProcessRestriction.js' +import { withMetrics } from './middleware/withMetrics.js' /** * TODO: could be moved into a route utils or middleware @@ -46,7 +49,7 @@ export const withResultsRoutes = app => { app.get( '/results/:processId', compose( - withMiddleware, + withErrorHandler, withCuMode, withProcessRestrictionFromPath, withMetrics({ tracesFrom: (req) => ({ process_id: req.params.processId }) }), diff --git a/servers/cu/src/routes/state.js b/servers/cu/src/effects/ao-http/routes/state.js similarity index 80% rename from servers/cu/src/routes/state.js rename to servers/cu/src/effects/ao-http/routes/state.js index fe8ffb853..102e5809b 100644 --- a/servers/cu/src/routes/state.js +++ b/servers/cu/src/effects/ao-http/routes/state.js @@ -1,8 +1,12 @@ import { always, compose } from 'ramda' import { z } from 'zod' -import { arrayBufferFromMaybeView, busyIn } from '../domain/utils.js' -import { withMetrics, withMiddleware, withProcessRestrictionFromPath, withCuMode } from './middleware/index.js' +import { arrayBufferFromMaybeView, busyIn } from '../../../domain/utils.js' + +import { withErrorHandler } from './middleware/withErrorHandler.js' +import { withCuMode } from './middleware/withCuMode.js' +import { withProcessRestrictionFromPath } from './middleware/withProcessRestriction.js' +import { withMetrics } from './middleware/withMetrics.js' const inputSchema = z.object({ processId: z.string().min(1, 'an ao process id is required'), @@ -14,7 +18,7 @@ export const withStateRoutes = (app) => { app.get( '/state/:processId', compose( - withMiddleware, + withErrorHandler, withCuMode, withProcessRestrictionFromPath, withMetrics({ tracesFrom: (req) => ({ process_id: req.params.processId }) }), diff --git a/servers/cu/src/effects/ao-process.js b/servers/cu/src/effects/ao-process.js index 197417bf1..a571c3833 100644 --- a/servers/cu/src/effects/ao-process.js +++ b/servers/cu/src/effects/ao-process.js @@ -1312,9 +1312,11 @@ export function findLatestProcessMemoryWith ({ } } -export function saveLatestProcessMemoryWith ({ cache, logger, saveCheckpoint, EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD }) { - return async ({ processId, moduleId, assignmentId, messageId, hashChain, timestamp, epoch, nonce, ordinate, cron, blockHeight, Memory, gasUsed }) => { +export function saveLatestProcessMemoryWith ({ cache, logger, saveCheckpoint, EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD, EAGER_CHECKPOINT_EVAL_TIME_THRESHOLD }) { + return async ({ processId, moduleId, assignmentId, messageId, hashChain, timestamp, epoch, nonce, ordinate, cron, blockHeight, Memory, gasUsed, evalTime }) => { const cached = cache.get(processId) + let incrementedGasUsed = pathOr(BigInt(0), ['evaluation', 'gasUsed'], cached) + let incrementedEvalTime = pathOr(0, ['evaluation', 'evalTime'], cached) /** * Ensure that we are always caching a Buffer and not a TypedArray @@ -1331,7 +1333,6 @@ export function saveLatestProcessMemoryWith ({ cache, logger, saveCheckpoint, EA * the value currently cached, so overwrite it */ - let incrementedGasUsed = pathOr(BigInt(0), ['evaluation', 'gasUsed'], cached) /** * The cache is being reseeded ie. Memory was reloaded from a file */ @@ -1353,12 +1354,22 @@ export function saveLatestProcessMemoryWith ({ cache, logger, saveCheckpoint, EA processId, { messageId, timestamp, ordinate, cron, blockHeight } ) + incrementedGasUsed = pipe( pathOr(BigInt(0), ['evaluation', 'gasUsed']), add(gasUsed || BigInt(0)) )(cached) + + incrementedEvalTime = pipe( + pathOr(0, ['evaluation', 'evalTime']), + add(evalTime || 0) + )(cached) } + const gasThresholdReached = incrementedGasUsed && EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD && incrementedGasUsed >= EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD + const evalTimeThresholdReached = incrementedEvalTime && EAGER_CHECKPOINT_EVAL_TIME_THRESHOLD && incrementedEvalTime >= EAGER_CHECKPOINT_EVAL_TIME_THRESHOLD + const eitherThresholdReached = gasThresholdReached || evalTimeThresholdReached + const evaluation = { processId, moduleId, @@ -1378,16 +1389,17 @@ export function saveLatestProcessMemoryWith ({ cache, logger, saveCheckpoint, EA */ encoding: undefined, cron, - gasUsed: incrementedGasUsed < EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD ? incrementedGasUsed : 0 + gasUsed: eitherThresholdReached ? 0 : incrementedGasUsed, + evalTime: eitherThresholdReached ? 0 : incrementedEvalTime } // cache.set(processId, { Memory: zipped, evaluation }) cache.set(processId, { Memory, evaluation }) - if (!incrementedGasUsed || !EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD || incrementedGasUsed < EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD) return + if (!eitherThresholdReached) return /** * Eagerly create the Checkpoint on the next event queue drain */ - setImmediate(() => { + if (gasThresholdReached) { logger( 'Eager Checkpoint Accumulated Gas Threshold of "%d" gas used met when evaluating process "%s" up to "%j" -- "%d" gas used. Eagerly creating a Checkpoint...', EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD, @@ -1395,20 +1407,28 @@ export function saveLatestProcessMemoryWith ({ cache, logger, saveCheckpoint, EA { messageId, timestamp, ordinate, cron, blockHeight }, incrementedGasUsed ) + } else { + logger( + 'Eager Checkpoint Accumulated Eval Time Threshold of "%d" ms eval time met when evaluating process "%s" up to "%j" -- "%d" ms eval time. Eagerly creating a Checkpoint...', + EAGER_CHECKPOINT_EVAL_TIME_THRESHOLD, + processId, + { messageId, timestamp, ordinate, cron, blockHeight }, + incrementedEvalTime + ) + } - /** + /** * Memory will always be defined at this point, so no reason * to pass File */ - return saveCheckpoint({ Memory, ...evaluation }) - .catch((err) => { - logger( - 'Error occurred when creating Eager Checkpoint for evaluation "%j". Skipping...', - evaluation, - err - ) - }) - }) + return saveCheckpoint({ Memory, ...evaluation }) + .catch((err) => { + logger( + 'Error occurred when creating Eager Checkpoint for evaluation "%j". Skipping...', + evaluation, + err + ) + }) } } diff --git a/servers/cu/src/effects/ao-process.test.js b/servers/cu/src/effects/ao-process.test.js index 16a4414b4..730ab4080 100644 --- a/servers/cu/src/effects/ao-process.test.js +++ b/servers/cu/src/effects/ao-process.test.js @@ -1342,10 +1342,10 @@ describe('ao-process', () => { assignmentId: 'assignment-123', hashChain: 'hashchain-123', epoch: 0, - nonce: 11, + nonce: 14, timestamp: now + 1000, blockHeight: 123, - ordinate: '11', + ordinate: '14', encoding: 'gzip', gasUsed: BigInt(100) } @@ -1380,7 +1380,7 @@ describe('ao-process', () => { Memory, evaluation: cachedEvalFuture }), - set: () => assert.fail('should not call if found in cache') + set: (f) => assert.fail('should not call if later found in cache') }, logger, saveCheckpoint: () => assert.fail('should not call if found in cache'), diff --git a/servers/cu/src/effects/ao-su.js b/servers/cu/src/effects/ao-su.js index 627b61443..d702933a5 100644 --- a/servers/cu/src/effects/ao-su.js +++ b/servers/cu/src/effects/ao-su.js @@ -267,7 +267,7 @@ export const loadMessagesWith = ({ hashChain, fetch, logger: _logger, pageSize } */ const isHashChainValidationEnabled = processBlock.height >= 1440000 if (!isHashChainValidationEnabled) { - logger('HashChain validation disabled for old process "%s" at block [%j]', [processId, processBlock]) + logger('HashChain validation disabled for old process "%s" at block [%j]', processId, processBlock) } // Set this to simulate a stream error @@ -433,15 +433,15 @@ export const loadMessageMetaWith = ({ fetch, logger }) => { const mapMeta = ifElse(has('assignment'), meta, legacyMeta) - return async ({ suUrl, processId, messageTxId }) => { + return async ({ suUrl, processId, messageUid }) => { return backoff( - () => fetch(`${suUrl}/${messageTxId}?process-id=${processId}`, { method: 'GET' }).then(okRes), - { maxRetries: 5, delay: 500, log: logger, name: `loadMessageMeta(${JSON.stringify({ suUrl, processId, messageTxId })})` } + () => fetch(`${suUrl}/${messageUid}?process-id=${processId}`, { method: 'GET' }).then(okRes), + { maxRetries: 5, delay: 500, log: logger, name: `loadMessageMeta(${JSON.stringify({ suUrl, processId, messageUid })})` } ) .catch(async (err) => { logger( 'Error Encountered when loading message meta for message "%s" to process "%s" from SU "%s"', - messageTxId, processId, suUrl + messageUid, processId, suUrl ) throw new Error(`Error Encountered when loading message from Scheduler Unit: ${await strFromFetchError(err)}`) }) diff --git a/servers/cu/src/effects/ao-su.test.js b/servers/cu/src/effects/ao-su.test.js index a6182c2e6..d0ff3488e 100644 --- a/servers/cu/src/effects/ao-su.test.js +++ b/servers/cu/src/effects/ao-su.test.js @@ -245,7 +245,7 @@ describe('ao-su', () => { const res = await loadMessageMeta({ suUrl: 'https://foo.bar', processId: 'process-123', - messageTxId: 'message-tx-123' + messageUid: 'message-tx-123' }) assert.deepStrictEqual(res, { @@ -286,7 +286,7 @@ describe('ao-su', () => { const res = await loadMessageMeta({ suUrl: 'https://foo.bar', processId: 'process-123', - messageTxId: 'message-tx-123' + messageUid: 'message-tx-123' }) assert.ok(!!count) @@ -324,7 +324,7 @@ describe('ao-su', () => { const res = await loadMessageMeta({ suUrl: 'https://foo.bar', processId: 'process-123', - messageTxId: 'message-tx-123' + messageUid: 'message-tx-123' }) assert.ok(!!count) diff --git a/servers/cu/src/effects/hb/index.js b/servers/cu/src/effects/hb/index.js new file mode 100644 index 000000000..b769e49ce --- /dev/null +++ b/servers/cu/src/effects/hb/index.js @@ -0,0 +1,509 @@ +/* eslint-disable camelcase */ +import { Readable, Transform } from 'node:stream' + +import { always, applySpec, filter, isNil, isNotNil, juxt, last, mergeAll, nth, path, pathOr, pipe, prop } from 'ramda' +import DataLoader from 'dataloader' + +import { backoff, mapForwardedBy, mapFrom, parseTags, strFromFetchError } from '../../domain/utils.js' + +const okRes = (res) => { + if (res.ok) return res + throw res +} + +const resToJson = (res) => { + if (res.edges) { + return res + } + /** + * Janky extra parse b/c HB currently sends back a string + * that contains a JSON object, that must be parsed further. + * + * TODO: remove later. + */ + return res.json().then(maybeJSON => typeof maybeJSON === 'object' + ? maybeJSON + : JSON.parse(maybeJSON) + ) +} + +/** + * GET /...?target=ProcID[&from=X&to=y]&accept=application/aos-2 + */ +const toParams = ({ processId, from, to, pageSize }) => + new URLSearchParams(filter( + isNotNil, + { target: processId, from, to, limit: pageSize, accept: 'application/aos-2' } + )) + +const nodeAt = (idx) => pipe(path(['edges']), nth(idx), path(['node'])) + +/** + * TODO: eventually actually locate the scheduler + * For now, just always returning the HB_URL + */ +export const locateProcessWith = ({ fetch, HB_URL }) => { + return async ({ processId }) => { + return { url: HB_URL } + } +} + +export const isHashChainValidWith = ({ hashChain }) => async (prev, scheduled) => { + const { assignmentId: prevAssignmentId, hashChain: prevHashChain } = prev + const { message } = scheduled + const actual = message['Hash-Chain'] + + /** + * Depending on the source of the hot-start, there may not be + * a prev assignmentId and hashChain + */ + if (!prevAssignmentId || !prevHashChain) return !!actual + + const expected = await hashChain(prevHashChain, prevAssignmentId) + return expected === actual +} + +export const mapNode = pipe( + juxt([ + // derived from assignment + pipe( + path(['assignment']), + (assignment) => { + const tags = parseTags(assignment.Tags) + + return applySpec({ + /** + * The assignment id is needed in order to compute + * and verify the hash chain + */ + AssignmentId: always(assignment.Id), + /** + * There could be multiple Exclude tags, + * but parseTags will accumulate them into an array, + * + * which is what we want + */ + Exclude: path(['Exclude']), + /** + * Data from the assignment, to be placed + * on the message + */ + message: applySpec({ + Target: path(['Process']), + Epoch: pipe(path(['Epoch']), parseInt), + Nonce: pipe(path(['Slot'])(tags) ? path(['Slot']) : path(['Nonce']), parseInt), + Timestamp: pipe(path(['Timestamp']), parseInt), + 'Block-Height': pipe( + /** + * Returns a left padded integer like '000001331218' + * + * So use parseInt to convert it into a number + */ + path(['Block-Height']), + parseInt + ), + 'Hash-Chain': path(['Hash-Chain']) + }) + })(tags) + } + ), + // derived from message + pipe( + path(['message']), + (message) => { + /** + * No message, or only Id, meaning this is an Assignment for an existing message + * on chain, to be hydrated later (see hydrateMessages) + */ + if (Object.keys(message).length === 1) return { Id: message.Id } + + /** + * TODO: HB SU does not return the public key + * and so we cannot detect and derive other chain + * addresses ie. Ethereum + */ + const address = message.Owner + + return applySpec({ + Id: path(['Id']), + Signature: path(['Signature']), + Data: path(['Data']), + Owner: always(address), + Anchor: path(['Anchor']), + From: always(mapFrom({ tags: message.Tags, owner: address })), + 'Forwarded-By': always(mapForwardedBy({ tags: message.Tags, owner: address })), + Tags: pipe( + pathOr([], ['Tags']), + // Some tags are being returned as non strings, so coerce them to strings + (tags) => tags.map(t => ({ name: t.name, value: `${t.value}` })) + ) + })(message) + } + ), + // both + applySpec({ + /** + * In the case of an assignment of a message on-chain, only the Id + * will be present. + * + * So checking for any other field, like 'Owner' should tell us + * whether this is an assignment of data on-chain or not. + */ + isAssignment: pipe( + path(['message', 'Owner']), + isNil + ) + }), + // static + always({ Cron: false }) + ]), + // Combine into the desired shape + ([fAssignment, fMessage = {}, fBoth, fStatic]) => ({ + cron: undefined, + ordinate: fAssignment.message.Nonce, + name: `${fBoth.isAssignment ? 'Assigned' : 'Scheduled'} Message ${fMessage.Id} ${fAssignment.message.Timestamp}:${fAssignment.message.Nonce}`, + exclude: fAssignment.Exclude, + isAssignment: fBoth.isAssignment, + assignmentId: fAssignment.AssignmentId, + message: mergeAll([ + fMessage, + fAssignment.message, + /** + * We must ensure that this field is always set, regardless if this is a message + * or just an Assignment for an existing message on-chain + */ + { Id: fMessage.Id }, + /** + * For an Assignment, the Target is derived from the recipient of + * the assigned tx. + * + * So we explicitly set Target to undefined here, such that hydration of Target + * must occur later. Otherwise we will receive high signal, in the form of an error, + * that the impl is incorrect, as opposed to an assignment potentially being misinterpreted + * as a bonafide scheduled message. + */ + { Target: fBoth.isAssignment ? undefined : fAssignment.message.Target }, + fStatic + ]), + /** + * We need the block metadata per message, + * so that we can calculate cron messages + * + * Separating them here, makes it easier to access later + * down the pipeline + */ + block: { + height: fAssignment.message['Block-Height'], + timestamp: fAssignment.message.Timestamp + } + }) +) + +export const loadProcessWith = ({ fetch, logger }) => { + return async ({ suUrl, processId }) => { + const params = toParams({ processId, from: 0, to: 1, pageSize: 1 }) + + return backoff( + () => fetch(`${suUrl}/~scheduler@1.0/schedule?${params.toString()}`).then(okRes), + { maxRetries: 5, delay: 500, log: logger, name: `loadProcess(${JSON.stringify({ suUrl, processId })})` } + ) + .catch(async (err) => { + logger('Error Encountered when loading process "%s" from SU "%s"', processId, suUrl) + throw new Error(`Error Encountered when loading process from Scheduler Unit: ${await strFromFetchError(err)}`) + }) + .then(resToJson) + .then(nodeAt(0)) + .then(mapNode) + .then(applySpec({ + owner: applySpec({ + address: path(['message', 'Owner']), + /** + * TODO: currently will always be undefined, + * since HB SU does not return the public key + * of the owner + */ + key: path(['key']) + }), + tags: path(['message', 'Tags']), + signature: path(['message', 'Signature']), + data: path(['message', 'Data']), + anchor: path(['message', 'Anchor']), + timestamp: path(['message', 'Timestamp']), + block: path(['block']), + processId: always(processId), + nonce: always(0) + })) + } +} + +export const loadTimestampWith = ({ fetch, logger }) => { + return async ({ suUrl, processId }) => { + const params = toParams({ processId, from: 0, to: 0, pageSize: 1 }) + + return backoff( + () => fetch(`${suUrl}/~scheduler@1.0/schedule?${params.toString()}`).then(okRes), + { maxRetries: 5, delay: 500, log: logger, name: `loadTimestamp(${JSON.stringify({ suUrl, processId })})` } + ) + .catch(async (err) => { + logger('Error Encountered when loading timestamp for process "%s" from SU "%s"', processId, suUrl) + throw new Error(`Error Encountered when loading timestamp for process from Scheduler Unit: ${await strFromFetchError(err)}`) + }) + .then(resToJson) + .then(({ page_info }) => ({ + timestamp: parseInt(page_info.timestamp), + height: parseInt(page_info['block-height']) + })) + } +} + +export const loadMessagesWith = ({ hashChain, fetch, logger: _logger, pageSize }) => { + const logger = _logger.child('hb-su:loadMessages') + + const isHashChainValid = isHashChainValidWith({ hashChain }) + + const fetchPageDataloader = new DataLoader(async (args) => { + fetchPageDataloader.clearAll() + + return Promise.allSettled( + args.map(({ suUrl, processId, from, to, pageSize }) => { + return Promise.resolve({ processId, from: from || 0, to: to || undefined, pageSize }) + .then(toParams) + .then((params) => fetch(`${suUrl}/~scheduler@1.0/schedule?${params.toString()}`)) + .then(okRes) + .catch(async (err) => { + logger( + 'Error Encountered when fetching page of scheduled messages from SU \'%s\' for process \'%s\' between \'%s\' and \'%s\'', + suUrl, processId, from, to + ) + throw new Error(`Encountered Error fetching scheduled messages from Scheduler Unit: ${await strFromFetchError(err)}`) + }) + .then(resToJson) + }) + ).then((values) => + /** + * By returning the error, Dataloader will register as an error + * for the individual call to load, without failing the batch as a whole + * + * See https://www.npmjs.com/package/dataloader#caching-errors + */ + values.map(v => v.status === 'fulfilled' ? v.value : v.reason) + ) + }, { + cacheKeyFn: ({ suUrl, processId, from, to, pageSize }) => `${suUrl},${processId},${from},${to},${pageSize}`, + batchScheduleFn: (cb) => setTimeout(cb, 20) + }) + + /** + * Returns an async generator that emits scheduled messages, + * one at a time + * + * Scheduled messages are fetched a page at a time, but + * emitted from the generator one message at a time. + * + * When the currently fetched page is drained, the next page is fetched + * dynamically + */ + function fetchAllPages ({ suUrl, processId, isColdStart, from, to, body }) { + /** + * The HB SU 'from' and 'to' are both inclusive. + * So when we pass from (which is the cached most recent evaluated message) + * we need to increment by 1, so as to not include the message we have + * already evaluated. + * + * But in the case of a cold start, we _do_ need to include the process + * message and so do not increment by 1. + * + * Similar logic can be found below when checking for the expected nonce + */ + + if (!isColdStart) from = from + 1 + async function fetchPage ({ from }) { + const params = toParams({ processId, from, to, pageSize }) + + return backoff( + () => { + const bodyIsValid = body && + body.edges.length === (+to - +from + 1) && + +body.edges[0]?.node?.assignment?.Tags?.find(t => t.name === 'Nonce' || t.name === 'Slot')?.value === +from + if (bodyIsValid) return body + return fetchPageDataloader.load({ suUrl, processId, from, to, pageSize }) + }, + { maxRetries: 5, delay: 500, log: logger, name: `loadMessages(${JSON.stringify({ suUrl, processId, params: params.toString() })})` } + ) + } + + /** + * The HB SU has a bug where some boolean values + * are being sent as string values like 'true' + * and 'false', which will always be truthy. + * + * So as a workaround, we check whether the value is 'false' + * and coerce to false, and otherwise cast to a boolean. + */ + function toBoolean (value) { + return (value === 'false' || value === 'undefined') ? false : Boolean(value) + } + + return async function * scheduled () { + let curPage = [] + let curFrom = from + let hasNextPage = true + + let total = 0 + + while (hasNextPage) { + await Promise.resolve() + .then(() => { + logger( + 'Loading next page of max %s messages for process "%s" from SU "%s" between "%s" and "%s"', + pageSize, processId, suUrl, from || 'initial', to || 'latest' + ) + return fetchPage({ from: curFrom }) + }) + .then(({ page_info, edges }) => { + total += edges.length + curPage = edges + hasNextPage = toBoolean(page_info.has_next_page) + curFrom = hasNextPage && pipe( + last, + path(['cursor']) + )(edges) + }) + + while (curPage.length) yield curPage.shift() + } + + logger( + 'Successfully loaded a total of %s scheduled messages for process "%s" from SU "%s" between "%s" and "%s"...', + total, processId, suUrl, from || 'initial', to || 'latest') + } + } + + function mapAoMessage ({ + processId, processBlock, assignmentId, hashChain, from, isColdStart, + processOwner, processTags, moduleId, moduleOwner, moduleTags, logger + }) { + const AoGlobal = { + Process: { Id: processId, Owner: processOwner, Tags: processTags }, + Module: { Id: moduleId, Owner: moduleOwner, Tags: moduleTags } + } + /** + * Only perform hash chain validation on processes + * spawned after arweave day, since old hash chains are invalid + * anyway. + */ + const isHashChainValidationEnabled = processBlock.height >= 1440000 + if (!isHashChainValidationEnabled) { + logger('HashChain validation disabled for old process "%s" at block [%j]', processId, processBlock) + } + + // Set this to simulate a stream error + // eslint-disable-next-line + let simulateError = false + let prevAssignmentId = assignmentId + let prevHashChain = hashChain + + // let expectedNonce = parseInt(`${from}`) + let expectedNonce = isColdStart ? 0 : parseInt(`${from}`) + 1 + return async function * (edges) { + for await (const edge of edges) { + const scheduled = pipe( + prop('node'), + /** + * Map to the expected shape + */ + mapNode, + (scheduled) => { + scheduled.AoGlobal = AoGlobal + return scheduled + } + )(edge) + + if (expectedNonce !== scheduled.message.Nonce) { + const err = new Error(`Non-incrementing slot: expected ${expectedNonce} but got ${scheduled.message.Nonce}`) + err.status = 422 + throw err + } + + if (simulateError) { + logger(' message "%s" scheduled on process "%s"', scheduled.message.Id, processId) + const err = new Error(`Simulated Error on message ${scheduled.message.Id}`) + err.status = 422 + throw err + } + + if (isHashChainValidationEnabled) { + if (!(await isHashChainValid({ assignmentId: prevAssignmentId, hashChain: prevHashChain }, scheduled))) { + logger('HashChain invalid on message "%s" scheduled on process "%s"', scheduled.message.Id, processId) + const err = new Error(`HashChain invalid on message ${scheduled.message.Id}`) + err.status = 422 + throw err + } + } + + expectedNonce = expectedNonce + 1 + prevAssignmentId = scheduled.assignmentId + prevHashChain = scheduled.message['Hash-Chain'] + + yield scheduled + } + } + } + + return (args) => Promise.resolve(args) + .then(({ + suUrl, processId, block: processBlock, owner: processOwner, tags: processTags, + moduleId, moduleOwner, moduleTags, fromOrdinate, toOrdinate, assignmentId, hashChain, + isColdStart, body + }) => { + return [ + Readable.from(fetchAllPages({ suUrl, processId, isColdStart, from: fromOrdinate, to: toOrdinate, body })()), + Transform.from(mapAoMessage({ + processId, + processBlock, + assignmentId, + hashChain, + from: fromOrdinate, + isColdStart, + processOwner, + processTags, + moduleId, + moduleOwner, + moduleTags, + logger + })) + ] + }) +} + +export const loadMessageMetaWith = ({ fetch, logger }) => { + return async ({ suUrl, processId, messageUid, body }) => { + const params = toParams({ processId, to: messageUid, from: messageUid, pageSize: 1 }) + + return backoff( + () => { + const bodyIsValid = body && + body.edges.length === 1 && + +body.edges[0]?.node?.assignment?.Tags?.find(t => t.name === 'Nonce' || t.name === 'Slot')?.value === +messageUid + if (bodyIsValid) return body + return fetch(`${suUrl}/~scheduler@1.0/schedule?${params.toString()}`).then(okRes) + }, + { maxRetries: 5, delay: 500, log: logger, name: `loadMessageMeta(${JSON.stringify({ suUrl, processId, messageUid })})` } + ) + .catch(async (err) => { + logger( + 'Error Encountered when loading message meta for message "%s" to process "%s" from SU "%s"', + messageUid, processId, suUrl + ) + throw new Error(`Error Encountered when loading message from Scheduler Unit: ${await strFromFetchError(err)}`) + }) + .then(resToJson) + .then(nodeAt(0)) + .then(mapNode) + .then(applySpec({ + timestamp: path(['message', 'Timestamp']), + nonce: path(['message', 'Nonce']), + processId: always(processId) + })) + } +} diff --git a/servers/cu/src/effects/hb/index.test.js b/servers/cu/src/effects/hb/index.test.js new file mode 100644 index 000000000..ed8671fd9 --- /dev/null +++ b/servers/cu/src/effects/hb/index.test.js @@ -0,0 +1,419 @@ +/* eslint-disable no-throw-literal */ +import { describe, test } from 'node:test' +import assert from 'node:assert' +import { createHash } from 'node:crypto' + +import { loadMessageMetaSchema, loadProcessSchema, loadTimestampSchema } from '../../domain/dal.js' +import { messageSchema } from '../../domain/model.js' +import { createTestLogger } from '../../domain/logger.js' +import { isHashChainValidWith, loadMessageMetaWith, loadProcessWith, loadTimestampWith, mapNode } from './index.js' +import { hashChain } from '.././worker/hashChain/main.js' + +const withoutAoGlobal = messageSchema.omit({ AoGlobal: true }) +const logger = createTestLogger({ name: 'ao-cu:ao-su' }) + +describe('hb-su', () => { + describe('mapNode', () => { + const now = new Date().getTime() + const messageId = 'message-123' + const assignmentId = 'assignment-123' + const assignedMessageId = 'assigned-123' + const expected = { + cron: undefined, + ordinate: '23', + name: `Scheduled Message ${messageId} ${now}:23`, + exclude: undefined, + assignmentId, + message: { + Id: messageId, + Signature: 'sig-123', + Data: 'data-123', + Owner: 'owner-123', + Target: 'process-123', + Anchor: '00000000123', + From: 'owner-123', + 'Forwarded-By': undefined, + Tags: [{ name: 'Inner', value: 'test' }], + Epoch: 0, + Nonce: 23, + Timestamp: now, + 'Block-Height': 123, + 'Hash-Chain': 'hash-123', + Cron: false + }, + block: { + height: 123, + timestamp: now + } + } + + test('should map a scheduled message', () => { + const res = mapNode({ + message: { + Id: messageId, + Owner: 'owner-123', + Tags: [ + { name: 'Inner', value: 'test' } + ], + Signature: 'sig-123', + Anchor: '00000000123', + Data: 'data-123' + }, + assignment: { + Id: assignmentId, + Anchor: '', + Owner: 'su-123', + Tags: [ + { name: 'Block-Height', value: 123 }, + { name: 'Block-Timestamp', value: now - 10000 }, + { name: 'Data-Protocol', value: 'ao' }, + { name: 'Epoch', value: '0' }, + { name: 'Hash-Chain', value: 'hash-123' }, + { name: 'Path', value: 'compute' }, + { name: 'Process', value: 'process-123' }, + { name: 'Slot', value: 23 }, + { name: 'Timestamp', value: now }, + { name: 'Variant', value: 'ao.N.1' } + ], + Data: 'su-data-123' + } + }) + + assert.deepStrictEqual( + withoutAoGlobal.parse(res), + withoutAoGlobal.parse(expected) + ) + assert.equal(res.isAssignment, false) + assert.equal(res.assignmentId, assignmentId) + }) + + describe('should map an assignment tx', () => { + const res = mapNode({ + message: { + Id: assignedMessageId + }, + assignment: { + Owner: 'su-123', + Tags: [ + { name: 'Block-Height', value: 123 }, + { name: 'Block-Timestamp', value: now - 10000 }, + { name: 'Data-Protocol', value: 'ao' }, + { name: 'Epoch', value: '0' }, + { name: 'Hash-Chain', value: 'hash-123' }, + { name: 'Path', value: 'compute' }, + { name: 'Process', value: 'process-123' }, + { name: 'Slot', value: 23 }, + { name: 'Timestamp', value: now }, + { name: 'Variant', value: 'ao.N.1' } + ], + Data: 'su-data-123' + } + }) + + test('should set isAssignment to true', () => { + assert.ok(res.isAssignment) + }) + + test('should set message.Id to the assignment Message tag', () => { + assert.equal(res.message.Id, assignedMessageId) + }) + + test('should explicitly set message.Target to undefined', () => { + assert.equal(res.message.Target, undefined) + }) + + test('should set the name to indicate an assignment', () => { + assert.equal(res.name, `Assigned Message ${assignedMessageId} ${now}:23`) + }) + + test('should set all other message fields as normal', () => { + assert.deepStrictEqual(res.message, { + Id: assignedMessageId, + Target: undefined, + Epoch: 0, + Nonce: 23, + Timestamp: now, + 'Block-Height': 123, + 'Hash-Chain': 'hash-123', + Cron: false + }) + }) + }) + }) + + describe('isHashChainValid', () => { + const isHashChainValid = isHashChainValidWith({ hashChain }) + const now = new Date().getTime() + const messageId = 'message-123' + const scheduled = { + cron: undefined, + ordinate: '23', + name: `Scheduled Message ${messageId} ${now}:23`, + exclude: undefined, + message: { + Id: messageId, + Signature: 'sig-123', + Data: 'data-123', + Owner: 'owner-123', + Target: 'process-123', + Anchor: '00000000123', + From: 'owner-123', + 'Forwarded-By': undefined, + Tags: [{ name: 'Foo', value: 'Bar' }], + Epoch: 0, + Nonce: 23, + Timestamp: now, + 'Block-Height': 123, + 'Hash-Chain': 'hash-123', + Cron: false + }, + block: { + height: 123, + timestamp: now + } + } + + test('should return whether the hashChain exists if there is no previous assignment', async () => { + // no prev info + assert(await isHashChainValid({}, scheduled)) + // first assignment ergo has no prev assignment + assert(await isHashChainValid({ + hashChain: null, + assignmentId: 'foo' + }, scheduled)) + assert(await isHashChainValid({ + hashChain: 'foo', + assignmentId: null + }, scheduled)) + }) + + test('should calculate and compare the hashChain based on the previous assignment', async () => { + const prevAssignmentId = Buffer.from('assignment-123', 'utf8').toString('base64url') + const prevHashChain = Buffer.from('hashchain-123', 'utf8').toString('base64url') + + const prev = { assignmentId: prevAssignmentId, hashChain: prevHashChain } + + const expected = createHash('sha256') + .update(Buffer.from(prevAssignmentId, 'base64url')) + .update(Buffer.from(prevHashChain, 'base64url')) + .digest('base64url') + + const valid = { + message: { + // .... + 'Hash-Chain': expected + } + } + + assert(await isHashChainValid(prev, valid)) + + const invalid = { + message: { + 'Hash-Chain': createHash('sha256') + .update(Buffer.from('something else', 'base64url')) + .update(Buffer.from(prevHashChain, 'base64url')) + .digest('base64url') + } + } + + assert(!(await isHashChainValid(prev, invalid))) + }) + }) + + describe('loadMessageMetaWith', () => { + test('return the message meta', async () => { + const loadMessageMeta = loadMessageMetaSchema.implement( + loadMessageMetaWith({ + fetch: async (url) => { + url = new URL(url) + const params = Object.fromEntries(url.searchParams) + + assert.equal(url.origin, 'https://foo.bar') + assert.equal(url.pathname, '/~scheduler@1.0/schedule') + assert.deepStrictEqual(params, { + target: 'process-123', + from: '3', + to: '3', + limit: '1', + accept: 'application/aos-2' + }) + + return new Response(JSON.stringify({ + page_info: { has_next_page: false }, + edges: [{ + cursor: '3', + node: { + message: { + Id: 'message-123', + Owner: 'owner-123', + Tags: [ + { name: 'Inner', value: 'test' } + ], + Signature: 'sig-123', + Anchor: '00000000123', + Data: 'data-123' + }, + assignment: { + Id: 'assignment-123', + Anchor: '', + Owner: 'su-123', + Tags: [ + { name: 'Block-Height', value: 123 }, + { name: 'Block-Timestamp', value: new Date().getTime() }, + { name: 'Data-Protocol', value: 'ao' }, + { name: 'Epoch', value: '0' }, + { name: 'Hash-Chain', value: 'hash-123' }, + { name: 'Path', value: 'compute' }, + { name: 'Process', value: 'process-123' }, + { name: 'Slot', value: '3' }, + { name: 'Timestamp', value: 12345 }, + { name: 'Variant', value: 'ao.N.1' } + ], + Data: 'su-data-123' + } + } + }] + })) + }, + logger + }) + ) + + const res = await loadMessageMeta({ + suUrl: 'https://foo.bar', + processId: 'process-123', + messageUid: '3', + body: undefined + }) + + assert.deepStrictEqual(res, { + processId: 'process-123', + timestamp: 12345, + nonce: 3 + }) + }) + }) + + describe('loadProcessesWith', () => { + test('load the process metadata', async () => { + const loadProcess = loadProcessSchema.implement(loadProcessWith({ + fetch: async (url) => { + url = new URL(url) + const params = Object.fromEntries(url.searchParams) + + assert.equal(url.origin, 'https://foo.bar') + assert.equal(url.pathname, '/~scheduler@1.0/schedule') + assert.deepStrictEqual(params, { + target: 'pid-1', + from: '0', + to: '1', + limit: '1', + accept: 'application/aos-2' + }) + + return new Response(JSON.stringify({ + page_info: { has_next_page: false }, + edges: [{ + cursor: '0', + node: { + message: { + Id: 'message-123', + Owner: 'owner-123', + Tags: [ + { name: 'Foo', value: 'Bar' } + ], + Signature: 'signature-1', + Anchor: null, + Data: '1984' + }, + assignment: { + Id: 'assignment-123', + Anchor: '', + Owner: 'su-123', + Tags: [ + { name: 'Block-Height', value: 123 }, + { name: 'Block-Timestamp', value: new Date().getTime() }, + { name: 'Data-Protocol', value: 'ao' }, + { name: 'Epoch', value: '0' }, + { name: 'Hash-Chain', value: 'hash-123' }, + { name: 'Path', value: 'compute' }, + { name: 'Process', value: 'process-123' }, + { name: 'Slot', value: 0 }, + { name: 'Timestamp', value: 123456 }, + { name: 'Variant', value: 'ao.N.1' } + ], + Data: 'su-data-123' + } + } + }] + })) + }, + logger + })) + const result = await loadProcess({ + suUrl: 'https://foo.bar', + processId: 'pid-1' + }) + assert.deepStrictEqual(result, { + owner: { + address: 'owner-123', + key: undefined + }, + tags: [ + { name: 'Foo', value: 'Bar' } + ], + block: { + height: 123, + timestamp: 123456 + }, + processId: 'pid-1', + timestamp: 123456, + signature: 'signature-1', + data: '1984', + nonce: 0, + anchor: null + }) + }) + }) + + describe('loadTimestampWith', () => { + test('load process timestamp and block height', async () => { + const loadTimestamp = loadTimestampSchema.implement(loadTimestampWith({ + fetch: async (url) => { + url = new URL(url) + const params = Object.fromEntries(url.searchParams) + + assert.equal(url.origin, 'https://foo.bar') + assert.equal(url.pathname, '/~scheduler@1.0/schedule') + assert.deepStrictEqual(params, { + target: 'pid-1', + from: '0', + to: '0', + limit: '1', + accept: 'application/aos-2' + }) + + return new Response(JSON.stringify({ + page_info: { + has_next_page: false, + timestamp: 123456, + 'block-height': '1' + }, + edges: [] + })) + }, + logger + })) + + const result = await loadTimestamp({ + suUrl: 'https://foo.bar', + processId: 'pid-1' + }) + + assert.deepStrictEqual(result, { + timestamp: 123456, + height: 1 + }) + }) + }) +}) diff --git a/servers/cu/src/bootstrap.js b/servers/cu/src/effects/main.cu.js similarity index 66% rename from servers/cu/src/bootstrap.js rename to servers/cu/src/effects/main.cu.js index bd9b69547..52ce1fcb8 100644 --- a/servers/cu/src/bootstrap.js +++ b/servers/cu/src/effects/main.cu.js @@ -5,32 +5,22 @@ import { writeFile, mkdir, rename as renameFile } from 'node:fs/promises' import { createReadStream } from 'node:fs' import { BroadcastChannel } from 'node:worker_threads' -import pMap from 'p-map' import PQueue from 'p-queue' import Dataloader from 'dataloader' import workerpool from 'workerpool' import { connect as schedulerUtilsConnect } from '@permaweb/ao-scheduler-utils' -import { fromPromise } from 'hyper-async' import lt from 'long-timeout' -// Precanned clients to use for OOTB apis -import * as DbClient from './effects/db.js' -import * as ArweaveClient from './effects/arweave.js' -import * as AoSuClient from './effects/ao-su.js' -import * as WasmClient from './effects/wasm.js' -import * as AoProcessClient from './effects/ao-process.js' -import * as AoModuleClient from './effects/ao-module.js' -import * as AoEvaluationClient from './effects/ao-evaluation.js' -import * as AoBlockClient from './effects/ao-block.js' -import * as MetricsClient from './effects/metrics.js' - -import { readResultWith } from './domain/api/readResult.js' -import { readStateWith, pendingReadStates } from './domain/api/readState.js' -import { readCronResultsWith } from './domain/api/readCronResults.js' -import { healthcheckWith } from './domain/api/healthcheck.js' -import { readResultsWith } from './domain/api/readResults.js' -import { dryRunWith } from './domain/api/dryRun.js' -import { statsWith } from './domain/api/perf.js' +import * as DbClient from './db.js' +import * as ArweaveClient from './arweave.js' +import * as AoSuClient from './ao-su.js' +import * as WasmClient from './wasm.js' +import * as AoProcessClient from './ao-process.js' +import * as AoModuleClient from './ao-module.js' +import * as AoEvaluationClient from './ao-evaluation.js' +import * as AoBlockClient from './ao-block.js' +import * as MetricsClient from './metrics.js' +import * as AoHttp from './ao-http/index.js' const __dirname = dirname(fileURLToPath(import.meta.url)) @@ -50,9 +40,7 @@ async function readFile (file) { return Buffer.concat(chunks) } -export const createApis = async (ctx) => { - ctx.logger('Creating business logic apis') - +export const createEffects = async (ctx) => { const setTimeout = (...args) => lt.setTimeout(...args) const clearTimeout = (...args) => lt.clearTimeout(...args) @@ -142,10 +130,10 @@ export const createApis = async (ctx) => { * Since hash chain valiation is done for _every single scheduled message_, we offload the * work to a worker thread, so at least the main thread isn't blocked. */ - const hashChainWorkerPath = join(__dirname, 'effects', 'worker', 'hashChain', 'index.js') + const hashChainWorkerPath = join(__dirname, 'worker', 'hashChain', 'index.js') const hashChainWorker = workerpool.pool(hashChainWorkerPath, { maxWorkers: maxPrimaryWorkerThreads }) - const worker = join(__dirname, 'effects', 'worker', 'evaluator', 'index.js') + const worker = join(__dirname, 'worker', 'evaluator', 'index.js') const primaryWorkerPool = workerpool.pool(worker, { maxWorkers: maxPrimaryWorkerThreads, onCreateWorker: onCreateWorker('primary') @@ -170,7 +158,7 @@ export const createApis = async (ctx) => { * TODO: I don't really like implictly doing this, * but works for now. */ - const _metrics = MetricsClient.initializeRuntimeMetricsWith({})() + const metrics = MetricsClient.initializeRuntimeMetricsWith({})() const gauge = MetricsClient.gaugeWith({}) @@ -208,6 +196,8 @@ export const createApis = async (ctx) => { mkdir }) + // TODO: these log statements ought to be moved close to the code that is pertinent + // For now, just keeping here ctx.logger('Process Arweave Checkpoint creation is set to "%s"', !ctx.DISABLE_PROCESS_CHECKPOINT_CREATION) ctx.logger('Process File Checkpoint creation is set to "%s"', !ctx.DISABLE_PROCESS_FILE_CHECKPOINT_CREATION) ctx.logger('Ignoring Arweave Checkpoints for processes [ %s ]', ctx.PROCESS_IGNORE_ARWEAVE_CHECKPOINTS.join(', ')) @@ -257,46 +247,37 @@ export const createApis = async (ctx) => { cache: WasmClient.createWasmModuleCache({ MAX_SIZE: ctx.WASM_MODULE_CACHE_MAX_SIZE }) }) - const stats = statsWith({ - gauge, - loadWorkerStats: () => ({ - primary: ({ - ...primaryWorkerPool.stats(), - /** - * We use a work queue on the main thread while keeping - * worker pool queues empty (see comment above) - * - * So we use the work queue size to report pending tasks - */ - pendingTasks: primaryWorkQueue.size - }), - dryRun: ({ - ...dryRunWorkerPool.stats(), - /** - * We use a work queue on the main thread while keeping - * worker pool queues empty (see comment above) - * - * So we use the work queue size to report pending tasks - */ - pendingTasks: dryRunWorkQueue.size - }) + const loadWorkerStats = () => ({ + primary: ({ + ...primaryWorkerPool.stats(), + /** + * We use a work queue on the main thread while keeping + * worker pool queues empty (see comment above) + * + * So we use the work queue size to report pending tasks + */ + pendingTasks: primaryWorkQueue.size }), - /** - * https://nodejs.org/api/process.html#processmemoryusage - * - * Note: worker thread resources will be included in rss, - * as that is the Resident Set Size for the entire node process - */ - loadMemoryUsage: () => process.memoryUsage(), - loadProcessCacheUsage: () => wasmMemoryCache.data.loadProcessCacheUsage() + dryRun: ({ + ...dryRunWorkerPool.stats(), + /** + * We use a work queue on the main thread while keeping + * worker pool queues empty (see comment above) + * + * So we use the work queue size to report pending tasks + */ + pendingTasks: dryRunWorkQueue.size + }) }) + /** - * TODO: Should this just be a field on stats to call? + * https://nodejs.org/api/process.html#processmemoryusage + * + * Note: worker thread resources will be included in rss, + * as that is the Resident Set Size for the entire node process */ - const metrics = { - contentType: _metrics.contentType, - compute: fromPromise(() => _metrics.metrics()) - } + const loadMemoryUsage = () => process.memoryUsage() + const loadProcessCacheUsage = () => wasmMemoryCache.data.loadProcessCacheUsage() const evaluationCounter = MetricsClient.counterWith({})({ name: 'ao_process_total_evaluations', @@ -315,7 +296,7 @@ export const createApis = async (ctx) => { const BLOCK_GRAPHQL_ARRAY = ctx.GRAPHQL_URLS.length > 0 ? ctx.GRAPHQL_URLS : [ctx.GRAPHQL_URL] - const sharedDeps = (logger) => ({ + const common = (logger) => ({ loadTransactionMeta: ArweaveClient.loadTransactionMetaWith({ fetch: ctx.fetch, GRAPHQL_URL: ctx.GRAPHQL_URL, logger }), loadTransactionData: ArweaveClient.loadTransactionDataWith({ fetch: ctx.fetch, ARWEAVE_URL: ctx.ARWEAVE_URL, logger }), isProcessOwnerSupported: AoProcessClient.isProcessOwnerSupportedWith({ ALLOW_OWNERS: ctx.ALLOW_OWNERS }), @@ -338,6 +319,7 @@ export const createApis = async (ctx) => { saveLatestProcessMemory: AoProcessClient.saveLatestProcessMemoryWith({ cache: wasmMemoryCache, EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD: ctx.EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD, + EAGER_CHECKPOINT_EVAL_TIME_THRESHOLD: ctx.EAGER_CHECKPOINT_EVAL_TIME_THRESHOLD, saveCheckpoint, logger }), @@ -387,147 +369,74 @@ export const createApis = async (ctx) => { isModuleMemoryLimitSupported: WasmClient.isModuleMemoryLimitSupportedWith({ PROCESS_WASM_MEMORY_MAX_LIMIT: ctx.PROCESS_WASM_MEMORY_MAX_LIMIT }), isModuleComputeLimitSupported: WasmClient.isModuleComputeLimitSupportedWith({ PROCESS_WASM_COMPUTE_MAX_LIMIT: ctx.PROCESS_WASM_COMPUTE_MAX_LIMIT }), isModuleFormatSupported: WasmClient.isModuleFormatSupportedWith({ PROCESS_WASM_SUPPORTED_FORMATS: ctx.PROCESS_WASM_SUPPORTED_FORMATS }), - isModuleExtensionSupported: WasmClient.isModuleExtensionSupportedWith({ PROCESS_WASM_SUPPORTED_EXTENSIONS: ctx.PROCESS_WASM_SUPPORTED_EXTENSIONS }), - MODULE_MODE: ctx.MODULE_MODE, - logger - }) - /** - * default readState that works OOTB - * - Uses PouchDB to cache evaluations and processes - * - Uses ao Sequencer Unit - * - Use arweave.net gateway - */ - const readStateLogger = ctx.logger.child('readState') - const readState = readStateWith(sharedDeps(readStateLogger)) - - const dryRunLogger = ctx.logger.child('dryRun') - const dryRun = dryRunWith({ - ...sharedDeps(dryRunLogger), - setTimeout, - clearTimeout, - loadMessageMeta: AoSuClient.loadMessageMetaWith({ fetch: ctx.fetch, logger: dryRunLogger }), - /** - * Dry-runs use a separate worker thread pool, so as to not block - * primary evaluations - */ - loadDryRunEvaluator: AoModuleClient.evaluatorWith({ - loadWasmModule, - evaluateWith: (prep) => dryRunWorkQueue.add(() => - Promise.resolve() - /** - * prep work is deferred until the work queue tasks is executed - */ - .then(prep) - .then(([args, options]) => - Promise.resolve() - .then(() => { - /** - * TODO: is this the best place for this? - * - * It keeps it abstracted away from business logic, - * and tied to the specific evaluator, so seems kosher, - * but also feels kind of misplaced - */ - if (args.close) return broadcastCloseStream(args.streamId) - - return dryRunWorkerPool.exec('evaluate', [args], options) - }) - .catch((err) => { - /** - * Hack to detect when the max queue size is being exceeded and to reject - * with a more informative error - */ - if (err.message.startsWith('Max queue size of')) { - const dryRunLimitErr = new Error('Dry-Run enqueue limit exceeded') - dryRunLimitErr.status = 429 - return Promise.reject(dryRunLimitErr) - } - - // Bubble as normal - return Promise.reject(err) - }) - ) - ), - logger: dryRunLogger - }) - }) - - /** - * default readResult that works OOTB - * - Uses PouchDB to cache evaluations and processes - * - Uses ao Sequencer Unit - * - Use arweave.net gateway - */ - const readResultLogger = ctx.logger.child('readResult') - const readResult = readResultWith({ - ...sharedDeps(readResultLogger), - loadMessageMeta: AoSuClient.loadMessageMetaWith({ fetch: ctx.fetch, logger: readResultLogger }) - }) - - const readCronResultsLogger = ctx.logger.child('readCronResults') - const readCronResults = readCronResultsWith({ - ...sharedDeps(readCronResultsLogger), - findEvaluations: AoEvaluationClient.findEvaluationsWith({ db, logger: readCronResultsLogger }) - }) - - const readResultsLogger = ctx.logger.child('readResults') - const readResults = readResultsWith({ - ...sharedDeps(readResultsLogger), - findEvaluations: AoEvaluationClient.findEvaluationsWith({ db, logger: readResultsLogger }) + isModuleExtensionSupported: WasmClient.isModuleExtensionSupportedWith({ PROCESS_WASM_SUPPORTED_EXTENSIONS: ctx.PROCESS_WASM_SUPPORTED_EXTENSIONS }) }) - let checkpointP - const checkpointWasmMemoryCache = fromPromise(async () => { - if (checkpointP) { - ctx.logger('Checkpointing of WASM Memory Cache already in progress. Nooping...') - return checkpointP - } + const loadMessageMeta = AoSuClient.loadMessageMetaWith({ fetch: ctx.fetch, logger: ctx.logger }) - const pArgs = [] - /** - * push a new object to keep references to original data intact - */ - wasmMemoryCache.data.forEach((value) => - pArgs.push({ Memory: value.Memory, File: value.File, evaluation: value.evaluation }) - ) - - checkpointP = pMap( - pArgs, - (value) => saveCheckpoint({ Memory: value.Memory, File: value.File, ...value.evaluation }) - .catch((err) => { - ctx.logger( - 'Error occurred when creating Checkpoint for evaluation "%j". Skipping...', - value.evaluation, - err - ) - }), - { - /** - * TODO: allow to be configured on CU - * - * Helps prevent the gateway from being overwhelmed and then timing out - */ - concurrency: 10, + const loadDryRunEvaluator = AoModuleClient.evaluatorWith({ + loadWasmModule, + evaluateWith: (prep) => dryRunWorkQueue.add(() => + Promise.resolve() /** - * Prevent any one rejected promise from causing other invocations - * to not be attempted. - * - * The overall promise will still reject, which is why we have - * an empty catch below, which will allow all Promises to either resolve, - * or reject, then the final wrapping promise to always resolve. - * - * https://github.com/sindresorhus/p-map?tab=readme-ov-file#stoponerror + * prep work is deferred until the work queue tasks is executed */ - stopOnError: false - } + .then(prep) + .then(([args, options]) => + Promise.resolve() + .then(() => { + /** + * TODO: is this the best place for this? + * + * It keeps it abstracted away from business logic, + * and tied to the specific evaluator, so seems kosher, + * but also feels kind of misplaced + */ + if (args.close) return broadcastCloseStream(args.streamId) + + return dryRunWorkerPool.exec('evaluate', [args], options) + }) + .catch((err) => { + /** + * Hack to detect when the max queue size is being exceeded and to reject + * with a more informative error + */ + if (err.message.startsWith('Max queue size of')) { + const dryRunLimitErr = new Error('Dry-Run enqueue limit exceeded') + dryRunLimitErr.status = 429 + return Promise.reject(dryRunLimitErr) + } + + // Bubble as normal + return Promise.reject(err) + }) + ) ) - .catch(() => {}) - - await checkpointP - checkpointP = undefined }) - const healthcheck = healthcheckWith({ walletAddress: address }) + const findEvaluations = AoEvaluationClient.findEvaluationsWith({ db, logger: ctx.logger }) - return { metrics, stats, pendingReadStates, readState, dryRun, readResult, readResults, readCronResults, checkpointWasmMemoryCache, healthcheck } + /** + * The presentation side-effect, that accepts the domain + * and any additional context, and returns the app + */ + const app = (domain) => AoHttp.createAoHttp({ ...ctx, domain }) + + return { + common, + setTimeout, + clearTimeout, + metrics, + address, + gauge, + wasmMemoryCache, + loadWorkerStats, + loadMemoryUsage, + loadProcessCacheUsage, + loadMessageMeta, + loadDryRunEvaluator, + findEvaluations, + saveCheckpoint, + app + } } diff --git a/servers/cu/src/effects/main.hb.js b/servers/cu/src/effects/main.hb.js new file mode 100644 index 000000000..5e622bf17 --- /dev/null +++ b/servers/cu/src/effects/main.hb.js @@ -0,0 +1,419 @@ +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { randomBytes } from 'node:crypto' +import { writeFile, mkdir, rename as renameFile } from 'node:fs/promises' +import { createReadStream } from 'node:fs' +import { BroadcastChannel } from 'node:worker_threads' + +import PQueue from 'p-queue' +import workerpool from 'workerpool' +import lt from 'long-timeout' + +import * as DbClient from './db.js' +import * as ArweaveClient from './arweave.js' +// import * as AoSuClient from './ao-su.js' +import * as WasmClient from './wasm.js' +import * as AoProcessClient from './ao-process.js' +import * as AoModuleClient from './ao-module.js' +import * as AoEvaluationClient from './ao-evaluation.js' +import * as AoBlockClient from './ao-block.js' +import * as MetricsClient from './metrics.js' +import * as AoHttp from './ao-http/index.js' + +import * as HbClient from './hb/index.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +/** + * readFile from NodeJS has a limit of 2GB. + * + * Since we may need to read files larger than this, + * we create simple wrapper around createReadStream + * that provides the same api as readFile + */ +async function readFile (file) { + const stream = createReadStream(file) + + const chunks = [] + for await (const chunk of stream) chunks.push(chunk) + + return Buffer.concat(chunks) +} + +export const createEffects = async (ctx) => { + const setTimeout = (...args) => lt.setTimeout(...args) + const clearTimeout = (...args) => lt.clearTimeout(...args) + + const db = await DbClient.createDbClient({ url: ctx.DB_URL, bootstrap: true }) + + const BROADCAST = 'workers' + const workerBroadcast = new BroadcastChannel(BROADCAST).unref() + const broadcastCloseStream = (streamId) => workerBroadcast.postMessage({ type: 'close-stream', streamId }) + + const onCreateWorker = (type) => () => { + const workerId = randomBytes(8).toString('hex') + ctx.logger('Spinning up "%s" pool worker with id "%s"...', type, workerId) + + return { + workerThreadOpts: { + workerData: { + BROADCAST, + WASM_MODULE_CACHE_MAX_SIZE: ctx.WASM_MODULE_CACHE_MAX_SIZE, + WASM_INSTANCE_CACHE_MAX_SIZE: ctx.WASM_INSTANCE_CACHE_MAX_SIZE, + WASM_BINARY_FILE_DIRECTORY: ctx.WASM_BINARY_FILE_DIRECTORY, + ARWEAVE_URL: ctx.ARWEAVE_URL, + GRAPHQL_URL: ctx.GRAPHQL_URL, + CHECKPOINT_GRAPHQL_URL: ctx.CHECKPOINT_GRAPHQL_URL, + DB_URL: ctx.DB_URL, + id: workerId, + MODE: ctx.MODE, + LOG_CONFIG_PATH: ctx.LOG_CONFIG_PATH, + DEFAULT_LOG_LEVEL: ctx.DEFAULT_LOG_LEVEL, + DISABLE_PROCESS_EVALUATION_CACHE: ctx.DISABLE_PROCESS_EVALUATION_CACHE + } + } + } + } + + /** + * Some of the work performed, in prep for sending the task to the worker thread pool, + * ie. copying the process memory so that each eval stream may have their own memory + * to pass back and forth (see below), can be resource intensive. + * + * If the thread pool is fully utilized, the pool will start to queue tasks sent to it on the main-thread. + * Subsequently, the amount of time between when the "prep-work" and + * the "actual evaluation work" are performed can grow very large. + * + * This effectively produces a "front-loading" effect, where all the "prep-work" is ran + * all up front, then queued for a worker to eventually take on the actual work. + * + * In other words, resource intensive data strucutres ie. process memory can just be + * sitting queued for long periods of time, waiting for an available worker thread. We need to mitigate this. + * + * So for each worker thread pool, we utilize a queue with matching concurrency as the thread pool, + * which will defer performing the "prep-work" until right before a worker is available to perform the "actual work", + * ergo eliminating the "front-loading" effect. + * + * (see pQueue instantitations below) + */ + + const maxPrimaryWorkerThreads = Math.min( + Math.max(1, ctx.WASM_EVALUATION_MAX_WORKERS - 1), + Math.ceil(ctx.WASM_EVALUATION_MAX_WORKERS * (ctx.WASM_EVALUATION_PRIMARY_WORKERS_PERCENTAGE / 100)) + ) + + /** + * node's crypto module is synchronous, which blocks the main thread. + * Since hash chain valiation is done for _every single scheduled message_, we offload the + * work to a worker thread, so at least the main thread isn't blocked. + */ + const hashChainWorkerPath = join(__dirname, 'worker', 'hashChain', 'index.js') + const hashChainWorker = workerpool.pool(hashChainWorkerPath, { maxWorkers: maxPrimaryWorkerThreads }) + + const worker = join(__dirname, 'worker', 'evaluator', 'index.js') + const primaryWorkerPool = workerpool.pool(worker, { + maxWorkers: maxPrimaryWorkerThreads, + onCreateWorker: onCreateWorker('primary') + }) + const primaryWorkQueue = new PQueue({ concurrency: maxPrimaryWorkerThreads }) + + const maxDryRunWorkerTheads = Math.max( + 1, + Math.floor(ctx.WASM_EVALUATION_MAX_WORKERS * (1 - (ctx.WASM_EVALUATION_PRIMARY_WORKERS_PERCENTAGE / 100))) + ) + const dryRunWorkerPool = workerpool.pool(worker, { + maxWorkers: maxDryRunWorkerTheads, + onCreateWorker: onCreateWorker('dry-run'), + maxQueueSize: ctx.WASM_EVALUATION_WORKERS_DRY_RUN_MAX_QUEUE + }) + const dryRunWorkQueue = new PQueue({ concurrency: maxDryRunWorkerTheads }) + + const arweave = ArweaveClient.createWalletClient() + const address = ArweaveClient.addressWith({ WALLET: ctx.WALLET, arweave }) + + /** + * TODO: I don't really like implictly doing this, + * but works for now. + */ + const metrics = MetricsClient.initializeRuntimeMetricsWith({})() + + const gauge = MetricsClient.gaugeWith({}) + + const readProcessMemoryFile = AoProcessClient.readProcessMemoryFileWith({ + DIR: ctx.PROCESS_MEMORY_CACHE_FILE_DIR, + readFile + }) + + const readFileCheckpointMemory = AoProcessClient.readProcessMemoryFileWith({ + readFile, + DIR: ctx.PROCESS_MEMORY_FILE_CHECKPOINTS_DIR + }) + + const writeProcessMemoryFile = AoProcessClient.writeProcessMemoryFileWith({ + DIR: ctx.PROCESS_MEMORY_CACHE_FILE_DIR, + writeFile, + renameFile, + mkdir + }) + + const writeFileCheckpointMemory = AoProcessClient.writeProcessMemoryFileWith({ + /** + * Using separate files between the LRU cache and file checkpoints prevents + * race conditions that arise from using the same physical file, meaning that + * the LRU cache and SQLite do not have to be kept in sync. + * + * This also reduces SQLite write operations due to LRU Caches that are constantly + * draining to files. + * + * In other words, file checkpoints only _need_ to be created/updated on shutdown + */ + DIR: ctx.PROCESS_MEMORY_FILE_CHECKPOINTS_DIR, + writeFile, + renameFile, + mkdir + }) + + // TODO: these log statements ought to be moved close to the code that is pertinent + // For now, just keeping here + ctx.logger('Process Arweave Checkpoint creation is set to "%s"', !ctx.DISABLE_PROCESS_CHECKPOINT_CREATION) + ctx.logger('Process File Checkpoint creation is set to "%s"', !ctx.DISABLE_PROCESS_FILE_CHECKPOINT_CREATION) + ctx.logger('Ignoring Arweave Checkpoints for processes [ %s ]', ctx.PROCESS_IGNORE_ARWEAVE_CHECKPOINTS.join(', ')) + ctx.logger('Ignoring Arweave Checkpoints [ %s ]', ctx.IGNORE_ARWEAVE_CHECKPOINTS.join(', ')) + ctx.logger('Trusting Arweave Checkpoint Owners [ %s ]', ctx.PROCESS_CHECKPOINT_TRUSTED_OWNERS.join(', ')) + ctx.logger('Allowing only process owners [ %s ]', ctx.ALLOW_OWNERS.join(', ')) + ctx.logger('Restricting processes [ %s ]', ctx.RESTRICT_PROCESSES.join(', ')) + ctx.logger('Allowing only processes [ %s ]', ctx.ALLOW_PROCESSES.join(', ')) + ctx.logger('Max worker threads set to %s', ctx.WASM_EVALUATION_MAX_WORKERS) + ctx.logger('Max primary worker threads set to %s', maxPrimaryWorkerThreads) + ctx.logger('Max dry-run worker threads set to %s', maxDryRunWorkerTheads) + ctx.logger('Max dry-run worker thread pool queue size set to %s', ctx.WASM_EVALUATION_WORKERS_DRY_RUN_MAX_QUEUE) + + const saveCheckpoint = AoProcessClient.saveCheckpointWith({ + address, + readProcessMemoryFile, + queryGateway: ArweaveClient.queryGatewayWith({ fetch: ctx.fetch, GRAPHQL_URL: ctx.GRAPHQL_URL, logger: ctx.logger }), + queryCheckpointGateway: ArweaveClient.queryGatewayWith({ fetch: ctx.fetch, GRAPHQL_URL: ctx.CHECKPOINT_GRAPHQL_URL, logger: ctx.logger }), + hashWasmMemory: WasmClient.hashWasmMemoryWith({ logger: ctx.logger }), + buildAndSignDataItem: ArweaveClient.buildAndSignDataItemWith({ WALLET: ctx.WALLET }), + uploadDataItem: ArweaveClient.uploadDataItemWith({ UPLOADER_URL: ctx.UPLOADER_URL, fetch: ctx.fetch, logger: ctx.logger }), + writeCheckpointRecord: AoProcessClient.writeCheckpointRecordWith({ db }), + writeFileCheckpointMemory, + writeFileCheckpointRecord: AoProcessClient.writeFileCheckpointRecordWith({ db }), + logger: ctx.logger, + DISABLE_PROCESS_CHECKPOINT_CREATION: ctx.DISABLE_PROCESS_CHECKPOINT_CREATION, + DISABLE_PROCESS_FILE_CHECKPOINT_CREATION: ctx.DISABLE_PROCESS_FILE_CHECKPOINT_CREATION, + PROCESS_CHECKPOINT_CREATION_THROTTLE: ctx.PROCESS_CHECKPOINT_CREATION_THROTTLE, + PROCESS_MEMORY_FILE_CHECKPOINTS_DIR: ctx.PROCESS_MEMORY_FILE_CHECKPOINTS_DIR + }) + + const wasmMemoryCache = await AoProcessClient.createProcessMemoryCache({ + gauge, + MAX_SIZE: ctx.PROCESS_MEMORY_CACHE_MAX_SIZE, + TTL: ctx.PROCESS_MEMORY_CACHE_TTL, + writeProcessMemoryFile, + logger: ctx.logger, + setTimeout, + clearTimeout + }) + + const loadWasmModule = WasmClient.loadWasmModuleWith({ + fetch: ctx.fetch, + ARWEAVE_URL: ctx.ARWEAVE_URL, + WASM_BINARY_FILE_DIRECTORY: ctx.WASM_BINARY_FILE_DIRECTORY, + logger: ctx.logger, + cache: WasmClient.createWasmModuleCache({ MAX_SIZE: ctx.WASM_MODULE_CACHE_MAX_SIZE }) + }) + + const loadWorkerStats = () => ({ + primary: ({ + ...primaryWorkerPool.stats(), + /** + * We use a work queue on the main thread while keeping + * worker pool queues empty (see comment above) + * + * So we use the work queue size to report pending tasks + */ + pendingTasks: primaryWorkQueue.size + }), + dryRun: ({ + ...dryRunWorkerPool.stats(), + /** + * We use a work queue on the main thread while keeping + * worker pool queues empty (see comment above) + * + * So we use the work queue size to report pending tasks + */ + pendingTasks: dryRunWorkQueue.size + }) + }) + + /** + * https://nodejs.org/api/process.html#processmemoryusage + * + * Note: worker thread resources will be included in rss, + * as that is the Resident Set Size for the entire node process + */ + const loadMemoryUsage = () => process.memoryUsage() + const loadProcessCacheUsage = () => wasmMemoryCache.data.loadProcessCacheUsage() + + const evaluationCounter = MetricsClient.counterWith({})({ + name: 'ao_process_total_evaluations', + description: 'The total number of evaluations on a CU', + labelNames: ['stream_type', 'message_type', 'process_error'] + }) + + /** + * TODO: Gas can grow to a huge number. We need to make sure this doesn't crash when that happens + */ + // const gasCounter = MetricsClient.counterWith({})({ + // name: 'total_gas_used', + // description: 'The total number of gas used on a CU', + // labelNames: ['processId', 'cron', 'dryRun', 'error'] + // }) + + const BLOCK_GRAPHQL_ARRAY = ctx.GRAPHQL_URLS.length > 0 ? ctx.GRAPHQL_URLS : [ctx.GRAPHQL_URL] + + const common = (logger) => ({ + loadTransactionMeta: ArweaveClient.loadTransactionMetaWith({ fetch: ctx.fetch, GRAPHQL_URL: ctx.GRAPHQL_URL, logger }), + loadTransactionData: ArweaveClient.loadTransactionDataWith({ fetch: ctx.fetch, ARWEAVE_URL: ctx.ARWEAVE_URL, logger }), + isProcessOwnerSupported: AoProcessClient.isProcessOwnerSupportedWith({ ALLOW_OWNERS: ctx.ALLOW_OWNERS }), + findProcess: AoProcessClient.findProcessWith({ db, logger }), + findLatestProcessMemory: AoProcessClient.findLatestProcessMemoryWith({ + cache: wasmMemoryCache, + loadTransactionData: ArweaveClient.loadTransactionDataWith({ fetch: ctx.fetch, ARWEAVE_URL: ctx.ARWEAVE_URL, logger }), + readProcessMemoryFile, + readFileCheckpointMemory, + findFileCheckpointBefore: AoProcessClient.findFileCheckpointBeforeWith({ db }), + findRecordCheckpointBefore: AoProcessClient.findRecordCheckpointBeforeWith({ db }), + address, + queryGateway: ArweaveClient.queryGatewayWith({ fetch: ctx.fetch, GRAPHQL_URL: ctx.GRAPHQL_URL, logger }), + queryCheckpointGateway: ArweaveClient.queryGatewayWith({ fetch: ctx.fetch, GRAPHQL_URL: ctx.CHECKPOINT_GRAPHQL_URL, logger }), + PROCESS_IGNORE_ARWEAVE_CHECKPOINTS: ctx.PROCESS_IGNORE_ARWEAVE_CHECKPOINTS, + IGNORE_ARWEAVE_CHECKPOINTS: ctx.IGNORE_ARWEAVE_CHECKPOINTS, + PROCESS_CHECKPOINT_TRUSTED_OWNERS: ctx.PROCESS_CHECKPOINT_TRUSTED_OWNERS, + logger + }), + saveLatestProcessMemory: AoProcessClient.saveLatestProcessMemoryWith({ + cache: wasmMemoryCache, + EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD: ctx.EAGER_CHECKPOINT_ACCUMULATED_GAS_THRESHOLD, + EAGER_CHECKPOINT_EVAL_TIME_THRESHOLD: ctx.EAGER_CHECKPOINT_EVAL_TIME_THRESHOLD, + saveCheckpoint, + logger + }), + evaluationCounter, + // gasCounter, + saveProcess: AoProcessClient.saveProcessWith({ db, logger }), + findEvaluation: AoEvaluationClient.findEvaluationWith({ db, logger }), + saveEvaluation: AoEvaluationClient.saveEvaluationWith({ db, logger }), + findBlocks: AoBlockClient.findBlocksWith({ db, logger }), + saveBlocks: AoBlockClient.saveBlocksWith({ db, logger }), + loadBlocksMeta: AoBlockClient.loadBlocksMetaWith({ fetch: ctx.fetch, GRAPHQL_URLS: BLOCK_GRAPHQL_ARRAY, pageSize: 90, logger }), + findModule: AoModuleClient.findModuleWith({ db, logger }), + saveModule: AoModuleClient.saveModuleWith({ db, logger }), + loadEvaluator: AoModuleClient.evaluatorWith({ + loadWasmModule, + evaluateWith: (prep) => primaryWorkQueue.add(() => + Promise.resolve() + /** + * prep work is deferred until the work queue tasks is executed + */ + .then(prep) + .then(([args, options]) => { + /** + * TODO: is this the best place for this? + * + * It keeps it abstracted away from business logic, + * and tied to the specific evaluator, so seems kosher, + * but also feels kind of misplaced + */ + if (args.close) return broadcastCloseStream(args.streamId) + + return primaryWorkerPool.exec('evaluate', [args], options) + }) + ), + logger + }), + findMessageBefore: AoEvaluationClient.findMessageBeforeWith({ db, logger }), + loadTimestamp: HbClient.loadTimestampWith({ fetch: ctx.fetch, logger }), + loadProcess: HbClient.loadProcessWith({ fetch: ctx.fetch, logger }), + loadMessages: HbClient.loadMessagesWith({ + hashChain: (...args) => hashChainWorker.exec('hashChain', args), + fetch: ctx.fetch, + pageSize: 1000, + logger + }), + locateProcess: HbClient.locateProcessWith({ fetch: ctx.fetch, HB_URL: ctx.HB_URL }), + isModuleMemoryLimitSupported: WasmClient.isModuleMemoryLimitSupportedWith({ PROCESS_WASM_MEMORY_MAX_LIMIT: ctx.PROCESS_WASM_MEMORY_MAX_LIMIT }), + isModuleComputeLimitSupported: WasmClient.isModuleComputeLimitSupportedWith({ PROCESS_WASM_COMPUTE_MAX_LIMIT: ctx.PROCESS_WASM_COMPUTE_MAX_LIMIT }), + isModuleFormatSupported: WasmClient.isModuleFormatSupportedWith({ PROCESS_WASM_SUPPORTED_FORMATS: ctx.PROCESS_WASM_SUPPORTED_FORMATS }), + isModuleExtensionSupported: WasmClient.isModuleExtensionSupportedWith({ PROCESS_WASM_SUPPORTED_EXTENSIONS: ctx.PROCESS_WASM_SUPPORTED_EXTENSIONS }) + }) + + const loadMessageMeta = HbClient.loadMessageMetaWith({ fetch: ctx.fetch, logger: ctx.logger }) + + const loadDryRunEvaluator = AoModuleClient.evaluatorWith({ + loadWasmModule, + evaluateWith: (prep) => dryRunWorkQueue.add(() => + Promise.resolve() + /** + * prep work is deferred until the work queue tasks is executed + */ + .then(prep) + .then(([args, options]) => + Promise.resolve() + .then(() => { + /** + * TODO: is this the best place for this? + * + * It keeps it abstracted away from business logic, + * and tied to the specific evaluator, so seems kosher, + * but also feels kind of misplaced + */ + if (args.close) return broadcastCloseStream(args.streamId) + + return dryRunWorkerPool.exec('evaluate', [args], options) + }) + .catch((err) => { + /** + * Hack to detect when the max queue size is being exceeded and to reject + * with a more informative error + */ + if (err.message.startsWith('Max queue size of')) { + const dryRunLimitErr = new Error('Dry-Run enqueue limit exceeded') + dryRunLimitErr.status = 429 + return Promise.reject(dryRunLimitErr) + } + + // Bubble as normal + return Promise.reject(err) + }) + ) + ) + }) + + const findEvaluations = AoEvaluationClient.findEvaluationsWith({ db, logger: ctx.logger }) + + /** + * The presentation side-effect, that accepts the domain + * and any additional context, and returns the app + */ + const app = (domain) => AoHttp.createAoHttp({ ...ctx, domain }) + + return { + common, + setTimeout, + clearTimeout, + metrics, + address, + gauge, + wasmMemoryCache, + loadWorkerStats, + loadMemoryUsage, + loadProcessCacheUsage, + loadMessageMeta, + loadDryRunEvaluator, + findEvaluations, + saveCheckpoint, + app + } +} diff --git a/servers/cu/src/effects/worker/evaluate.js b/servers/cu/src/effects/worker/evaluate.js index f2f3c1cce..2864797f9 100644 --- a/servers/cu/src/effects/worker/evaluate.js +++ b/servers/cu/src/effects/worker/evaluate.js @@ -128,6 +128,7 @@ export function evaluateWith ({ Messages: pathOr([], ['Messages']), Assignments: pathOr([], ['Assignments']), Spawns: pathOr([], ['Spawns']), + Patches: pathOr([], ['Patches']), Output: pipe( pathOr('', ['Output']), /** diff --git a/servers/cu/src/routes/index.js b/servers/cu/src/routes/index.js deleted file mode 100644 index ac20424d7..000000000 --- a/servers/cu/src/routes/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import { pipe } from 'ramda' - -import { withStateRoutes } from './state.js' -import { withResultRoutes } from './result.js' -import { withDryRunRoutes } from './dryRun.js' -import { withResultsRoutes } from './results.js' -import { withCronRoutes } from './cron.js' -import { withHealthcheckRoutes } from './healthcheck.js' -import { withMetricRoutes } from './metrics.js' - -export const withRoutes = pipe( - withHealthcheckRoutes, - withStateRoutes, - withResultRoutes, - withDryRunRoutes, - withResultsRoutes, - withCronRoutes, - withMetricRoutes -) diff --git a/servers/cu/src/routes/middleware/index.js b/servers/cu/src/routes/middleware/index.js deleted file mode 100644 index a31853bf8..000000000 --- a/servers/cu/src/routes/middleware/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import { compose } from 'ramda' - -import { withErrorHandler } from './withErrorHandler.js' -import { withDomain } from './withDomain.js' - -export * from './withProcessRestriction.js' -export * from './withMetrics.js' -export * from './withCuMode.js' - -/** - * A convenience method that composes common middleware needed on most routes, - * such that other routes can simply compose this one middleware. - */ -export const withMiddleware = compose( - withDomain, - withErrorHandler -) diff --git a/servers/cu/src/routes/middleware/withCuMode.js b/servers/cu/src/routes/middleware/withCuMode.js deleted file mode 100644 index 001d4f845..000000000 --- a/servers/cu/src/routes/middleware/withCuMode.js +++ /dev/null @@ -1,12 +0,0 @@ -import { config } from '../../config.js' - -const withUnitMode = (mode) => (handler) => (req, res, next) => { - const { UNIT_MODE } = config - - if (UNIT_MODE !== mode) return res.status(404).send('Not Found') - - return handler(req, res, next) -} - -export const withCuMode = withUnitMode('cu') -export const withRuMode = withUnitMode('ru') diff --git a/servers/cu/src/routes/middleware/withDomain.js b/servers/cu/src/routes/middleware/withDomain.js deleted file mode 100644 index bddac99ad..000000000 --- a/servers/cu/src/routes/middleware/withDomain.js +++ /dev/null @@ -1,39 +0,0 @@ -import { fetch, setGlobalDispatcher, Agent } from 'undici' - -import { config } from '../../config.js' -import { logger } from '../../logger.js' -import { createApis } from '../../bootstrap.js' -import { domainConfigSchema } from '../../domain/model.js' - -setGlobalDispatcher(new Agent({ - /** the timeout, in milliseconds, after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server */ - keepAliveTimeout: 30 * 1000, // 30 seconds - /** the maximum allowed `idleTimeout`, in milliseconds, when overridden by *keep-alive* hints from the server. */ - keepAliveMaxTimeout: 10 * 60 * 1000 // 10 mins -})) - -/** - * TODO: probably a better place to expose this, - * but this works for now - */ -export const domain = { - /** - * Ensure server lvl config is never exposed to domain, - * by simply parsing it out - */ - ...(domainConfigSchema.parse(config)), - fetch, - logger -} -domain.apis = await createApis(domain) - -/** - * A middleware that exposes the domain business logic to a request - * by attaching each api underneath the 'domain' field on the Request object - * - * This allows routes to be encapsulated and easily testable with unit tests - */ -export const withDomain = (handler) => (req, res) => { - req.domain = domain - return handler(req, res) -} diff --git a/servers/cu/src/routes/result.js b/servers/cu/src/routes/result.js deleted file mode 100644 index d946ce289..000000000 --- a/servers/cu/src/routes/result.js +++ /dev/null @@ -1,63 +0,0 @@ -import { compose } from 'ramda' -import { z } from 'zod' - -import { busyIn } from '../domain/utils.js' -import { withMetrics, withMiddleware, withProcessRestrictionFromQuery, withCuMode } from './middleware/index.js' -import { withInMemoryCache } from './middleware/withInMemoryCache.js' - -const inputSchema = z.object({ - messageTxId: z.string().min(1, 'a message tx id is required'), - processId: z.string().min(1, 'a process-id query parameter is required') -}) - -export const withResultRoutes = app => { - app.get( - '/result/:messageTxId', - compose( - withMiddleware, - withCuMode, - withProcessRestrictionFromQuery, - withMetrics({ tracesFrom: (req) => ({ process_id: req.query['process-id'] }) }), - withInMemoryCache({ - keyer: (req) => { - const { params: { messageTxId } } = req - return messageTxId - }, - loader: async ({ req, res }) => { - const { - params: { messageTxId }, - /** - * Client may set the 'no-busy' query parameter to any value - * to disable the busy response. - * - * Useful for MUs that would rather wait on the evaluation response - */ - query: { 'process-id': processId, 'no-busy': noBusy }, - domain: { BUSY_THRESHOLD, apis: { readResult } } - } = req - - const input = inputSchema.parse({ messageTxId, processId }) - - return busyIn( - noBusy ? 0 : BUSY_THRESHOLD, - readResult(input) - .map((output) => [output]) - .toPromise(), - () => { - res.status(202) - return [ - { message: `Evaluation of process "${input.processId}" to "${input.messageTxId || 'latest'}" is in progress.` }, - /** - * Don't store the busy message in the In-Memory cache - */ - true // noCache - ] - } - ) - } - }) - )() - ) - - return app -} diff --git a/servers/mu/package-lock.json b/servers/mu/package-lock.json index c8c6f8191..5e945fb0f 100644 --- a/servers/mu/package-lock.json +++ b/servers/mu/package-lock.json @@ -18,6 +18,7 @@ "debug": "^4.3.4", "dotenv": "^16.3.1", "express": "^4.18.2", + "http-message-signatures": "^1.0.4", "hyper-async": "^1.1.2", "lru-cache": "^10.2.0", "node-cron": "^3.0.3", @@ -1705,6 +1706,15 @@ "node": ">= 0.8" } }, + "node_modules/http-message-signatures": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/http-message-signatures/-/http-message-signatures-1.0.4.tgz", + "integrity": "sha512-gavCQWnxHFg0BVlKs6CmYK7hNSH1o0x0mHTC68yBAHYOYuTVXPv52mEE7QuT5TenfiagTdOa/zPJzen4lEX7Rg==", + "license": "ISC", + "dependencies": { + "structured-headers": "^1.0.1" + } + }, "node_modules/hyper-async": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/hyper-async/-/hyper-async-1.1.2.tgz", @@ -2582,6 +2592,16 @@ "node": ">=0.10.0" } }, + "node_modules/structured-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-1.0.1.tgz", + "integrity": "sha512-QYBxdBtA4Tl5rFPuqmbmdrS9kbtren74RTJTcs0VSQNVV5iRhJD4QlYTLD0+81SBwUQctjEQzjTRI3WG4DzICA==", + "license": "MIT", + "engines": { + "node": ">= 14", + "npm": ">=6" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -4030,6 +4050,14 @@ "toidentifier": "1.0.1" } }, + "http-message-signatures": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/http-message-signatures/-/http-message-signatures-1.0.4.tgz", + "integrity": "sha512-gavCQWnxHFg0BVlKs6CmYK7hNSH1o0x0mHTC68yBAHYOYuTVXPv52mEE7QuT5TenfiagTdOa/zPJzen4lEX7Rg==", + "requires": { + "structured-headers": "^1.0.1" + } + }, "hyper-async": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/hyper-async/-/hyper-async-1.1.2.tgz", @@ -4647,6 +4675,11 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" }, + "structured-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-1.0.1.tgz", + "integrity": "sha512-QYBxdBtA4Tl5rFPuqmbmdrS9kbtren74RTJTcs0VSQNVV5iRhJD4QlYTLD0+81SBwUQctjEQzjTRI3WG4DzICA==" + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/servers/mu/package.json b/servers/mu/package.json index 7fc1fc537..a4a6b482e 100644 --- a/servers/mu/package.json +++ b/servers/mu/package.json @@ -28,6 +28,7 @@ "debug": "^4.3.4", "dotenv": "^16.3.1", "express": "^4.18.2", + "http-message-signatures": "^1.0.4", "hyper-async": "^1.1.2", "lru-cache": "^10.2.0", "node-cron": "^3.0.3", diff --git a/servers/mu/src/config.js b/servers/mu/src/config.js index 454cab4d6..889cc948d 100644 --- a/servers/mu/src/config.js +++ b/servers/mu/src/config.js @@ -16,6 +16,18 @@ const positiveIntSchema = z.preprocess((val) => { return typeof val === 'string' ? parseInt(val.replaceAll('_', '')) : -1 }, z.number().nonnegative()) +const jsonObjectSchema = z.preprocess((val) => { + if (typeof val === 'string') { + try { + return JSON.parse(val); + } catch (error) { + return {}; // Default to an empty object if parsing fails + } + } + return val; +}, z.any()) + + /** * Some frameworks will implicitly override NODE_ENV * @@ -62,7 +74,8 @@ export const domainConfigSchema = z.object({ GET_RESULT_MAX_RETRIES: positiveIntSchema, GET_RESULT_RETRY_DELAY: positiveIntSchema, MESSAGE_RECOVERY_MAX_RETRIES: positiveIntSchema, - MESSAGE_RECOVERY_RETRY_DELAY: positiveIntSchema + MESSAGE_RECOVERY_RETRY_DELAY: positiveIntSchema, + RELAY_MAP: jsonObjectSchema }) /** @@ -112,7 +125,8 @@ const CONFIG_ENVS = { GET_RESULT_MAX_RETRIES: process.env.GET_RESULT_MAX_RETRIES || 5, GET_RESULT_RETRY_DELAY: process.env.GET_RESULT_RETRY_DELAY || 1000, MESSAGE_RECOVERY_MAX_RETRIES: process.env.MESSAGE_RECOVERY_MAX_RETRIES || 17, - MESSAGE_RECOVERY_RETRY_DELAY: process.env.MESSAGE_RECOVERY_RETRY_DELAY || 1000 + MESSAGE_RECOVERY_RETRY_DELAY: process.env.MESSAGE_RECOVERY_RETRY_DELAY || 1000, + RELAY_MAP: process.env.RELAY_MAP || '' }, production: { MODE, @@ -139,7 +153,8 @@ const CONFIG_ENVS = { GET_RESULT_MAX_RETRIES: process.env.GET_RESULT_MAX_RETRIES || 5, GET_RESULT_RETRY_DELAY: process.env.GET_RESULT_RETRY_DELAY || 1000, MESSAGE_RECOVERY_MAX_RETRIES: process.env.MESSAGE_RECOVERY_MAX_RETRIES || 17, - MESSAGE_RECOVERY_RETRY_DELAY: process.env.MESSAGE_RECOVERY_RETRY_DELAY || 1000 + MESSAGE_RECOVERY_RETRY_DELAY: process.env.MESSAGE_RECOVERY_RETRY_DELAY || 1000, + RELAY_MAP: process.env.RELAY_MAP || '' } } diff --git a/servers/mu/src/domain/api/processMsg.js b/servers/mu/src/domain/api/processMsg.js index f2364f922..b62556c80 100644 --- a/servers/mu/src/domain/api/processMsg.js +++ b/servers/mu/src/domain/api/processMsg.js @@ -19,10 +19,12 @@ export function processMsgWith ({ logger, writeDataItemArweave, isWallet, - fetchSchedulerProcess + fetchSchedulerProcess, + topUp, + RELAY_MAP }) { const buildTx = buildTxWith({ buildAndSign, logger, locateProcess, fetchSchedulerProcess, isWallet }) - const writeMessage = writeMessageTxWith({ writeDataItem, logger, writeDataItemArweave }) + const writeMessage = writeMessageTxWith({ writeDataItem, logger, writeDataItemArweave, RELAY_MAP, topUp }) const getCuAddress = getCuAddressWith({ selectNode, logger }) const pullResult = pullResultWith({ fetchResult, logger }) diff --git a/servers/mu/src/domain/clients/relay.js b/servers/mu/src/domain/clients/relay.js new file mode 100644 index 000000000..c2269ff71 --- /dev/null +++ b/servers/mu/src/domain/clients/relay.js @@ -0,0 +1,75 @@ +import { createPrivateKey } from 'node:crypto' +import { httpbis, createSigner } from 'http-message-signatures' + +const { signMessage } = httpbis + +function httpSigName (address) { + // Decode the base64 address + const decoded = Buffer.from(address, 'base64url'); + + // Get the first 8 bytes + const first8Bytes = decoded.subarray(1, 9); + + // Convert to hexadecimal + const hexString = [...first8Bytes].map(byte => byte.toString(16).padStart(2, '0')).join(''); + + return `http-sig-${hexString}`; +} + +export function topUpWith ({ fetch, logger, wallet, address, fetchTransactions }) { + const privateKey = createPrivateKey({ key: wallet, format: 'jwk' }) + const s = createSigner(privateKey, 'rsa-pss-sha512', address) + const params = ['alg', 'keyid'].sort() + + return async ({ logId, relayUrls, amount, recipientProcessId }) => { + if(relayUrls.length < 1) { + return new Error('No relay urls configured for this currency.', { cause: {} }) + } + + if(!relayUrls[0].url) { + return new Error('Relay urls improperly configured for this currency.', { cause: {} }) + } + + let processInfo = await fetchTransactions([recipientProcessId]) + let recipient = processInfo?.data?.transactions?.edges?.length >= 1 ? processInfo.data.transactions.edges[0].node.owner.address : null + + if(!recipient) { + return new Error("Invalid recipient for top up") + } + + for (const relay of relayUrls) { + let relayUrlObj = new URL(relay["url"]) + const urlString = `${relay["url"]}?amount+integer=${amount}&recipient=${recipient}` + + logger({ log: `Forwarding to relay: ${urlString}`, logId }) + + const request = { + url: new URL(urlString), + method: 'POST', + headers: { + 'amount+integer': `${amount}`, + 'recipient': `${recipient}`, + 'path': relayUrlObj.pathname, + } + } + + const { method, headers } = await signMessage({ + key: s, + fields: [ + ...Object.keys(request.headers) + ].sort(), + name: httpSigName(address), + params + }, request) + + try { + await fetch( urlString, { method, headers } ) + return { request: { method, url: urlString, headers } } + } catch(e) { + continue + } + } + + return new Error('All relay urls failed.', { cause: {} }) + } +} \ No newline at end of file diff --git a/servers/mu/src/domain/clients/relay.test.js b/servers/mu/src/domain/clients/relay.test.js new file mode 100644 index 000000000..edc4f8483 --- /dev/null +++ b/servers/mu/src/domain/clients/relay.test.js @@ -0,0 +1,76 @@ +import { createPrivateKey, createPublicKey } from 'node:crypto' +import { httpbis, createSigner, createVerifier } from 'http-message-signatures' +import Arweave from 'arweave' +import { describe, it } from 'node:test' + +import { topUpWith } from './relay.js' + +const { verifyMessage } = httpbis +const arweave = Arweave.init() +const verifiers = new Map() + +let { wallet, address } = await arweave.wallets.generate() + .then((jwk) => arweave.wallets.getAddress(jwk) + .then(address => { + return { + address, + privateKey: createPrivateKey({ key: jwk, format: 'jwk' }), + publicKey: createPublicKey({ key: jwk, format: 'jwk' }), + jwk + }; + }) + ) + .then(({ address, publicKey, jwk }) => { + const verifier = createVerifier(publicKey, 'rsa-pss-sha512') + verifiers.set(address, { verify: verifier }) + return { wallet: jwk, address } + }) + +describe('topUpWith function', function () { + let fetch = async (_url, options) => ({ ok: true, status: 200, json: async () => ({ success: true }), ...options }) + const logger = () => undefined + logger.tap = () => (args) => { + return args + } + const fetchTransactions = async () => { + return { + data: { + transactions: { + edges: [ + { + node: { + owner: { + address: "asdf" + } + } + } + ] + } + } + } + } + let topUp = topUpWith({ fetch, logger, wallet, address, fetchTransactions }) + + it('should correctly sign and verify a request', async function () { + const params = { + logId: 'test-log', + relayUrls: [{ url: 'https://relay1.example/path/topup' }, { url: 'https://relay2.example/path/topup' }], + amount: 100, + recipient: 'recipient-process-id' + } + + const result = await topUp(params) + + const { request } = result + + const verified = await verifyMessage({ + keyLookup: (params) => { + return verifiers.get(params.keyid) + } + }, request) + + if (!verified) { + throw new Error('Signature verification failed') + } + }) +}) diff --git a/servers/mu/src/domain/clients/worker.js b/servers/mu/src/domain/clients/worker.js index f38fc2d39..108cafb9b 100644 --- a/servers/mu/src/domain/clients/worker.js +++ b/servers/mu/src/domain/clients/worker.js @@ -46,13 +46,13 @@ async function whileTrue (fn) { } const broadcastLogger = createBroadcastLogger({ namespace: 'mu-worker-broadcast', config }) - export const domain = { ...(domainConfigSchema.parse(config)), fetch, logger: broadcastLogger } + /** * This program utilizes the business logic for * processing results but since the worker is also diff --git a/servers/mu/src/domain/index.js b/servers/mu/src/domain/index.js index 0aa2c1854..45967076a 100644 --- a/servers/mu/src/domain/index.js +++ b/servers/mu/src/domain/index.js @@ -7,6 +7,7 @@ import warpArBundles from 'warp-arbundles' import { connect as schedulerUtilsConnect } from '@permaweb/ao-scheduler-utils' import { fromPromise } from 'hyper-async' import workerpool from 'workerpool' +import Arweave from 'arweave' import cuClient from './clients/cu.js' import schedulerClient from './clients/scheduler.js' @@ -18,6 +19,7 @@ import * as MetricsClient from './clients/metrics.js' import * as SqliteClient from './clients/sqlite.js' import cronClient, { deleteCronProcessWith, getCronProcessCursorWith, saveCronProcessWith, updateCronProcessCursorWith } from './clients/cron.js' import { readTracesWith } from './clients/tracer.js' +import * as RelayClient from './clients/relay.js' import { processMsgWith } from './api/processMsg.js' import { processSpawnWith } from './api/processSpawn.js' @@ -73,6 +75,8 @@ const errorStageGauge = MetricsClient.gaugeWith({})({ labelNames: ['stage', 'type'] }) +const arweave = Arweave.init() + /** * A set of apis used by the express server * to send initial items and start the message @@ -352,10 +356,13 @@ export const createResultApis = async (ctx) => { const GRAPHQL_URL = ctx.GRAPHQL_URL const ARWEAVE_URL = ctx.ARWEAVE_URL const SPAWN_PUSH_ENABLED = ctx.SPAWN_PUSH_ENABLED + const RELAY_MAP = ctx.RELAY_MAP const logger = ctx.logger const fetch = ctx.fetch + const walletAddress = await arweave.wallets.getAddress(MU_WALLET) + const fetchWithCache = cuFetchWithCache({ fetch, cache: muRedirectCache, @@ -385,7 +392,15 @@ export const createResultApis = async (ctx) => { buildAndSign: signerClient.buildAndSignWith({ MU_WALLET, logger: processMsgLogger }), fetchResult: cuClient.resultWith({ fetch: fetchWithCache, histogram, CU_URL, logger: processMsgLogger }), isWallet: gatewayClient.isWalletWith({ fetch, histogram, ARWEAVE_URL, logger: processMsgLogger, setById, getById }), - writeDataItemArweave: uploaderClient.uploadDataItemWith({ UPLOADER_URL, logger: processMsgLogger, fetch, histogram }) + writeDataItemArweave: uploaderClient.uploadDataItemWith({ UPLOADER_URL, logger: processMsgLogger, fetch, histogram }), + RELAY_MAP, + topUp: RelayClient.topUpWith({ + fetch, + logger: processMsgLogger, + wallet: MU_WALLET, + address: walletAddress, + fetchTransactions: gatewayClient.fetchTransactionDetailsWith({ fetch, GRAPHQL_URL }) + }), }) const processSpawnLogger = logger.child('processSpawn') diff --git a/servers/mu/src/domain/lib/write-message-tx.js b/servers/mu/src/domain/lib/write-message-tx.js index 376ce7ea4..e159b7cd4 100644 --- a/servers/mu/src/domain/lib/write-message-tx.js +++ b/servers/mu/src/domain/lib/write-message-tx.js @@ -1,5 +1,5 @@ -import { Resolved, fromPromise } from 'hyper-async' -import { __, assoc } from 'ramda' +import { Rejected, Resolved, fromPromise, of } from 'hyper-async' +import { __, assoc, T } from 'ramda' import z from 'zod' import { checkStage, setStage } from '../utils.js' import { uploadDataItemSchema, writeDataItemSchema } from '../dal.js' @@ -19,10 +19,11 @@ const ctxSchemaArweave = z.object({ }).passthrough() export function writeMessageTxWith (env) { - let { logger, writeDataItem, writeDataItemArweave } = env + let { logger, writeDataItem, writeDataItemArweave, topUp, RELAY_MAP } = env writeDataItem = fromPromise(writeDataItemSchema.implement(writeDataItem)) writeDataItemArweave = fromPromise(uploadDataItemSchema.implement(writeDataItemArweave)) + topUp = fromPromise(topUp) return (ctx) => { /* @@ -31,34 +32,69 @@ export function writeMessageTxWith (env) { */ if (!checkStage('write-message')(ctx)) return Resolved(ctx) - if (ctx.schedLocation) { - ctx = setStage('write-message', 'write-message-su')(ctx) - if (!checkStage('write-message-su')(ctx)) return Resolved(ctx) + return of() + .chain(() => { + if(RELAY_MAP && Object.keys(RELAY_MAP).includes(ctx.tx.processId)) { + if(ctx.cachedMsg?.msg?.Tags?.find((t) => t.name === 'Action' && t.value === 'Credit-Notice')) { + let sender = ctx.cachedMsg?.msg?.Tags?.find((t) => t.name === 'Sender')?.value; + let amount = ctx.cachedMsg?.msg?.Tags?.find((t) => t.name === 'Quantity')?.value; - return writeDataItem({ suUrl: ctx.schedLocation.url, data: ctx.tx.data.toString('base64'), logId: ctx.logId }) - .map(assoc('schedulerTx', __, ctx)) - .map(ctxSchema.parse) - .bimap( - (e) => { - return new Error(e, { cause: { ...ctx, stage: 'write-message' } }) - }, - logger.tap({ log: 'Added schedulerTx to ctx' }) - ) - } else { - ctx = setStage('write-message', 'write-message-arweave')(ctx) + if(!amount || !sender) { + return Rejected(new Error('Must set Sender and Quantity to top up.', { cause: ctx })) + } - if (!checkStage('write-message-arweave')(ctx)) return Resolved(ctx) + if(!Object.keys(RELAY_MAP).includes("ALLOWED_CURRENCIES")) { + return Rejected(new Error('No allowed currencies configured on this MU.', { cause: ctx })) + } - return writeDataItemArweave(ctx.tx.data) - .map(assoc('arweaveTx', __, ctx)) - .map(ctxSchemaArweave.parse) - .bimap( - (e) => { - return new Error(e, { cause: { ...ctx, stage: 'write-message' } }) - }, - logger.tap({ log: 'Added "arweaveTx" to ctx' }) - ) - } + if(!RELAY_MAP["ALLOWED_CURRENCIES"].includes("ALL")) { + if(!RELAY_MAP["ALLOWED_CURRENCIES"].includes(ctx.cachedMsg.fromProcessId)) { + return Rejected(new Error('This currency is not supported on this MU.', { cause: ctx })) + } + } + + return topUp({ctx, relayUrls: RELAY_MAP[ctx.tx.processId], amount, recipientProcessId: sender}) + .bimap( + (e) => { + return new Error(e, { cause: { ...ctx, stage: 'write-message' } }) + }, + logger.tap({ log: `Topped up relay id ${ctx.tx.processId}` }) + ) + } + } + return Resolved() + }) + .chain(() => { + if (ctx.schedLocation) { + ctx = setStage('write-message', 'write-message-su')(ctx) + + if (!checkStage('write-message-su')(ctx)) return Resolved(ctx) + + return writeDataItem({ suUrl: ctx.schedLocation.url, data: ctx.tx.data.toString('base64'), logId: ctx.logId }) + .map(assoc('schedulerTx', __, ctx)) + .map(ctxSchema.parse) + .bimap( + (e) => { + return new Error(e, { cause: { ...ctx, stage: 'write-message' } }) + }, + logger.tap({ log: 'Added schedulerTx to ctx' }) + ) + } else { + ctx = setStage('write-message', 'write-message-arweave')(ctx) + + if (!checkStage('write-message-arweave')(ctx)) return Resolved(ctx) + + return writeDataItemArweave(ctx.tx.data) + .map(assoc('arweaveTx', __, ctx)) + .map(ctxSchemaArweave.parse) + .bimap( + (e) => { + return new Error(e, { cause: { ...ctx, stage: 'write-message' } }) + }, + logger.tap({ log: 'Added "arweaveTx" to ctx' }) + ) + } + }) } -} +} \ No newline at end of file diff --git a/servers/mu/src/domain/lib/write-message-tx.test.js b/servers/mu/src/domain/lib/write-message-tx.test.js index 682f3a9d8..8672049d1 100644 --- a/servers/mu/src/domain/lib/write-message-tx.test.js +++ b/servers/mu/src/domain/lib/write-message-tx.test.js @@ -77,4 +77,126 @@ describe('writeMessageTxWith', () => { timestamp: 1234567 }) }) + + test('top up a process from a relay', async () => { + const writeMessageTx = writeMessageTxWith({ + writeDataItem: async ({ data }) => { + assert.equal(data, 'raw-123') + + return { + id: 'id-3', + timestamp: 1234567 + } + }, + logger, + writeDataItemArweave: async (buffer) => { + return { + id: 'arweave-id-3', + timestamp: 1234567 + } + }, + RELAY_MAP: { + 'ALLOWED_CURRENCIES': ["fromid"], + 'mappedid1': [{ 'url': 'url1' }], + 'mappedid2': [{ 'url': 'url1' }] + }, + topUp: async (ctx) => { + assert.deepStrictEqual(ctx.relayUrls, [{ 'url': 'url1' }]) + return {} + }, + }) + + const result = await writeMessageTx({ + tx: { + processId: 'mappedid1', + id: 'id-2', + data: 'raw-123' + }, + schedLocation: { + url: 'https://foo.bar' + }, + cachedMsg: { + fromProcessId: 'fromid', + msg: { + Tags: [{ + name: 'Action', + value: 'Credit-Notice' + }, + { + name: 'Sender', + value: 's1' + }, + { + name: 'Quantity', + value: '5000' + }] + } + } + }).toPromise() + + assert.deepStrictEqual(result.schedulerTx, { + id: 'id-3', + timestamp: 1234567 + }) + }) + + test('top up a process from a relay', async () => { + const writeMessageTx = writeMessageTxWith({ + writeDataItem: async ({ data }) => { + assert.equal(data, 'raw-123') + + return { + id: 'id-3', + timestamp: 1234567 + } + }, + logger, + writeDataItemArweave: async (buffer) => { + return { + id: 'arweave-id-3', + timestamp: 1234567 + } + }, + RELAY_MAP: { + 'ALLOWED_CURRENCIES': ["ALL"], + 'mappedid1': { 'url': 'url1' }, + 'mappedid2': { 'url': 'url1' } + }, + topUp: async () => { + assert.fail('Should not top up this process') + }, + }) + + const result = await writeMessageTx({ + tx: { + processId: 'nonmappedid1', + id: 'id-2', + data: 'raw-123' + }, + schedLocation: { + url: 'https://foo.bar' + }, + cachedMsg: { + msg: { + Tags: [{ + name: 'Action', + value: 'Credit-Notice' + }, + { + name: 'Sender', + value: 's1' + }, + { + name: 'Quantity', + value: '5000' + }] + } + } + }).toPromise() + + assert.deepStrictEqual(result.schedulerTx, { + id: 'id-3', + timestamp: 1234567 + }) + }) }) diff --git a/servers/su/DockerfileMig b/servers/su/DockerfileCli similarity index 82% rename from servers/su/DockerfileMig rename to servers/su/DockerfileCli index ef8518007..6ed5c1958 100644 --- a/servers/su/DockerfileMig +++ b/servers/su/DockerfileCli @@ -1,8 +1,8 @@ # Stage 1: Build the dynamic binary -FROM rust:1.75.0 as mig-builder +FROM rust:1.75.0 as cli-builder # Set the working directory in the container -WORKDIR /usr/src/mig +WORKDIR /usr/src/cli # Install required dependencies for building RUN apt-get update && apt-get install -y \ @@ -18,11 +18,11 @@ COPY Cargo.toml Cargo.lock ./ RUN mkdir src && \ echo "fn main() {}" > src/main.rs && \ cargo build --release && \ - rm -f target/release/deps/mig* + rm -f target/release/deps/cli* # Now copy the actual source code and build the application COPY src ./src COPY migrations ./migrations -RUN cargo build --release --bin mig +RUN cargo build --release --bin cli -# The final output binary will be in /usr/src/mig/target/release/mig +# The final output binary will be in /usr/src/mig/target/release/cli diff --git a/servers/su/DockerfileMigLocal b/servers/su/DockerfileMigLocal deleted file mode 100644 index 32d05c8ba..000000000 --- a/servers/su/DockerfileMigLocal +++ /dev/null @@ -1,28 +0,0 @@ -# Stage 1: Build the dynamic binary -FROM rust:1.75.0 as mig-local-builder - -# Set the working directory in the container -WORKDIR /usr/src/mig_local - -# Install required dependencies for building -RUN apt-get update && apt-get install -y \ - llvm-dev \ - libclang-dev \ - clang \ - librocksdb-dev - -# Copy the manifests -COPY Cargo.toml Cargo.lock ./ - -# This step is to cache your dependencies -RUN mkdir src && \ - echo "fn main() {}" > src/main.rs && \ - cargo build --release && \ - rm -f target/release/deps/mig_local* - -# Now copy the actual source code and build the application -COPY src ./src -COPY migrations ./migrations -RUN cargo build --release --bin mig_local - -# The final output binary will be in /usr/src/mig/target/release/mig diff --git a/servers/su/README.md b/servers/su/README.md index 620ff103f..4d069fdae 100644 --- a/servers/su/README.md +++ b/servers/su/README.md @@ -54,6 +54,8 @@ Create a .env file with the following variables, or set them in the OS: - `PROCESS_CACHE_SIZE` max size of the in memory cache of processes held by the data store - `ENABLE_PROCESS_ASSIGNMENT` enables AOP-6 boot loader, if enabled, the Process on a new spawn will become the first Message/Nonce in its message list. It will get an Assignment. - `ARWEAVE_URL_LIST` list of arweave urls that have tx access aka url/txid returns the tx. Used by gateway calls for checking transactions etc... +- `SU_FILE_SYNC_DB_DIR` a directory for a RocksDB backup that will hold the full binary files that are the bundles, messages, and assignments. Only used by the cli binary. +- `SU_INDEX_SYNC_DB_DIR` a directory for a RocksDB backup that will hold an index of Processes and Messages for ordering and querying. Only used by the cli binary. ## Experimental environment variables To use the expirimental fully local storage system set the following evnironment variables. @@ -165,35 +167,36 @@ docker run --env-file .env.router -v ./.wallet.json:/app/.wallet.json -v ./sched Over time the su database has evolved. It started as only Postgres then went to Postgres + RocksDB for performance enhancement. It now has a purely RocksDB implementation. For existing su's that already have data, you can follow the below to migration processes to bring it up to date to the latest implementation. + +Building the cli binary, delete all su images and containers if you have previously run this, then run +```sh +docker system prune -a +docker build --target cli-builder -t cli-binary -f DockerfileCli . +docker create --name temp-container-cli cli-binary +docker cp temp-container-cli:/usr/src/cli/target/release/cli . +``` + ### Migrating data to disk for an existing su instance If a su has been running using postgres for sometime there may be performance issues. Writing to and reading files from disk has been added. In order to switch this on set the environment variables - `USE_DISK` whether or not to read and write binary files from/to the disk/rocksdb. If the su has already been running for a while the data will need to be migrated using the mig binary before turning this on. - `SU_DATA_DIR` the data directory on disk where the su will read from and write binaries to -Then the `mig` binary can be used to migrate data in segments from the existing db. It will currently only migrate the message files to the disk. It takes a range which represents a range in the messages table. So 0-500 would grab the first 500 messages from the messages table and write them to rocksdb on the disk and so on. Just 0 as an argument would read the whole table, the range is so you can run multiple instances of the program on different segments of data for faster migration. To read from record 1000 to the end of the table you would just send 1000 as an argument. +Then the `cli` binary can be used to migrate data in segments from the existing db. It will currently only migrate the message files to the disk. It takes a range which represents a range in the messages table. So 0-500 would grab the first 500 messages from the messages table and write them to rocksdb on the disk and so on. Just 0 as an argument would read the whole table, the range is so you can run multiple instances of the program on different segments of data for faster migration. To read from record 1000 to the end of the table you would just send 1000 as an argument. Migrate the entire messages table to disk ```sh -./mig 0 +./cli migrate_to_disk 0 ``` Migrate the first 1000 messages ```sh -./mig 0-1000 +./cli migrate_to_disk 0-1000 ``` Migrate from 1000 to the end of the table ```sh -./mig 1000 -``` - -Building the mig binary, delete all su images and containers if you have previously run this, then run -```sh -docker system prune -a -docker build --target mig-builder -t mig-binary -f DockerfileMig . -docker create --name temp-container-mig mig-binary -docker cp temp-container-mig:/usr/src/mig/target/release/mig . +./cli migrate_to_disk 1000 ``` ### Migrating data to fully local data store @@ -203,21 +206,26 @@ If a su has been running using postgres + rocksdb using the above migration, it - `SU_FILE_DB_DIR` a directory for a RocksDB instance that will hold the full binary files that are the bundles, messages, and assignments. - `SU_INDEX_DB_DIR` a directory for a RocksDB instance that will hold an index of Processes and Messages for ordering and querying. -Then the `mig_local` binary can be used to migrate data in segments from the existing source. Note that you must have already run the above `mig` binary before running `mig_local` will work. It cannot migrate from a purely postgres implementation. So to get to this point if the su was running on only postgres, first follow the above steps using the `mig` binary. And then follow the `mig_local` steps. +Then the `cli` binary can be used to migrate data in segments from the existing source. Note that you must have already run the above `cli` binary with `migrate_to_disk` before running it with `migrate_to_local` will work. It cannot migrate from a purely postgres implementation. So to get to this point if the su was running on only postgres, first follow the above steps using the `cli` binary with `migrate_to_disk`. And then follow the `migrate_to_local` steps. Migrate all Messages and Processes to RocksDB ```sh -./mig_local +./cli migrate_to_local ``` -Building the mig_local binary, delete all su images and containers if you have previously run this, then run + +### Keeping a backup database in sync with a running SU +There is a program available to keep another directory in sync with a running SU, copy the environment variables from the running su and add these, and then run the cli binary with the `sync_local_drives` argument. This is to keep 2 fully local data stores in sync. + +- `SU_FILE_SYNC_DB_DIR` a directory for a RocksDB backup that will hold the full binary files that are the bundles, messages, and assignments. +- `SU_INDEX_SYNC_DB_DIR` a directory for a RocksDB backup that will hold an index of Processes and Messages for ordering and querying. + +Keep the sync db directories up to date with a running SU on a 5 second interval ```sh -docker system prune -a -docker build --target mig-local-builder -t mig-local-binary -f DockerfileMigLocal . -docker create --name temp-container-mig-local mig-local-binary -docker cp temp-container-mig-local:/usr/src/mig_local/target/release/mig_local . +./cli sync_local_drives 5 ``` + # System Requirements for SU + SU-R cluster The SU + SU-R runs as a cluster of nodes. The SU-R acts as a redirector to a set of SU's. In order to run the cluster you need at least 2 nodes. 1 SU and one SU-R (a SU running in router mode). In order for the SU-R to initialize properly when it boots up, it has to be started up with a configured set of SU's in the SCHEDULER_LIST_PATH environment variable. diff --git a/servers/su/mig_local b/servers/su/cli similarity index 63% rename from servers/su/mig_local rename to servers/su/cli index 467e643b5..207e8b17d 100644 Binary files a/servers/su/mig_local and b/servers/su/cli differ diff --git a/servers/su/mig b/servers/su/mig deleted file mode 100644 index 7b183d897..000000000 Binary files a/servers/su/mig and /dev/null differ diff --git a/servers/su/src/bin/cli.rs b/servers/su/src/bin/cli.rs new file mode 100644 index 000000000..4b2f5489e --- /dev/null +++ b/servers/su/src/bin/cli.rs @@ -0,0 +1,46 @@ +use std::env; +use std::io; +use su::domain::sync_local_drives; +use su::domain::migrate_to_local; +use su::domain::migrate_to_disk; + +#[tokio::main] +async fn main() -> io::Result<()> { + let args: Vec = env::args().collect(); + + if args.len() < 2 { + eprintln!("Usage: {} ", args[0]); + eprintln!("Available functions: migrate_to_disk, migrate_to_local, sync_local_drives"); + return Ok(()); + } + + let interval = if args.len() >= 3 { + match args[2].parse::() { + Ok(val) => val, + Err(_) => { + eprintln!("Invalid interval: {}. Using default (5 seconds).", args[3]); + 5 + } + } + } else { + 5 + }; + + match args[1].as_str() { + "migrate_to_disk" => { + migrate_to_disk().await.unwrap(); + } + "migrate_to_local" => { + migrate_to_local().await.unwrap(); + } + "sync_local_drives" => { + sync_local_drives(interval).await.unwrap(); + } + _ => { + eprintln!("Invalid function name: {}", args[1]); + eprintln!("Available functions: migrate_to_disk, migrate_to_local, sync_local_drives"); + } + } + + Ok(()) +} diff --git a/servers/su/src/bin/mig.rs b/servers/su/src/bin/mig.rs deleted file mode 100644 index d1d3165a7..000000000 --- a/servers/su/src/bin/mig.rs +++ /dev/null @@ -1,8 +0,0 @@ -use std::io; - -use su::domain::migrate_to_disk; - -#[tokio::main] -async fn main() -> io::Result<()> { - migrate_to_disk().await -} diff --git a/servers/su/src/bin/mig_local.rs b/servers/su/src/bin/mig_local.rs deleted file mode 100644 index 679c962c1..000000000 --- a/servers/su/src/bin/mig_local.rs +++ /dev/null @@ -1,8 +0,0 @@ -use std::io; - -use su::domain::migrate_to_local; - -#[tokio::main] -async fn main() -> io::Result<()> { - migrate_to_local().await -} diff --git a/servers/su/src/domain/clients/local_store/mod.rs b/servers/su/src/domain/clients/local_store/mod.rs index d79564fb3..95feff2b8 100644 --- a/servers/su/src/domain/clients/local_store/mod.rs +++ b/servers/su/src/domain/clients/local_store/mod.rs @@ -1,3 +1,4 @@ pub mod migration; +pub mod sync_local; pub mod store; pub mod tests; diff --git a/servers/su/src/domain/clients/local_store/store.rs b/servers/su/src/domain/clients/local_store/store.rs index 8fc867a39..6eb5de074 100644 --- a/servers/su/src/domain/clients/local_store/store.rs +++ b/servers/su/src/domain/clients/local_store/store.rs @@ -69,6 +69,40 @@ impl LocalStoreClient { }) } + pub fn new_read_only(file_db_dir: &String, index_db_dir: &String) -> Result { + let logger = SuLog::init(); + + let mut opts = Options::default(); + opts.create_if_missing(true); + opts.set_enable_blob_files(true); + opts.set_blob_file_size(5 * 1024 * 1024 * 1024); // 5GB max + + /* + This low value makes it so pretty much all + the values are stored in blobs on the file db + */ + opts.set_min_blob_size(1024); + + let file_db = DB::open_for_read_only(&opts, file_db_dir, false)?; + + let mut opts_index = Options::default(); + opts_index.create_if_missing(true); + opts_index.create_missing_column_families(true); + + let cfs = LocalStoreClient::generate_cfs(); + + let index_db = match DB::open_cf_with_opts_for_read_only(&opts_index, &index_db_dir, cfs, false) { + Ok(_db) => _db, + Err(e) => panic!("failed to open cf with options: {}", e), + }; + + Ok(LocalStoreClient { + _logger: logger, + file_db, + index_db, + }) + } + /* Generate a column family for each prefix type in the index. This allows us to query them all seperately without conflicting results. @@ -511,7 +545,6 @@ impl DataStore for LocalStoreClient { */ for (_, assignment_id) in paginated_keys { let assignment_key = self.msg_assignment_key(&assignment_id); - /* It is possible the file isnt finished saving and available on the file db yet that is why this retry loop diff --git a/servers/su/src/domain/clients/local_store/sync_local.rs b/servers/su/src/domain/clients/local_store/sync_local.rs new file mode 100644 index 000000000..e3631a924 --- /dev/null +++ b/servers/su/src/domain/clients/local_store/sync_local.rs @@ -0,0 +1,204 @@ +use std::io; +use std::sync::Arc; +use tokio::time::{ sleep, Duration }; + +use crate::domain::config::AoConfig; +use crate::domain::core::dal::{DataStore, Process, StoreErrorType, Gateway}; +use crate::domain::flows::msg_deephash; +use super::super::gateway::ArweaveGateway; + +/* + This program allows us to keep another RocksDB instance + in sync with a live and running SU in order to create + an up to date backup of the database. + + This script is using .unwrap() everywhere because we want + to fail loudly if something goes wrong +*/ + +pub async fn sync_local_drives(interval: u64) -> io::Result<()> { + let config = AoConfig::new(None).expect("Failed to read configuration"); + let gateway: Arc = Arc::new( + ArweaveGateway::new() + .await + .expect("Failed to initialize gateway"), + ); + + // New database (writable) + let write_sync_store = Arc::new( + super::store::LocalStoreClient::new( + &config.su_file_sync_db_dir, + &config.su_index_sync_db_dir + ).expect("Failed to create LocalStoreClient"), + ); + + println!( + "Initialized a write store to sync at {} and {}", + config.su_file_sync_db_dir, + config.su_index_sync_db_dir + ); + + loop { + /* + Old database (read-only) + initialize every iteration for a fresh snapshot + */ + let read_only_local_store = Arc::new( + super::store::LocalStoreClient::new_read_only( + &config.su_file_db_dir, + &config.su_index_db_dir + ).expect("Failed to create LocalStoreClient"), + ); + + // Fetch all processes from the old database + let process_keys = get_all_processes(&read_only_local_store).await.unwrap(); + let process_keys_sync = get_all_processes(&write_sync_store).await.unwrap(); + + for process_id in process_keys { + /* + If the new database has the process get the latest message + otherwise create the process and there is not latest message + in the new db so we will start from the beginning + */ + let latest_message_synced = match process_keys_sync.contains(&process_id) { + true => { + match write_sync_store.get_latest_message(&process_id).await { + Ok(m) => m, + Err(_) => None + } + }, + false => { + /* + Read the existing process information from the running + database, aka the one the running su is using + */ + let process_to_sync = read_only_local_store + .get_process(&process_id) + .await + .unwrap(); + + let assignment_key = format!( + "process_assignment:{}", + process_to_sync.assignment_id().unwrap() + ); + + let process_bundle = read_only_local_store + .file_db.get(assignment_key.as_bytes()) + .unwrap() + .unwrap(); + + write_sync_store.save_process(&process_to_sync, &process_bundle).unwrap(); + + None + } + }; + + let process = write_sync_store.get_process(&process_id).await.unwrap(); + + let mut from = match latest_message_synced { + Some(message) => { + Some(message.timestamp().unwrap().to_string()) + }, + None => { + None + } + }; + + let mut has_next_page = true; + + while has_next_page { + let mut messages_fetch = read_only_local_store + .get_messages(&process, &from, &None, &None) + .await + .unwrap(); + + if messages_fetch.edges.len() >= 1 { + if let Some(m) = &messages_fetch.edges[0].node.message { + if let Some(type_tag) = m.tags.iter().find(|t| t.name == "Type") { + if type_tag.value == "Process" { + messages_fetch.edges.remove(0); + } + } + } + } + + if messages_fetch.edges.len() > 0 { + println!( + "Syncing {} messages for process {} ...", + messages_fetch.edges.len(), + process_id + ); + } + + for message in messages_fetch.edges.iter() { + let gw = Arc::clone(&gateway); + + let assignment_key = format!( + "message_assignment:{}", + message.node.assignment_id().unwrap() + ); + + let bundle_data_item = read_only_local_store.file_db.get( + assignment_key.as_bytes() + ).unwrap().unwrap(); + + match write_sync_store.get_message( + &message.node.assignment_id().unwrap() + ) { + Err(StoreErrorType::NotFound(_)) => { + let deep_hash = msg_deephash( + gw, + &message.node, + &bundle_data_item + ).await.unwrap(); + + write_sync_store.save_message( + &message.node, + &bundle_data_item, + Some(&deep_hash) + ).await.unwrap(); + }, + Ok(_) => (), + Err(_) => panic!("error querying for old message") + } + } + + if let Some(last) = messages_fetch.edges.last() { + from = Some(last.cursor.clone()); + } + + has_next_page = messages_fetch.page_info.has_next_page; + } + } + + /* + Important, sleep so we dont use the CPU to + much with the loop + */ + sleep(Duration::from_secs(interval)).await; + } +} + +pub async fn get_all_processes( + store: &Arc +) -> Result, StoreErrorType> { + let cf = store.index_db.cf_handle("process").ok_or_else(|| { + StoreErrorType::DatabaseError("Column family 'process' not found".to_string()) + })?; + + let iter = store.index_db.iterator_cf(cf, rocksdb::IteratorMode::Start); + let mut processes = Vec::new(); + + for item in iter { + let (_key, assignment_id_bytes) = item?; + let assignment_id = String::from_utf8(assignment_id_bytes.to_vec())?; + let assignment_key = format!("process_assignment:{}", assignment_id); + + if let Some(process_bundle) = store.file_db.get(assignment_key.as_bytes())? { + let process = Process::from_bytes(process_bundle)?; + processes.push(process.process.process_id); + } + } + + Ok(processes) +} \ No newline at end of file diff --git a/servers/su/src/domain/clients/store.rs b/servers/su/src/domain/clients/store.rs index 2368b3f39..528a90b20 100644 --- a/servers/su/src/domain/clients/store.rs +++ b/servers/su/src/domain/clients/store.rs @@ -1633,7 +1633,7 @@ pub async fn migrate_to_disk() -> io::Result<()> { .expect("Failed to connect to bytestore"); let args: Vec = env::args().collect(); - let range: &String = args.get(1).expect("Range argument not provided"); + let range: &String = args.get(2).expect("Range argument not provided"); let parts: Vec<&str> = range.split('-').collect(); let from = parts[0].parse().expect("Invalid starting offset"); let to = if parts.len() > 1 { diff --git a/servers/su/src/domain/config.rs b/servers/su/src/domain/config.rs index 804a98605..dae4d53a9 100644 --- a/servers/su/src/domain/config.rs +++ b/servers/su/src/domain/config.rs @@ -40,10 +40,13 @@ pub struct AoConfig { pub su_file_db_dir: String, pub su_index_db_dir: String, + pub su_file_sync_db_dir: String, + pub su_index_sync_db_dir: String, + pub enable_deep_hash_checks: bool, } -fn get_db_dirs() -> (String, String) { +fn get_db_dirs() -> (String, String, String, String) { // Get the user's home directory based on platform let home_dir = if cfg!(target_os = "windows") { env::var("USERPROFILE").unwrap_or_else(|_| ".".to_string()) @@ -71,7 +74,25 @@ fn get_db_dirs() -> (String, String) { } }; - (su_file_db_dir, su_index_db_dir) + let su_file_sync_db_dir = match env::var("SU_FILE_SYNC_DB_DIR") { + Ok(val) => val, + Err(_) => { + let mut path = PathBuf::from(&home_dir); + path.push("sudatasync"); + path.to_string_lossy().to_string() + } + }; + + let su_index_sync_db_dir = match env::var("SU_INDEX_SYNC_DB_DIR") { + Ok(val) => val, + Err(_) => { + let mut path = PathBuf::from(&home_dir); + path.push("suindexsync"); + path.to_string_lossy().to_string() + } + }; + + (su_file_db_dir, su_index_db_dir, su_file_sync_db_dir, su_index_sync_db_dir) } impl AoConfig { @@ -140,7 +161,7 @@ impl AoConfig { Ok(val) => val == "true", Err(_e) => false, }; - let (su_file_db_dir, su_index_db_dir) = get_db_dirs(); + let (su_file_db_dir, su_index_db_dir, su_file_sync_db_dir, su_index_sync_db_dir) = get_db_dirs(); let enable_deep_hash_checks = match env::var("ENABLE_DEEP_HASH_CHECKS") { Ok(val) => val == "true", Err(_e) => false, @@ -167,7 +188,9 @@ impl AoConfig { use_local_store, su_file_db_dir, su_index_db_dir, - enable_deep_hash_checks + enable_deep_hash_checks, + su_file_sync_db_dir, + su_index_sync_db_dir }) } } diff --git a/servers/su/src/domain/core/flows.rs b/servers/su/src/domain/core/flows.rs index 56fb9fcdf..3504a8597 100644 --- a/servers/su/src/domain/core/flows.rs +++ b/servers/su/src/domain/core/flows.rs @@ -9,7 +9,7 @@ use simd_json::to_string as simd_to_string; use super::builder::Builder; use super::json::{Message, Process}; use super::scheduler; -use super::bytes::DataItem; +use super::bytes::{DataBundle, DataItem}; use super::dal::{ Config, CoreMetrics, DataStore, Gateway, Log, RouterDataStore, Signer, Uploader, Wallet, @@ -494,3 +494,44 @@ pub async fn health(deps: Arc) -> Result { Err(e) => Err(format!("{:?}", e)), } } + +pub async fn msg_deephash( + gateway: Arc, + message: &Message, + bundle_bytes: &Vec +) -> Result { + let bundle_data_item = match DataItem::from_bytes(bundle_bytes.clone()) { + Ok(b) => b, + Err(_) => { return Err("Error parsing bundle for message deephash".to_string()); } + }; + let data_bytes = match bundle_data_item.data_bytes() { + Some(b) => b, + None => { return Err("Error parsing bundle for message deephash".to_string()); } + }; + let bundle = match DataBundle::from_bytes(&data_bytes) { + Ok(b) => b, + Err(_) => { return Err("Error parsing bundle for message deephash".to_string()); } + }; + match &message.message { + Some(_) => { + let mut message_item = bundle.items[1].clone(); + match message_item.deep_hash() { + Ok(d) => Ok(d), + Err(_) => return Err("Unable to calculate deep hash".to_string()) + } + }, + None => { + let message_id = &message.message_id()?; + let gateway_tx = gateway.gql_tx(&message_id).await?; + let tx_data = gateway.raw(&message_id).await?; + let dh = DataItem::deep_hash_fields( + gateway_tx.recipient, + gateway_tx.anchor, + gateway_tx.tags, + tx_data, + ) + .map_err(|_| "Unable to calculate deep hash".to_string())?; + Ok(dh) + } + } +} \ No newline at end of file diff --git a/servers/su/src/domain/mod.rs b/servers/su/src/domain/mod.rs index acaecdd0a..f5646a8cd 100644 --- a/servers/su/src/domain/mod.rs +++ b/servers/su/src/domain/mod.rs @@ -21,6 +21,7 @@ pub use core::flows; pub use core::router; pub use flows::Deps; pub use local_store::migration::migrate_to_local; +pub use local_store::sync_local::sync_local_drives; pub use store::migrate_to_disk; pub async fn init_deps(mode: Option) -> (Arc, Arc) {