diff --git a/.github/workflows/run-v10-tests.yaml b/.github/workflows/run-v10-tests.yaml new file mode 100644 index 000000000..3c5153ba2 --- /dev/null +++ b/.github/workflows/run-v10-tests.yaml @@ -0,0 +1,271 @@ +name: "v10 SDK Tests" +on: + pull_request: + types: [labeled, opened, synchronize, reopened, auto_merge_enabled] + paths: + - "v10/**" + - ".github/workflows/run-v10-tests.yaml" + push: + branches: + - main + paths: + - "v10/**" + - ".github/workflows/run-v10-tests.yaml" + +permissions: + contents: read + +# Cancel in-progress runs for the same branch +concurrency: + group: v10-tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ── Build + Lint ── + build-and-lint: + name: "Build & Lint" + runs-on: ubuntu-latest + defaults: + run: + working-directory: v10 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .node-version + registry-url: "https://registry.npmjs.org" + - uses: pnpm/action-setup@v4 + - run: pnpm install --frozen-lockfile + - run: pnpm build + - run: pnpm check + + # ── Unit Tests ── + unit-tests: + name: "Unit Tests" + runs-on: ubuntu-latest + defaults: + run: + working-directory: v10 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .node-version + registry-url: "https://registry.npmjs.org" + - uses: pnpm/action-setup@v4 + - run: pnpm install --frozen-lockfile + - run: pnpm test:unit + + # ── E2E Tests (local testnet) ── + e2e-tests: + name: "E2E Tests" + runs-on: ubuntu-latest + defaults: + run: + working-directory: v10 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .node-version + registry-url: "https://registry.npmjs.org" + - uses: pnpm/action-setup@v4 + - run: pnpm install --frozen-lockfile + - run: pnpm build + + # Install the CLI and start local testnet + - run: pnpm install -g @aptos-labs/aptos-cli + shell: bash + - run: aptos node run-local-testnet --force-restart --assume-yes --with-indexer-api --log-to-stdout >& "$TESTNET_LOG" & + shell: bash + env: + TESTNET_LOG: ${{ runner.temp }}/local-testnet-logs.txt + - run: pnpm install -g wait-on + shell: bash + - run: wait-on --verbose --interval 1500 --timeout 120000 --httpTimeout 120000 http-get://127.0.0.1:8070 + shell: bash + + # Run E2E tests (API + compat) + - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # pin@v3 + name: v10-e2e-tests + env: + TMPDIR: ${{ runner.temp }} + with: + max_attempts: 3 + timeout_minutes: 10 + command: cd v10 && pnpm test:e2e + + - name: Print local testnet logs on failure + shell: bash + if: failure() + env: + TESTNET_LOG: ${{ runner.temp }}/local-testnet-logs.txt + run: cat "$TESTNET_LOG" + + # ── Bun Runtime Tests ── + bun-tests: + name: "Bun Runtime" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .node-version + registry-url: "https://registry.npmjs.org" + - uses: pnpm/action-setup@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + # Build v10 SDK + - run: cd v10 && pnpm install --frozen-lockfile && pnpm build + shell: bash + + # Install the CLI and start local testnet + - run: pnpm install -g @aptos-labs/aptos-cli + shell: bash + - run: aptos node run-local-testnet --force-restart --assume-yes --with-indexer-api --log-to-stdout >& "$TESTNET_LOG" & + shell: bash + env: + TESTNET_LOG: ${{ runner.temp }}/local-testnet-logs.txt + - run: pnpm install -g wait-on + shell: bash + - run: wait-on --verbose --interval 1500 --timeout 120000 --httpTimeout 120000 http-get://127.0.0.1:8070 + shell: bash + + # Run Bun tests + - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # pin@v3 + name: v10-bun-tests + env: + TMPDIR: ${{ runner.temp }} + APTOS_NETWORK: local + with: + max_attempts: 3 + timeout_minutes: 10 + command: | + cd v10/examples/bun-test + bun install + bun run test + + - name: Print local testnet logs on failure + shell: bash + if: failure() + env: + TESTNET_LOG: ${{ runner.temp }}/local-testnet-logs.txt + run: cat "$TESTNET_LOG" + + # ── Deno Runtime Tests ── + deno-tests: + name: "Deno Runtime" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .node-version + registry-url: "https://registry.npmjs.org" + - uses: pnpm/action-setup@v4 + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + # Build v10 SDK + - run: cd v10 && pnpm install --frozen-lockfile && pnpm build + shell: bash + + # Install the CLI and start local testnet + - run: pnpm install -g @aptos-labs/aptos-cli + shell: bash + - run: aptos node run-local-testnet --force-restart --assume-yes --with-indexer-api --log-to-stdout >& "$TESTNET_LOG" & + shell: bash + env: + TESTNET_LOG: ${{ runner.temp }}/local-testnet-logs.txt + - run: pnpm install -g wait-on + shell: bash + - run: wait-on --verbose --interval 1500 --timeout 120000 --httpTimeout 120000 http-get://127.0.0.1:8070 + shell: bash + + # Run Deno tests + - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # pin@v3 + name: v10-deno-tests + env: + TMPDIR: ${{ runner.temp }} + APTOS_NETWORK: local + with: + max_attempts: 3 + timeout_minutes: 10 + command: | + cd v10/examples/deno-test + pnpm install + deno install + deno task test + + - name: Print local testnet logs on failure + shell: bash + if: failure() + env: + TESTNET_LOG: ${{ runner.temp }}/local-testnet-logs.txt + run: cat "$TESTNET_LOG" + + # ── Browser Tests (Playwright) ── + browser-tests: + name: "Browser (Playwright)" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .node-version + registry-url: "https://registry.npmjs.org" + - uses: pnpm/action-setup@v4 + + # Build v10 SDK + - run: cd v10 && pnpm install --frozen-lockfile && pnpm build + shell: bash + + # Install the CLI and start local testnet + - run: pnpm install -g @aptos-labs/aptos-cli + shell: bash + - run: aptos node run-local-testnet --force-restart --assume-yes --with-indexer-api --log-to-stdout >& "$TESTNET_LOG" & + shell: bash + env: + TESTNET_LOG: ${{ runner.temp }}/local-testnet-logs.txt + - run: pnpm install -g wait-on + shell: bash + - run: wait-on --verbose --interval 1500 --timeout 120000 --httpTimeout 120000 http-get://127.0.0.1:8070 + shell: bash + + # Install web test deps + Playwright browsers + - run: | + cd v10/examples/web-test + pnpm install + pnpm exec playwright install --with-deps chromium + shell: bash + + # Run browser tests + - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # pin@v3 + name: v10-browser-tests + env: + TMPDIR: ${{ runner.temp }} + APTOS_NETWORK: local + with: + max_attempts: 3 + timeout_minutes: 15 + command: | + cd v10/examples/web-test + pnpm test + + - name: Print local testnet logs on failure + shell: bash + if: failure() + env: + TESTNET_LOG: ${{ runner.temp }}/local-testnet-logs.txt + run: cat "$TESTNET_LOG" + + - name: Upload Playwright report on failure + uses: actions/upload-artifact@v4 + if: failure() + with: + name: v10-playwright-report + path: v10/examples/web-test/playwright-report/ + retention-days: 7 diff --git a/v10/CHANGELOG.md b/v10/CHANGELOG.md new file mode 100644 index 000000000..70bca02ca --- /dev/null +++ b/v10/CHANGELOG.md @@ -0,0 +1,136 @@ +# Changelog + +All notable changes to the Aptos TypeScript SDK are documented in this file. + +## 10.0.0 + +A ground-up rewrite of the Aptos TypeScript SDK. See [MIGRATION.md](./MIGRATION.md) for upgrade instructions. + +### Breaking Changes + +- **ESM-only module format.** The SDK is now a pure ES module (`"type": "module"`). CJS consumers must use the `/compat` subpath or dynamic `import()`. Browser bundles (`dist/browser/`) are no longer shipped — modern bundlers handle ESM natively. + +- **Node.js >= 22.1.2 required.** The minimum Node.js version is now 22.1.2 (for native ESM support and `require(esm)` compatibility in the compat layer). + +- **Noble/Scure v2 cryptography.** Upgraded to `@noble/curves@^2.0.1`, `@noble/hashes@^2.0.1`, `@scure/bip32@^2.0.1`, and `@scure/bip39@^2.0.1`. These are breaking if you import from Noble/Scure directly (changed import paths and API surface). + +- **Account factory functions replace static methods.** `Account.generate()`, `Account.fromPrivateKey()`, and `Account.fromDerivationPath()` are now standalone functions: `generateAccount()`, `accountFromPrivateKey()`, `accountFromDerivationPath()`. + +- **Namespace-first API.** The `Aptos` class no longer exposes flat methods via mixins. Instead, methods are organized under namespace objects: + - `aptos.getAccountInfo(...)` → `aptos.account.getInfo(...)` + - `aptos.transaction.build.simple(...)` → `aptos.transaction.buildSimple(...)` + - `aptos.transaction.sign(...)` → `aptos.transaction.sign(...)` (unchanged) + - `aptos.transaction.submit.simple(...)` → `aptos.transaction.submit(...)` + - `aptos.fundAccount(...)` → `aptos.faucet.fund(...)` + - `aptos.waitForTransaction(...)` → `aptos.transaction.waitForTransaction(...)` + - `aptos.view(...)` → `aptos.general.view(...)` + +- **Positional arguments.** Namespace methods and standalone functions use positional arguments instead of a single `{ ... }` options object. For example: + - v6: `aptos.getAccountInfo({ accountAddress: "0x1" })` + - v10: `aptos.account.getInfo("0x1")` + +- **`AptosConfig` constructor accepts settings directly.** The `Aptos` constructor now accepts an `AptosSettings` object directly (no need to wrap in `new AptosConfig()`): + - v6: `new Aptos(new AptosConfig({ network: Network.TESTNET }))` + - v10: `new Aptos({ network: Network.TESTNET })` + +- **`AptosApiError` restructured.** Error fields have been reorganized. The `data` field structure may differ from v6. + +- **`AptosConfig.client` redesigned.** The v6 `client` property (which accepted a provider object) has been replaced with a new `Client` interface. The new interface has a single `sendRequest()` method. See the "Custom HTTP Client" section in the README. + +- **`TransactionGenerationConfig` removed.** Transaction options are now passed directly to `buildSimple()` via the `options` parameter. + +- **`InputGenerateTransactionOptions.accountSequenceNumber` → `sequenceNumber`.** The field was renamed in v10's `BuildSimpleTransactionOptions`. + +### Added + +- **Subpath exports.** 10 subpath exports for tree-shakeable, targeted imports: `./bcs`, `./hex`, `./crypto`, `./core`, `./transactions`, `./account`, `./client`, `./api`, `./compat`. + +- **`AptosSettings` shorthand.** Pass a plain settings object directly to the `Aptos` constructor instead of wrapping in `AptosConfig`. + +- **`signAndSubmitTransaction()`.** Combined sign + submit in a single function call — both as a namespace method (`aptos.transaction.signAndSubmit()`) and standalone function. + +- **Long-poll `waitForTransaction()`.** Uses server-side long polling for faster confirmation detection. + +- **`paginateWithObfuscatedCursor()`.** Pagination helper for APIs that use opaque cursor tokens instead of numeric offsets. + +- **Signed integer BCS types.** `I8`, `I16`, `I32`, `I64`, `I128`, `I256` with bounds checking and proper two's complement serialization. + +- **`AccountAddress.isSpecial()` and `AccountAddress.A`.** Check if an address is a special short address; `.A` is a constant for the `0xa` address. + +- **`Network.SHELBYNET` and `Network.NETNA`.** New network enum values with preconfigured endpoint URLs. + +- **`APTOS_FA` constant.** The fungible asset metadata address for the native APT token (`0xa`). + +- **`SingleKeyAccount.fromEd25519Account()`.** Convert a legacy Ed25519 account to SingleKey format. + +- **Type guard functions.** `isSingleKeySigner()`, `isKeylessSigner()`, `isPendingTransactionResponse()`, `isUserTransactionResponse()`. + +- **`KeylessError` taxonomy.** Structured error categories for Keyless authentication failures via `KeylessErrorCategory`. + +- **`ProcessorType` enum.** Indexer processor type constants for filtering processor status. + +- **`Serializer` pool.** Internal pooling for `Serializer` instances to reduce allocations in hot paths. + +- **Compatibility layer (`/compat`).** Full v6-style flat API on top of v10, with CJS support via `require()`. Enables gradual migration. + +- **Custom HTTP client (`Client` interface).** `AptosConfig` now accepts a `client` option to replace the default `@aptos-labs/aptos-client` transport. Implement `Client.sendRequest()` to add custom auth, proxies, logging, or use an alternative HTTP library. + +- **`http2` client config.** Explicit `http2: boolean` option in `ClientConfig` (defaults to `true` in Node.js). Uses `@aptos-labs/aptos-client` for HTTP/2 transport. + +- **`createConfig()` factory.** Functional alternative to `new AptosConfig()`. + +### Security + +- **TypeTag deserialization depth limit.** Added a recursion depth limit (128) to `TypeTag.deserialize()` to prevent stack overflow DoS from deeply nested `vector>` payloads. `StructTag.deserialize` correctly increments depth for type arguments and caps count at 32. + +- **MultiKeySignature bitmap validation.** `MultiKeySignature.deserialize()` now validates the bitmap is exactly 4 bytes before constructing the object. + +- **WebAuthnSignature field size limits.** `WebAuthnSignature.deserialize()` now enforces size limits on all fields: `signature` (128 bytes), `authenticatorData` (2 KB), and `clientDataJSON` (4 KB). + +- **MultiSigTransactionPayload variant validation.** `MultiSigTransactionPayload.deserialize()` now validates the variant index instead of silently discarding it. + +- **Secondary signer vector bounds.** Multi-agent and fee-payer transaction deserializers cap secondary signer vectors at 255 elements (matching on-chain limits). + +- **bytesToBigIntLE lookup table.** Pre-computed 256-element byte-to-bigint lookup table avoids per-byte BigInt allocations in the Poseidon hash path. + +### Performance + +- **Serializer single BigInt conversion.** `serializeU64`, `serializeU128`, and `serializeU256` now call `BigInt(value)` once instead of 2–4 times per invocation. + +- **Deserializer DataView singleton.** `Deserializer` now creates a single `DataView` at construction time instead of allocating a new one per `deserializeU16/U32/I8/I16/I32` call. + +- **TypeTag and StructTag singletons.** `TypeTagVector.u8()`, `aptosCoinStructTag()`, and `stringStructTag()` return cached singleton instances instead of allocating new objects per call. + +- **AccountAddress.toStringWithoutPrefix fast path.** Special addresses (`0x0`–`0xf`) skip the full 32-byte hex encoding and return immediately. + +### Changed + +- **Composition over mixins.** The `Aptos` class uses composition with namespace objects (`GeneralAPI`, `AccountAPI`, `TransactionAPI`, `CoinAPI`, `FaucetAPI`, `TableAPI`) instead of mixin-based class hierarchies. + +- **Crypto, Hex, and BCS promoted to top-level subpaths.** These were internal modules in v6 — now they are first-class subpath exports importable independently. + +- **Internal queries inlined.** The `src/internal/` directory and generated query layer are removed; API functions call HTTP endpoints directly. + +- **Biome replaces ESLint + Prettier.** Linting and formatting are handled by Biome (`pnpm check`, `pnpm fmt`). + +- **Vitest replaces Jest.** Test runner migrated to Vitest with faster execution and native ESM support. + +- **Plain `tsc` build.** No bundler — TypeScript compiles directly to `dist/esm/` and `dist/types/`. + +### Removed + +- **CJS output.** No `dist/cjs/` directory. The only CJS entry point is `/compat` via a thin wrapper. + +- **Browser bundle.** No `dist/browser/index.global.js`. Use a bundler (Vite, esbuild, webpack) with the ESM output. + +- **`Account.generate()`, `Account.fromPrivateKey()`, `Account.fromDerivationPath()` static methods.** Replaced by `generateAccount()`, `accountFromPrivateKey()`, `accountFromDerivationPath()`. + +- **`AptosConfig.client` (v6 shape).** The v6 `client` property is replaced by a new `Client` interface with a `sendRequest()` method. + +- **`TransactionGenerationConfig` type.** Options are passed directly to transaction building functions. + +- **`src/internal/` directory.** Internal query abstractions removed; API functions are self-contained. + +- **ANS, Digital Asset, Fungible Asset, Staking namespaces.** These domain-specific APIs are not yet ported to v10. Use the v6 SDK or `/compat` for these features in the interim. + +- **`transaction.build.simple()` / `transaction.submit.simple()` sub-objects.** Replaced by `transaction.buildSimple()` and `transaction.submit()` directly. The compat layer still provides the v6 `transaction.build.simple()` shape. diff --git a/v10/MIGRATION.md b/v10/MIGRATION.md new file mode 100644 index 000000000..b42daabfc --- /dev/null +++ b/v10/MIGRATION.md @@ -0,0 +1,538 @@ +# Migration Guide: v6.x → v10 + +This guide helps you upgrade from `@aptos-labs/ts-sdk` v6.x to v10. Two paths are available: + +1. **Quick migration** — Use the `/compat` subpath for a near-drop-in replacement +2. **Full migration** — Adopt the v10 namespaced API for maximum tree-shaking and ergonomics + +## Table of Contents + +- [Quick Migration (Compat Layer)](#quick-migration-compat-layer) +- [Requirements](#requirements) +- [Breaking Changes](#breaking-changes) + 1. [Module Format: CJS → ESM](#1-module-format-cjs--esm) + 2. [Aptos Constructor](#2-aptos-constructor) + 3. [API Style: Flat → Namespaced](#3-api-style-flat--namespaced) + 4. [Account Factory Functions](#4-account-factory-functions) + 5. [Transaction API](#5-transaction-api) + 6. [waitForTransaction Arguments](#6-waitfortransaction-arguments) + 7. [AptosConfig Changes](#7-aptosconfig-changes) + 8. [AptosApiError Fields](#8-aptosapierror-fields) + 9. [Noble v2 Import Paths](#9-noble-v2-import-paths) +- [Removed APIs](#removed-apis) +- [Method Mapping: v6 → v10](#method-mapping-v6--v10) + +--- + +## Quick Migration (Compat Layer) + +The fastest path: change your import to `/compat` and keep your existing code. + +**Before (v6):** + +```typescript +import { Aptos, AptosConfig, Network, Account } from "@aptos-labs/ts-sdk"; +``` + +**After (v10, compat):** + +```typescript +import { Aptos, AptosConfig, Network, generateAccount } from "@aptos-labs/ts-sdk/compat"; +``` + +The compat `Aptos` class extends the v10 `Aptos` class with all the v6-style flat methods, so existing code like `aptos.getAccountInfo(...)`, `aptos.fundAccount(...)`, and `aptos.transaction.build.simple(...)` continues to work. + +You can then migrate to v10's namespaced API incrementally — both styles work on the same object: + +```typescript +// Both work on the compat Aptos class: +const v6Result = await aptos.getAccountInfo({ accountAddress: "0x1" }); +const v10Result = await aptos.account.getInfo("0x1"); +``` + +### CJS Projects + +The `/compat` subpath is the only entry point that supports `require()`: + +```javascript +// Node >= 22.12 +const { Aptos, Network } = require("@aptos-labs/ts-sdk/compat"); + +// Node 22.1.2–22.11 +const { Aptos, Network } = await import("@aptos-labs/ts-sdk/compat"); +``` + +### CJS TypeScript Projects + +```typescript +// tsconfig.json needs: "module": "commonjs", "moduleResolution": "node16" +import { Aptos, Network, generateAccount } from "@aptos-labs/ts-sdk/compat"; +``` + +--- + +## Requirements + +| | v6 | v10 | +|---|---|---| +| **Node.js** | >= 16 | >= 22.1.2 | +| **Module format** | CJS + ESM | ESM-only (CJS via `/compat`) | +| **package.json** | Any | `"type": "module"` (or use `.mts` files) | +| **TypeScript** | >= 4.7 | >= 5.0 | + +--- + +## Breaking Changes + +### 1. Module Format: CJS → ESM + +v10 is ESM-only. If your project uses CommonJS, you have three options: + +#### Option A: Migrate to ESM (recommended) + +Add `"type": "module"` to your `package.json`: + +```json +{ + "type": "module" +} +``` + +Then use standard ESM imports: + +```typescript +import { Aptos, Network } from "@aptos-labs/ts-sdk"; +``` + +#### Option B: Use the compat layer + +```typescript +import { Aptos, Network } from "@aptos-labs/ts-sdk/compat"; +``` + +#### Option C: Dynamic import + +```javascript +const { Aptos, Network } = await import("@aptos-labs/ts-sdk"); +``` + +### 2. Aptos Constructor + +The `Aptos` constructor now accepts settings directly — no need to create an `AptosConfig` first. + +**Before (v6):** + +```typescript +const config = new AptosConfig({ network: Network.TESTNET }); +const aptos = new Aptos(config); +``` + +**After (v10):** + +```typescript +// Settings shorthand (recommended) +const aptos = new Aptos({ network: Network.TESTNET }); + +// AptosConfig still works too +const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET })); +``` + +### 3. API Style: Flat → Namespaced + +v6 used flat mixin-based methods. v10 organizes them into namespaces with positional arguments. + +**Before (v6):** + +```typescript +// Single-object args, flat methods +const info = await aptos.getAccountInfo({ accountAddress: "0x1" }); +const modules = await aptos.getAccountModules({ accountAddress: "0x1" }); +const resource = await aptos.getAccountResource({ + accountAddress: "0x1", + resourceType: "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", +}); +const ledger = await aptos.getLedgerInfo(); +const gas = await aptos.getGasPriceEstimation(); +``` + +**After (v10):** + +```typescript +// Positional args, namespaced methods +const info = await aptos.account.getInfo("0x1"); +const modules = await aptos.account.getModules("0x1"); +const resource = await aptos.account.getResource( + "0x1", + "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", +); +const ledger = await aptos.general.getLedgerInfo(); +const gas = await aptos.general.getGasPriceEstimation(); +``` + +### 4. Account Factory Functions + +Static methods on `Account` are now standalone functions. + +**Before (v6):** + +```typescript +import { Account, SigningSchemeInput, Ed25519PrivateKey } from "@aptos-labs/ts-sdk"; + +// Generate +const account = Account.generate(); +const secp = Account.generate({ scheme: SigningSchemeInput.Secp256k1Ecdsa }); + +// From private key +const account = Account.fromPrivateKey({ privateKey: new Ed25519PrivateKey("0x...") }); + +// From derivation path +const account = Account.fromDerivationPath({ + path: "m/44'/637'/0'/0'/0'", + mnemonic: "various float stumble ...", +}); +``` + +**After (v10):** + +```typescript +import { + generateAccount, + accountFromPrivateKey, + accountFromDerivationPath, + SigningSchemeInput, + Ed25519PrivateKey, +} from "@aptos-labs/ts-sdk"; + +// Generate +const account = generateAccount(); +const secp = generateAccount({ scheme: SigningSchemeInput.Secp256k1Ecdsa }); + +// From private key +const account = accountFromPrivateKey({ privateKey: new Ed25519PrivateKey("0x...") }); + +// From derivation path +const account = accountFromDerivationPath({ + path: "m/44'/637'/0'/0'/0'", + mnemonic: "various float stumble ...", +}); +``` + +**ESM JavaScript equivalent:** + +```javascript +import { generateAccount, accountFromPrivateKey } from "@aptos-labs/ts-sdk"; + +const account = generateAccount(); +``` + +**CJS JavaScript equivalent (via compat):** + +```javascript +const { generateAccount, accountFromPrivateKey } = require("@aptos-labs/ts-sdk/compat"); + +const account = generateAccount(); +``` + +### 5. Transaction API + +The build → sign → submit flow has changed from nested sub-objects to flat namespace methods with positional arguments. + +**Before (v6):** + +```typescript +// Build +const txn = await aptos.transaction.build.simple({ + sender: alice.accountAddress, + data: { + function: "0x1::aptos_account::transfer", + functionArguments: [bob.accountAddress, 100], + }, +}); + +// Sign +const auth = aptos.transaction.sign({ signer: alice, transaction: txn }); + +// Submit +const pending = await aptos.transaction.submit.simple({ + transaction: txn, + senderAuthenticator: auth, +}); + +// Or sign+submit +const pending = await aptos.signAndSubmitTransaction({ + signer: alice, + transaction: txn, +}); +``` + +**After (v10):** + +```typescript +// Build +const txn = await aptos.transaction.buildSimple(alice.accountAddress, { + function: "0x1::aptos_account::transfer", + functionArguments: [bob.accountAddress, 100], +}); + +// Sign +const auth = aptos.transaction.sign(alice, txn); + +// Submit +const pending = await aptos.transaction.submit(txn, auth); + +// Or sign+submit (recommended) +const pending = await aptos.transaction.signAndSubmit(alice, txn); +``` + +**ESM JavaScript equivalent:** + +```javascript +import { Aptos, Network, generateAccount } from "@aptos-labs/ts-sdk"; + +const aptos = new Aptos({ network: Network.DEVNET }); +const alice = generateAccount(); + +const txn = await aptos.transaction.buildSimple(alice.accountAddress, { + function: "0x1::aptos_account::transfer", + functionArguments: [bob.accountAddress, 100], +}); + +const pending = await aptos.transaction.signAndSubmit(alice, txn); +``` + +**CJS TypeScript equivalent (via compat, v6 style):** + +```typescript +import { Aptos, Network, generateAccount } from "@aptos-labs/ts-sdk/compat"; + +const aptos = new Aptos({ network: Network.DEVNET }); +const alice = generateAccount(); + +// v6-style still works in compat +const txn = await aptos.transaction.build.simple({ + sender: alice.accountAddress, + data: { + function: "0x1::aptos_account::transfer", + functionArguments: [bob.accountAddress, 100], + }, +}); + +const pending = await aptos.signAndSubmitTransaction({ + signer: alice, + transaction: txn, +}); +``` + +### 6. waitForTransaction Arguments + +**Before (v6):** + +```typescript +const result = await aptos.waitForTransaction({ + transactionHash: pending.hash, +}); +``` + +**After (v10):** + +```typescript +const result = await aptos.transaction.waitForTransaction(pending.hash, { + checkSuccess: true, +}); +``` + +### 7. AptosConfig Changes + +| v6 | v10 | Notes | +|----|-----|-------| +| `config.client` | `config.client` | New `Client` interface with `sendRequest()` method | +| `TransactionGenerationConfig` | *(removed)* | Pass options directly to `buildSimple()` | +| `options.accountSequenceNumber` | `options.sequenceNumber` | Renamed in `BuildSimpleTransactionOptions` | + +The `client` property is back but with a redesigned interface. In v6, `client` accepted a provider-style object. In v10, it uses a `Client` interface with a single `sendRequest()` method. + +**Before (v6):** + +```typescript +const config = new AptosConfig({ + network: Network.TESTNET, + client: { provider: myHttpClient }, +}); +``` + +**After (v10):** + +```typescript +import type { Client, ClientRequest, ClientResponse } from "@aptos-labs/ts-sdk/client"; + +const myClient: Client = { + async sendRequest(request: ClientRequest): Promise> { + const response = await fetch(request.url, { + method: request.method, + headers: request.headers, + body: request.body ? JSON.stringify(request.body) : undefined, + }); + return { + status: response.status, + statusText: response.statusText, + data: (await response.json()) as Res, + headers: response.headers, + }; + }, +}; + +const aptos = new Aptos({ + network: Network.TESTNET, + client: myClient, +}); +``` + +For header/auth customization without replacing the entire transport, use `clientConfig`: + +```typescript +const aptos = new Aptos({ + network: Network.TESTNET, + clientConfig: { http2: true, HEADERS: { "X-Custom": "value" } }, +}); +``` + +### 8. AptosApiError Fields + +The `AptosApiError` class has been restructured. If you catch and inspect API errors: + +**Before (v6):** + +```typescript +try { + await aptos.getAccountInfo({ accountAddress: "0xINVALID" }); +} catch (e) { + if (e instanceof AptosApiError) { + console.log(e.status, e.message, e.data); + } +} +``` + +**After (v10):** + +```typescript +import { AptosApiError } from "@aptos-labs/ts-sdk"; + +try { + await aptos.account.getInfo("0xINVALID"); +} catch (e) { + if (e instanceof AptosApiError) { + console.log(e.status, e.message, e.data); + } +} +``` + +The import path and field names are the same, but internal field structure of `data` may differ. + +### 9. Noble v2 Import Paths + +If you import directly from `@noble/curves` or `@noble/hashes`, v2 has breaking changes: + +**Before (v6 / Noble v1):** + +```typescript +import { ed25519 } from "@noble/curves/ed25519"; +import { sha256 } from "@noble/hashes/sha256"; +import { sha3_256 } from "@noble/hashes/sha3"; +``` + +**After (v10 / Noble v2):** + +```typescript +// Same import paths, but internal APIs changed. +// The SDK abstracts over this — you only need to worry if you import Noble directly. +import { ed25519 } from "@noble/curves/ed25519"; +import { sha256 } from "@noble/hashes/sha2"; // sha256 moved to sha2 +import { sha3_256 } from "@noble/hashes/sha3"; +``` + +--- + +## Removed APIs + +| v6 API | v10 Status | Alternative | +|--------|-----------|-------------| +| `Account.generate()` | Removed | `generateAccount()` | +| `Account.fromPrivateKey()` | Removed | `accountFromPrivateKey()` | +| `Account.fromDerivationPath()` | Removed | `accountFromDerivationPath()` | +| `AptosConfig.client` (v6 shape) | Redesigned | New `Client` interface with `sendRequest()` | +| `TransactionGenerationConfig` | Removed | Pass options to `buildSimple()` | +| `aptos.transaction.build.simple()` | Removed | `aptos.transaction.buildSimple()` | +| `aptos.transaction.submit.simple()` | Removed | `aptos.transaction.submit()` | +| `aptos.signAndSubmitTransaction()` | Removed | `aptos.transaction.signAndSubmit()` | +| `aptos.waitForTransaction()` | Removed | `aptos.transaction.waitForTransaction()` | +| `aptos.ans.*` | Not yet ported | Use v6 SDK or `/compat` | +| `aptos.digitalAsset.*` | Not yet ported | Use v6 SDK or `/compat` | +| `aptos.fungibleAsset.*` | Not yet ported | Use v6 SDK or `/compat` | +| `aptos.staking.*` | Not yet ported | Use v6 SDK or `/compat` | +| `dist/browser/index.global.js` | Removed | Use a bundler with ESM | +| `dist/cjs/` | Removed | Use ESM or `/compat` for CJS | + +--- + +## Method Mapping: v6 → v10 + +### Account + +| v6 (flat, single-object args) | v10 (namespaced, positional args) | +|-------------------------------|-----------------------------------| +| `aptos.getAccountInfo({ accountAddress })` | `aptos.account.getInfo(accountAddress)` | +| `aptos.getAccountModules({ accountAddress, options? })` | `aptos.account.getModules(accountAddress, options?)` | +| `aptos.getAccountModule({ accountAddress, moduleName, options? })` | `aptos.account.getModule(accountAddress, moduleName, options?)` | +| `aptos.getAccountResource({ accountAddress, resourceType, options? })` | `aptos.account.getResource(accountAddress, resourceType, options?)` | +| `aptos.getAccountResources({ accountAddress, options? })` | `aptos.account.getResources(accountAddress, options?)` | +| `aptos.getAccountTransactions({ accountAddress, options? })` | `aptos.account.getTransactions(accountAddress, options?)` | + +### General + +| v6 | v10 | +|----|-----| +| `aptos.getLedgerInfo()` | `aptos.general.getLedgerInfo()` | +| `aptos.getChainId()` | `aptos.general.getChainId()` | +| `aptos.getBlockByVersion({ ledgerVersion, options? })` | `aptos.general.getBlockByVersion(ledgerVersion, options?)` | +| `aptos.getBlockByHeight({ blockHeight, options? })` | `aptos.general.getBlockByHeight(blockHeight, options?)` | +| `aptos.view({ payload, options? })` | `aptos.general.view(payload, options?)` | +| `aptos.getGasPriceEstimation()` | `aptos.general.getGasPriceEstimation()` | + +### Transactions + +| v6 | v10 | +|----|-----| +| `aptos.transaction.build.simple({ sender, data, options? })` | `aptos.transaction.buildSimple(sender, payload, options?)` | +| `aptos.transaction.sign({ signer, transaction })` | `aptos.transaction.sign(signer, transaction)` | +| `aptos.transaction.submit.simple({ transaction, senderAuthenticator })` | `aptos.transaction.submit(transaction, senderAuthenticator)` | +| `aptos.signAndSubmitTransaction({ signer, transaction })` | `aptos.transaction.signAndSubmit(signer, transaction)` | +| `aptos.waitForTransaction({ transactionHash, options? })` | `aptos.transaction.waitForTransaction(transactionHash, options?)` | +| `aptos.getTransactionByHash({ transactionHash })` | `aptos.transaction.getByHash(transactionHash)` | +| `aptos.getTransactionByVersion({ ledgerVersion })` | `aptos.transaction.getByVersion(ledgerVersion)` | +| `aptos.getTransactions({ options? })` | `aptos.transaction.getAll(options?)` | + +### Coin & Faucet + +| v6 | v10 | +|----|-----| +| `aptos.transferCoinTransaction({ sender, recipient, amount, coinType?, options? })` | `aptos.coin.transferTransaction(sender, recipient, amount, coinType?, options?)` | +| `aptos.fundAccount({ accountAddress, amount, options? })` | `aptos.faucet.fund(accountAddress, amount, options?)` | + +### Table + +| v6 | v10 | +|----|-----| +| `aptos.getTableItem({ handle, data, options? })` | `aptos.table.getItem(handle, data, options?)` | + +### Standalone Functions + +Every namespace method is also available as a standalone function that takes `config: AptosConfig` as the first argument. This enables maximum tree-shaking: + +```typescript +// v10 standalone functions (from @aptos-labs/ts-sdk or @aptos-labs/ts-sdk/api) +import { AptosConfig } from "@aptos-labs/ts-sdk"; +import { getAccountInfo, buildSimpleTransaction, waitForTransaction } from "@aptos-labs/ts-sdk/api"; + +const config = new AptosConfig({ network: Network.DEVNET }); +const info = await getAccountInfo(config, "0x1"); +``` diff --git a/v10/README.md b/v10/README.md new file mode 100644 index 000000000..3a25e097b --- /dev/null +++ b/v10/README.md @@ -0,0 +1,569 @@ +# Aptos TypeScript SDK v10 + +[![NPM Package Version][npm-image-version]][npm-url] +![Node Version](https://img.shields.io/node/v/%40aptos-labs%2Fts-sdk) +[![NPM Package Downloads][npm-image-downloads]][npm-url] + +A ground-up rewrite of the Aptos TypeScript SDK — **ESM-only**, **tree-shakeable**, and **function-first**. + +- **Namespace-first API** — `aptos.account.getInfo(...)`, `aptos.transaction.buildSimple(...)` +- **Standalone functions** — Every API method is also a standalone function for maximum tree-shaking +- **Subpath exports** — Import only what you need: `@aptos-labs/ts-sdk/crypto`, `@aptos-labs/ts-sdk/bcs`, etc. +- **Compatibility layer** — `@aptos-labs/ts-sdk/compat` provides the v6-style flat API for gradual migration +- **Zero bundler config** — Pure ESM with `sideEffects: false`; bundlers tree-shake automatically + +## Installation + +```bash +npm install @aptos-labs/ts-sdk@10 +# or +pnpm add @aptos-labs/ts-sdk@10 +# or +yarn add @aptos-labs/ts-sdk@10 +``` + +> **Requirements:** Node.js >= 22.1.2, ESM (`"type": "module"` in your `package.json` or `.mts` files). + +## Quick Start + +### ESM TypeScript + +```typescript +import { Aptos, Network, generateAccount } from "@aptos-labs/ts-sdk"; + +// 1. Create a client +const aptos = new Aptos({ network: Network.DEVNET }); + +// 2. Generate a new account +const alice = generateAccount(); +const bob = generateAccount(); + +console.log(`Alice: ${alice.accountAddress}`); +console.log(`Bob: ${bob.accountAddress}`); + +// 3. Fund from faucet (devnet/testnet only) +await aptos.faucet.fund(alice.accountAddress, 100_000_000); +await aptos.faucet.fund(bob.accountAddress, 0); + +// 4. Build a transfer transaction +const txn = await aptos.transaction.buildSimple(alice.accountAddress, { + function: "0x1::aptos_account::transfer", + functionArguments: [bob.accountAddress, 1_000_000], +}); + +// 5. Sign and submit +const pending = await aptos.transaction.signAndSubmit(alice, txn); + +// 6. Wait for confirmation +const committed = await aptos.transaction.waitForTransaction(pending.hash); +console.log(`Success: ${committed.success}`); +``` + +### ESM JavaScript + +```javascript +import { Aptos, Network, generateAccount } from "@aptos-labs/ts-sdk"; + +const aptos = new Aptos({ network: Network.DEVNET }); + +const alice = generateAccount(); +await aptos.faucet.fund(alice.accountAddress, 100_000_000); + +const info = await aptos.account.getInfo(alice.accountAddress); +console.log("Sequence number:", info.sequence_number); +``` + +### CommonJS (via compat layer) + +The main SDK is ESM-only. For CJS projects that cannot migrate to ESM, use the `/compat` subpath: + +```javascript +// Node >= 22.12 — require() works directly +const { Aptos, Network, generateAccount } = require("@aptos-labs/ts-sdk/compat"); + +const aptos = new Aptos({ network: Network.DEVNET }); +const alice = generateAccount(); +``` + +```javascript +// Node 22.1.2–22.11 — use dynamic import +const { Aptos, Network, generateAccount } = await import("@aptos-labs/ts-sdk/compat"); +``` + +### CJS TypeScript + +```typescript +// tsconfig.json: { "module": "commonjs", "moduleResolution": "node16" } +import { Aptos, Network, generateAccount } from "@aptos-labs/ts-sdk/compat"; + +const aptos = new Aptos({ network: Network.DEVNET }); +const alice = generateAccount(); + +const info = await aptos.getAccountInfo({ accountAddress: alice.accountAddress }); +console.log(info.sequence_number); +``` + +> **Note:** The compat `Aptos` class exposes both the v10 namespaced API (`aptos.account.*`) and v6-style flat methods (`aptos.getAccountInfo(...)`) so you can migrate incrementally. + +## Architecture + +v10 is organized into layers, each available as a standalone subpath: + +| Layer | Subpath | Description | +|-------|---------|-------------| +| L0 | `@aptos-labs/ts-sdk/bcs` | Binary Canonical Serialization — `Serializer`, `Deserializer`, Move types (`U64`, `MoveVector`, etc.) | +| L1 | `@aptos-labs/ts-sdk/hex` | Hex encoding — `Hex` class, `HexInput` type, `hexToAsciiString()` | +| L2 | `@aptos-labs/ts-sdk/crypto` | Cryptographic keys — Ed25519, Secp256k1, MultiKey, Keyless, HD derivation | +| L3 | `@aptos-labs/ts-sdk/core` | Core types — `AccountAddress`, `AuthenticationKey`, `Network`, `TypeTag`, error types | +| L4 | `@aptos-labs/ts-sdk/transactions` | Transaction primitives — `SimpleTransaction`, `EntryFunction`, authenticators, signing | +| L5 | `@aptos-labs/ts-sdk/account` | Account implementations — `Ed25519Account`, `SingleKeyAccount`, factory functions | +| L6 | `@aptos-labs/ts-sdk/client` | HTTP client — `aptosRequest()`, `get()`, `post()`, pagination helpers | +| L7 | `@aptos-labs/ts-sdk/api` | High-level API — `Aptos` class, `GeneralAPI`, `AccountAPI`, `TransactionAPI`, etc. | +| — | `@aptos-labs/ts-sdk` | Re-exports everything from all layers | +| — | `@aptos-labs/ts-sdk/compat` | v6-compatible flat API + CJS support | + +## API Reference + +### `Aptos` class + +The main entry point. Accepts an `AptosSettings` object or an `AptosConfig` instance: + +```typescript +import { Aptos, Network } from "@aptos-labs/ts-sdk"; + +// Settings shorthand (recommended) +const aptos = new Aptos({ network: Network.TESTNET }); + +// Or with an AptosConfig +const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET })); + +// Or defaults to devnet +const aptos = new Aptos(); +``` + +#### Namespace: `aptos.general` + +| Method | Description | +|--------|-------------| +| `getLedgerInfo()` | Get chain metadata (epoch, block height, chain ID) | +| `getChainId()` | Get the numeric chain ID | +| `getBlockByVersion(ledgerVersion, options?)` | Get block containing a specific version | +| `getBlockByHeight(blockHeight, options?)` | Get block at a specific height | +| `view(payload, options?)` | Execute a Move view function | +| `getGasPriceEstimation()` | Get gas price estimate | + +#### Namespace: `aptos.account` + +| Method | Description | +|--------|-------------| +| `getInfo(accountAddress)` | Get account info (sequence number, auth key) | +| `getModules(accountAddress, options?)` | List all published modules | +| `getModule(accountAddress, moduleName, options?)` | Get a specific module | +| `getResource(accountAddress, resourceType, options?)` | Get a specific resource | +| `getResources(accountAddress, options?)` | List all resources | +| `getTransactions(accountAddress, options?)` | List account transactions | + +#### Namespace: `aptos.transaction` + +| Method | Description | +|--------|-------------| +| `buildSimple(sender, payload, options?)` | Build an entry function transaction | +| `sign(signer, transaction)` | Sign a transaction | +| `submit(transaction, senderAuthenticator)` | Submit a signed transaction | +| `signAndSubmit(signer, transaction)` | Sign + submit in one call | +| `waitForTransaction(transactionHash, options?)` | Wait for on-chain confirmation | +| `getByHash(transactionHash)` | Get transaction by hash | +| `getByVersion(ledgerVersion)` | Get transaction by version | +| `getAll(options?)` | List recent transactions | +| `getSigningMessage(transaction)` | Get raw signing bytes | + +#### Namespace: `aptos.coin` + +| Method | Description | +|--------|-------------| +| `transferTransaction(sender, recipient, amount, coinType?, options?)` | Build a coin transfer transaction | + +#### Namespace: `aptos.faucet` + +| Method | Description | +|--------|-------------| +| `fund(accountAddress, amount, options?)` | Fund account from faucet (devnet/testnet) | + +#### Namespace: `aptos.table` + +| Method | Description | +|--------|-------------| +| `getItem(handle, data, options?)` | Get a table item by key | + +## Account Management + +v10 uses standalone factory functions instead of `Account.generate()` static methods. + +### Generate a new account + +```typescript +import { generateAccount, SigningSchemeInput } from "@aptos-labs/ts-sdk"; + +// Legacy Ed25519 (default) +const account = generateAccount(); + +// SingleKey Ed25519 +const account = generateAccount({ scheme: SigningSchemeInput.Ed25519, legacy: false }); + +// SingleKey Secp256k1 +const account = generateAccount({ scheme: SigningSchemeInput.Secp256k1Ecdsa }); +``` + +### From a private key + +```typescript +import { accountFromPrivateKey, Ed25519PrivateKey, Secp256k1PrivateKey } from "@aptos-labs/ts-sdk"; + +// Ed25519 (legacy, default) +const account = accountFromPrivateKey({ + privateKey: new Ed25519PrivateKey("0xYOUR_HEX_KEY"), +}); + +// Secp256k1 (SingleKey) +const account = accountFromPrivateKey({ + privateKey: new Secp256k1PrivateKey("0xYOUR_HEX_KEY"), + legacy: false, +}); +``` + +### From a mnemonic derivation path + +```typescript +import { accountFromDerivationPath } from "@aptos-labs/ts-sdk"; + +const account = accountFromDerivationPath({ + path: "m/44'/637'/0'/0'/0'", + mnemonic: "various float stumble ...", +}); +``` + +## Transaction Flow + +### Build → SignAndSubmit → Wait (common path) + +```typescript +// Build +const txn = await aptos.transaction.buildSimple(alice.accountAddress, { + function: "0x1::aptos_account::transfer", + functionArguments: [bob.accountAddress, 1_000_000], +}); + +// Sign + Submit in one call +const pending = await aptos.transaction.signAndSubmit(alice, txn); + +// Wait for confirmation +const result = await aptos.transaction.waitForTransaction(pending.hash, { + checkSuccess: true, +}); +``` + +### Build → Sign → Submit → Wait (granular control) + +```typescript +// Build +const txn = await aptos.transaction.buildSimple(alice.accountAddress, { + function: "0x1::aptos_account::transfer", + functionArguments: [bob.accountAddress, 1_000_000], +}); + +// Sign +const auth = aptos.transaction.sign(alice, txn); + +// Submit +const pending = await aptos.transaction.submit(txn, auth); + +// Wait +const result = await aptos.transaction.waitForTransaction(pending.hash); +``` + +### Coin transfer shorthand + +```typescript +const txn = await aptos.coin.transferTransaction( + alice.accountAddress, + bob.accountAddress, + 1_000_000, +); +const pending = await aptos.transaction.signAndSubmit(alice, txn); +await aptos.transaction.waitForTransaction(pending.hash); +``` + +## Examples by Module + +### BCS — Serialization & deserialization + +```typescript +import { Serializer, Deserializer, U64, MoveVector, MoveString, Bool } from "@aptos-labs/ts-sdk/bcs"; + +// Serialize Move values +const serializer = new Serializer(); +new U64(42n).serialize(serializer); +new MoveString("hello").serialize(serializer); +new Bool(true).serialize(serializer); + +const bytes = serializer.toUint8Array(); + +// Deserialize +const deserializer = new Deserializer(bytes); +const num = U64.deserialize(deserializer); +const str = MoveString.deserialize(deserializer); +const flag = Bool.deserialize(deserializer); + +console.log(num.value, str.value, flag.value); // 42n, "hello", true +``` + +### Hex — Encoding and decoding + +```typescript +import { Hex, hexToAsciiString } from "@aptos-labs/ts-sdk/hex"; + +// From string +const hex = Hex.fromString("0x48656c6c6f"); +console.log(hex.toUint8Array()); // Uint8Array [72, 101, 108, 108, 111] + +// From bytes +const hex2 = Hex.fromUint8Array(new Uint8Array([72, 101, 108, 108, 111])); +console.log(hex2.toString()); // "0x48656c6c6f" + +// Decode hex-encoded ASCII +console.log(hexToAsciiString("0x48656c6c6f")); // "Hello" +``` + +### Crypto — Key generation and signing + +```typescript +import { Ed25519PrivateKey, Ed25519PublicKey, Secp256k1PrivateKey } from "@aptos-labs/ts-sdk/crypto"; + +// Generate a random Ed25519 key pair +const privateKey = Ed25519PrivateKey.generate(); +const publicKey = privateKey.publicKey(); +console.log("Public key:", publicKey.toString()); + +// Sign a message +const signature = privateKey.sign(new TextEncoder().encode("hello")); +const valid = publicKey.verifySignature({ message: "hello", signature }); +console.log("Valid:", valid); // true + +// Secp256k1 keys work the same way +const secpKey = Secp256k1PrivateKey.generate(); +``` + +### Core — Addresses and network configuration + +```typescript +import { AccountAddress, Network } from "@aptos-labs/ts-sdk/core"; + +// Parse an address +const addr = AccountAddress.from("0x1"); +console.log(addr.toString()); // "0x0000...0001" + +// Check special addresses +console.log(AccountAddress.A.isSpecial()); // true — the 0xa address + +// Network values +console.log(Network.MAINNET); // "mainnet" +console.log(Network.TESTNET); // "testnet" +console.log(Network.SHELBYNET); // "shelbynet" +``` + +### Transactions — Building payloads manually + +```typescript +import { EntryFunction, SimpleTransaction } from "@aptos-labs/ts-sdk/transactions"; +import { AccountAddress } from "@aptos-labs/ts-sdk/core"; +import { U64 } from "@aptos-labs/ts-sdk/bcs"; + +// Construct an entry function payload directly +const payload = EntryFunction.build( + "0x1::aptos_account::transfer", + [], // type args + [AccountAddress.from("0xBOB"), new U64(1000n)], // function args +); +``` + +### Account — Type guards + +```typescript +import { generateAccount, isSingleKeySigner, isKeylessSigner, SigningSchemeInput } from "@aptos-labs/ts-sdk/account"; + +const ed25519Legacy = generateAccount(); +const singleKey = generateAccount({ scheme: SigningSchemeInput.Ed25519, legacy: false }); + +console.log(isSingleKeySigner(ed25519Legacy)); // false +console.log(isSingleKeySigner(singleKey)); // true +console.log(isKeylessSigner(singleKey)); // false +``` + +### Client — Low-level HTTP requests + +```typescript +import { get, post, aptosRequest } from "@aptos-labs/ts-sdk/client"; +import { AptosConfig, Network } from "@aptos-labs/ts-sdk"; + +const config = new AptosConfig({ network: Network.MAINNET }); + +// Typed GET request to full node +const ledgerInfo = await get<{}, LedgerInfo>({ + url: config.getRequestUrl(AptosApiType.FULLNODE), + path: "", + originMethod: "getLedgerInfo", + overrides: config.getMergedFullnodeConfig(), +}); +``` + +### Standalone functions (for maximum tree-shaking) + +Every namespace method is also a standalone function that takes `config` as the first argument: + +```typescript +import { AptosConfig, Network } from "@aptos-labs/ts-sdk/api"; +import { getAccountInfo } from "@aptos-labs/ts-sdk/api"; +import { buildSimpleTransaction, signAndSubmitTransaction, waitForTransaction } from "@aptos-labs/ts-sdk/api"; +import { generateAccount } from "@aptos-labs/ts-sdk/account"; + +const config = new AptosConfig({ network: Network.DEVNET }); + +// Standalone function calls — only the functions you import get bundled +const alice = generateAccount(); +const info = await getAccountInfo(config, alice.accountAddress); +console.log(info.sequence_number); +``` + +```javascript +// ESM JavaScript — same standalone function pattern +import { AptosConfig } from "@aptos-labs/ts-sdk/api"; +import { getLedgerInfo } from "@aptos-labs/ts-sdk/api"; + +const config = new AptosConfig({ network: "testnet" }); +const info = await getLedgerInfo(config); +console.log("Chain ID:", info.chain_id); +``` + +## Compatibility Layer + +The `/compat` subpath provides the **v6-style flat API** on top of the v10 SDK. Use it for: + +1. **Gradual migration** — swap the import path, keep your existing code, then migrate method-by-method +2. **CJS projects** — `/compat` is the only subpath that supports `require()` + +### ESM TypeScript (compat) + +```typescript +import { Aptos, AptosConfig, Network, Account } from "@aptos-labs/ts-sdk/compat"; + +const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET })); + +// v6-style flat calls (single-object args) +const info = await aptos.getAccountInfo({ accountAddress: "0x1" }); + +// v6-style transaction.build.simple +const txn = await aptos.transaction.build.simple({ + sender: alice.accountAddress, + data: { + function: "0x1::aptos_account::transfer", + functionArguments: [bob.accountAddress, 1_000_000], + }, +}); + +// v6-style sign and submit +const pending = await aptos.signAndSubmitTransaction({ signer: alice, transaction: txn }); +await aptos.waitForTransaction({ transactionHash: pending.hash }); +``` + +### CJS JavaScript (compat) + +```javascript +// Node >= 22.12 +const { Aptos, Network, generateAccount } = require("@aptos-labs/ts-sdk/compat"); + +async function main() { + const aptos = new Aptos({ network: Network.DEVNET }); + const alice = generateAccount(); + + await aptos.fundAccount({ accountAddress: alice.accountAddress, amount: 100_000_000 }); + const info = await aptos.getAccountInfo({ accountAddress: alice.accountAddress }); + console.log("Sequence:", info.sequence_number); +} + +main(); +``` + +### CJS TypeScript (compat) + +```typescript +// tsconfig.json: { "module": "commonjs", "moduleResolution": "node16" } +import { Aptos, Network, generateAccount } from "@aptos-labs/ts-sdk/compat"; + +async function main() { + const aptos = new Aptos({ network: Network.DEVNET }); + const alice = generateAccount(); + + await aptos.fundAccount({ accountAddress: alice.accountAddress, amount: 100_000_000 }); + + // Both styles work on the compat Aptos class: + const v6Style = await aptos.getAccountInfo({ accountAddress: alice.accountAddress }); + const v10Style = await aptos.account.getInfo(alice.accountAddress); + + console.log(v6Style.sequence_number === v10Style.sequence_number); // true +} + +main(); +``` + +## Bun Compatibility + +Bun's HTTP/2 support is not fully mature. Disable it when using Bun: + +```typescript +import { Aptos, Network } from "@aptos-labs/ts-sdk"; + +const aptos = new Aptos({ + network: Network.TESTNET, + clientConfig: { http2: false }, +}); +``` + +## Custom HTTP Client + +You can replace the default HTTP transport by providing a custom `Client` implementation. This is useful for adding custom auth, proxies, logging, or using an alternative HTTP library. + +```typescript +import { Aptos, Network } from "@aptos-labs/ts-sdk"; +import type { Client, ClientRequest, ClientResponse } from "@aptos-labs/ts-sdk/client"; + +const myClient: Client = { + async sendRequest(request: ClientRequest): Promise> { + console.log(`${request.method} ${request.url}`); + const response = await fetch(request.url, { + method: request.method, + headers: request.headers, + body: request.body ? JSON.stringify(request.body) : undefined, + }); + return { + status: response.status, + statusText: response.statusText, + data: (await response.json()) as Res, + headers: response.headers, + }; + }, +}; + +const aptos = new Aptos({ network: Network.DEVNET, client: myClient }); +const info = await aptos.general.getLedgerInfo(); +``` + +When no custom client is provided, the SDK uses `@aptos-labs/aptos-client` (HTTP/2 in Node.js via `got`, native `fetch` in browsers). + +## Contributing + +If you found a bug or would like to request a feature, please file an [issue](https://github.com/aptos-labs/aptos-ts-sdk/issues/new/choose). +If, based on the discussion on an issue, you would like to offer a code change, please make a [pull request](https://github.com/aptos-labs/aptos-ts-sdk/pulls). + +[npm-image-version]: https://img.shields.io/npm/v/%40aptos-labs%2Fts-sdk.svg +[npm-image-downloads]: https://img.shields.io/npm/dm/%40aptos-labs%2Fts-sdk.svg +[npm-url]: https://npmjs.org/package/@aptos-labs/ts-sdk diff --git a/v10/biome.json b/v10/biome.json new file mode 100644 index 000000000..56fe4ff21 --- /dev/null +++ b/v10/biome.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.6/schema.json", + "vcs": { + "enabled": false + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 120, + "lineEnding": "lf" + }, + "javascript": { + "formatter": { + "trailingCommas": "all", + "quoteStyle": "double", + "semicolons": "always" + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noStaticOnlyClass": "off" + }, + "style": { + "noNonNullAssertion": "warn", + "useNodejsImportProtocol": "error" + }, + "suspicious": { + "noExplicitAny": "warn" + } + } + }, + "files": { + "includes": ["src/**", "tests/**"] + } +} diff --git a/v10/docs/plans/2026-03-09-v10-examples-design.md b/v10/docs/plans/2026-03-09-v10-examples-design.md new file mode 100644 index 000000000..280aacdeb --- /dev/null +++ b/v10/docs/plans/2026-03-09-v10-examples-design.md @@ -0,0 +1,77 @@ +# v10 SDK Examples — Design + +Three standalone examples demonstrating v10 SDK usage patterns, each with unit tests (mocked HTTP) and optional e2e tests (devnet). + +## 1. `examples/simple-transfer/` — Lightweight Submit & Receive + +Single `main.ts` script: generate accounts, fund via faucet, transfer APT, wait for confirmation, check balances. Uses v10 native namespaced API. + +**Structure:** +``` +examples/simple-transfer/ +├── package.json +├── tsconfig.json +├── src/main.ts +└── tests/ + ├── transfer.unit.test.ts + └── transfer.e2e.test.ts +``` + +**Flow:** `generateAccount()` → `aptos.faucet.fund()` → `aptos.transaction.buildSimple()` → `aptos.transaction.signAndSubmit()` → `aptos.transaction.waitForTransaction()` → balance check via `aptos.account.getResource()` + +## 2. `examples/sponsored-txn-server/` — Hono Sponsor/Relayer + +Hono server accepting transaction intents, building/signing/submitting with a server-side key. Demonstrates the gas station / relayer pattern. + +**Structure:** +``` +examples/sponsored-txn-server/ +├── package.json +├── tsconfig.json +├── src/ +│ ├── server.ts # Hono app (exported for testing) +│ └── main.ts # starts server +└── tests/ + ├── server.unit.test.ts + └── server.e2e.test.ts +``` + +**Endpoints:** +- `POST /sponsor` — `{ sender, function, functionArguments, typeArguments? }` → builds, signs, submits, returns `{ hash, version, success }` +- `GET /health` — server status + chain info + +## 3. `examples/dapp-with-wallet/` — React dApp + Wallet Adapter + +Minimal React/Vite dApp using `@aptos-labs/wallet-adapter-react` with v10 compat layer. Connect wallet, read account info, submit transfer. + +**Structure:** +``` +examples/dapp-with-wallet/ +├── package.json +├── vite.config.ts +├── tsconfig.json +├── index.html +├── src/ +│ ├── main.tsx +│ ├── App.tsx +│ ├── WalletConnect.tsx +│ └── TransferForm.tsx +└── tests/ + ├── app.unit.test.ts + └── app.e2e.test.ts +``` + +Uses `@aptos-labs/ts-sdk/compat` so wallet-adapter v6 types are satisfied. + +## Test Strategy + +- **Unit:** Vitest, mocked `@aptos-labs/aptos-client` (jsonRequest/bcsRequest). Fast, no network. +- **E2E:** Behind `APTOS_E2E=1` env flag. Hits devnet. dApp uses Playwright. +- Each example has its own `vitest.config.ts`. + +## Decisions + +- Hono over Express (lightweight, runs on Node/Bun/Deno) +- Server is a relayer (sender = server key), not fee payer (v10 doesn't have fee payer build yet) +- dApp uses compat layer for wallet-adapter compatibility +- Simple transfer is a plain script (no CLI framework) diff --git a/v10/docs/plans/2026-03-09-v10-examples.md b/v10/docs/plans/2026-03-09-v10-examples.md new file mode 100644 index 000000000..2535921ec --- /dev/null +++ b/v10/docs/plans/2026-03-09-v10-examples.md @@ -0,0 +1,1270 @@ +# v10 SDK Examples Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Build three standalone examples (simple-transfer, sponsored-txn-server, dapp-with-wallet) each with unit tests (mocked HTTP) and optional e2e tests (devnet). + +**Architecture:** Each example is a standalone project inside `examples/` with its own `package.json`, `tsconfig.json`, and `vitest.config.ts`. They depend on `@aptos-labs/ts-sdk` via `link:../..` (the built v10 SDK). Unit tests mock `@aptos-labs/aptos-client` (the HTTP layer). E2E tests hit devnet behind `APTOS_E2E=1`. + +**Tech Stack:** TypeScript, Vitest, Hono (@hono/node-server), React 19, Vite, @aptos-labs/wallet-adapter-react + +--- + +## Shared Context + +**SDK import patterns (v10 native):** +```typescript +import { Aptos, Network, generateAccount, AccountAddress, U64 } from "@aptos-labs/ts-sdk"; +``` + +**SDK import patterns (compat layer):** +```typescript +import { Aptos, AptosConfig, Network, Account } from "@aptos-labs/ts-sdk/compat"; +``` + +**Mock pattern (from `tests/unit/api/api.test.ts`):** +```typescript +vi.mock("@aptos-labs/aptos-client", () => ({ + jsonRequest: vi.fn(), + bcsRequest: vi.fn(), +})); +import { jsonRequest } from "@aptos-labs/aptos-client"; +const mockClient = vi.mocked(jsonRequest); + +function mockJsonResponse(data: unknown, status = 200) { + return { status, statusText: "OK", data, headers: {} }; +} +``` + +**Building SDK first:** Before running any example, the SDK must be built: +```bash +cd /Users/greg/git/aptos-ts-sdk/v10 && pnpm build +``` + +**tsconfig pattern for examples:** +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["src"] +} +``` + +--- + +## Task 1: Simple Transfer Example — Project Scaffold + +**Files:** +- Create: `examples/simple-transfer/package.json` +- Create: `examples/simple-transfer/tsconfig.json` +- Create: `examples/simple-transfer/vitest.config.ts` + +**Step 1: Create package.json** + +```json +{ + "name": "simple-transfer-example", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "tsx src/main.ts", + "test": "vitest run", + "test:unit": "vitest run tests/unit", + "test:e2e": "APTOS_E2E=1 vitest run tests/e2e" + }, + "dependencies": { + "@aptos-labs/ts-sdk": "link:../.." + }, + "devDependencies": { + "tsx": "^4.19.0", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + } +} +``` + +**Step 2: Create tsconfig.json** + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["src"] +} +``` + +**Step 3: Create vitest.config.ts** + +```typescript +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + }, +}); +``` + +**Step 4: Install dependencies** + +Run: `cd /Users/greg/git/aptos-ts-sdk/v10/examples/simple-transfer && pnpm install` + +**Step 5: Commit** + +```bash +git add examples/simple-transfer/package.json examples/simple-transfer/tsconfig.json examples/simple-transfer/vitest.config.ts examples/simple-transfer/pnpm-lock.yaml +git commit -m "feat(examples): scaffold simple-transfer project" +``` + +--- + +## Task 2: Simple Transfer — Unit Test + +**Files:** +- Create: `examples/simple-transfer/tests/unit/transfer.test.ts` + +**Step 1: Write the unit test** + +This test mocks all HTTP and verifies the full flow: fund → build → signAndSubmit → wait → balance check. + +```typescript +import { afterEach, describe, expect, it, vi } from "vitest"; + +// Mock HTTP layer before importing SDK +vi.mock("@aptos-labs/aptos-client", () => ({ + jsonRequest: vi.fn(), + bcsRequest: vi.fn(), +})); + +import { jsonRequest, bcsRequest } from "@aptos-labs/aptos-client"; +import { Aptos, Network, generateAccount, AccountAddress, U64 } from "@aptos-labs/ts-sdk"; + +const mockJson = vi.mocked(jsonRequest); +const mockBcs = vi.mocked(bcsRequest); + +function mockJsonResponse(data: unknown, status = 200) { + return { status, statusText: "OK", data, headers: {} }; +} + +describe("simple transfer flow", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("generates accounts, funds, transfers, and checks balance", async () => { + const alice = generateAccount(); + const bob = generateAccount(); + + // Fund alice — faucet POST returns txn hash, then waitForTransaction GET returns committed + mockJson.mockResolvedValueOnce( + mockJsonResponse({ txn_hashes: ["0xfaucethash"] }), + ); + // waitForTransaction — long-poll returns committed user txn + mockJson.mockResolvedValueOnce( + mockJsonResponse({ + type: "user_transaction", + version: "1", + hash: "0xfaucethash", + success: true, + vm_status: "Executed successfully", + sender: "0x1", + sequence_number: "0", + max_gas_amount: "200000", + gas_unit_price: "100", + expiration_timestamp_secs: "9999999999", + payload: {}, + events: [], + timestamp: "1000", + state_change_hash: "0x", + event_root_hash: "0x", + accumulator_root_hash: "0x", + gas_used: "100", + changes: [], + }), + ); + + const aptos = new Aptos({ network: Network.LOCAL }); + const fundResult = await aptos.faucet.fund(alice.accountAddress, 100_000_000); + expect(fundResult.success).toBe(true); + + // buildSimple: 3 parallel requests — ledgerInfo, gasEstimation, sequenceNumber + mockJson.mockResolvedValueOnce( + mockJsonResponse({ + chain_id: 4, + epoch: "1", + ledger_version: "100", + oldest_ledger_version: "0", + ledger_timestamp: "0", + node_role: "full_node", + oldest_block_height: "0", + block_height: "50", + }), + ); + mockJson.mockResolvedValueOnce( + mockJsonResponse({ gas_estimate: 100, deprioritized_gas_estimate: 50, prioritized_gas_estimate: 200 }), + ); + mockJson.mockResolvedValueOnce( + mockJsonResponse({ sequence_number: "0", authentication_key: "0x" }), + ); + + const txn = await aptos.transaction.buildSimple(alice.accountAddress, { + function: "0x1::aptos_account::transfer", + functionArguments: [AccountAddress.from(bob.accountAddress), new U64(1_000)], + }); + expect(txn.rawTransaction).toBeDefined(); + expect(txn.rawTransaction.sender.toString()).toBe(alice.accountAddress.toString()); + + // signAndSubmit: BCS POST returns pending txn + mockBcs.mockResolvedValueOnce( + mockJsonResponse({ hash: "0xtransferhash" }), + ); + + const pending = await aptos.transaction.signAndSubmit(alice, txn); + expect(pending.hash).toBe("0xtransferhash"); + + // waitForTransaction: long-poll returns committed + mockJson.mockResolvedValueOnce( + mockJsonResponse({ + type: "user_transaction", + version: "2", + hash: "0xtransferhash", + success: true, + vm_status: "Executed successfully", + sender: alice.accountAddress.toString(), + sequence_number: "0", + max_gas_amount: "200000", + gas_unit_price: "100", + expiration_timestamp_secs: "9999999999", + payload: {}, + events: [], + timestamp: "2000", + state_change_hash: "0x", + event_root_hash: "0x", + accumulator_root_hash: "0x", + gas_used: "50", + changes: [], + }), + ); + + const committed = await aptos.transaction.waitForTransaction(pending.hash); + expect(committed.success).toBe(true); + expect(committed.hash).toBe("0xtransferhash"); + }); +}); +``` + +**Step 2: Run test to verify it passes** + +Run: `cd /Users/greg/git/aptos-ts-sdk/v10/examples/simple-transfer && npx vitest run tests/unit` +Expected: PASS + +**Step 3: Commit** + +```bash +git add examples/simple-transfer/tests/unit/transfer.test.ts +git commit -m "test(examples): add simple-transfer unit test" +``` + +--- + +## Task 3: Simple Transfer — Main Script + +**Files:** +- Create: `examples/simple-transfer/src/main.ts` + +**Step 1: Write the main script** + +```typescript +/** + * Simple Transfer Example — v10 SDK + * + * Demonstrates the core transaction flow: + * 1. Generate two accounts + * 2. Fund the sender via faucet + * 3. Build a transfer transaction + * 4. Sign and submit + * 5. Wait for confirmation + * 6. Verify the recipient received funds + */ + +import { Aptos, Network, generateAccount, AccountAddress, U64 } from "@aptos-labs/ts-sdk"; + +const FUND_AMOUNT = 100_000_000; // 1 APT +const TRANSFER_AMOUNT = 1_000_000; // 0.01 APT + +async function main() { + // Connect to devnet (faucet available) + const aptos = new Aptos({ network: Network.DEVNET }); + + // 1. Generate accounts + const alice = generateAccount(); + const bob = generateAccount(); + console.log(`Alice: ${alice.accountAddress}`); + console.log(`Bob: ${bob.accountAddress}`); + + // 2. Fund Alice via faucet + console.log("\nFunding Alice..."); + const fundTxn = await aptos.faucet.fund(alice.accountAddress, FUND_AMOUNT); + console.log(`Fund tx: ${fundTxn.hash} (success: ${fundTxn.success})`); + + // 3. Check Alice's balance + const [balanceBefore] = await aptos.general.view<[string]>({ + function: "0x1::coin::balance", + type_arguments: ["0x1::aptos_coin::AptosCoin"], + arguments: [alice.accountAddress.toString()], + }); + console.log(`Alice balance: ${balanceBefore} octas`); + + // 4. Build transfer transaction + console.log(`\nTransferring ${TRANSFER_AMOUNT} octas to Bob...`); + const txn = await aptos.transaction.buildSimple(alice.accountAddress, { + function: "0x1::aptos_account::transfer", + functionArguments: [AccountAddress.from(bob.accountAddress), new U64(TRANSFER_AMOUNT)], + }); + + // 5. Sign and submit + const pending = await aptos.transaction.signAndSubmit(alice, txn); + console.log(`Pending: ${pending.hash}`); + + // 6. Wait for confirmation + const committed = await aptos.transaction.waitForTransaction(pending.hash); + console.log(`Committed: version ${committed.version}, success: ${committed.success}`); + + // 7. Verify Bob received funds + const [bobBalance] = await aptos.general.view<[string]>({ + function: "0x1::coin::balance", + type_arguments: ["0x1::aptos_coin::AptosCoin"], + arguments: [bob.accountAddress.toString()], + }); + console.log(`Bob balance: ${bobBalance} octas`); + console.log("\nDone!"); +} + +main().catch(console.error); +``` + +**Step 2: Verify unit tests still pass** + +Run: `cd /Users/greg/git/aptos-ts-sdk/v10/examples/simple-transfer && npx vitest run tests/unit` +Expected: PASS + +**Step 3: Commit** + +```bash +git add examples/simple-transfer/src/main.ts +git commit -m "feat(examples): add simple-transfer main script" +``` + +--- + +## Task 4: Simple Transfer — E2E Test + +**Files:** +- Create: `examples/simple-transfer/tests/e2e/transfer.e2e.test.ts` + +**Step 1: Write the e2e test** + +```typescript +import { describe, expect, it } from "vitest"; +import { Aptos, Network, generateAccount, AccountAddress, U64 } from "@aptos-labs/ts-sdk"; + +const SKIP = !process.env.APTOS_E2E; + +describe.skipIf(SKIP)("simple transfer e2e", () => { + it("transfers APT between two accounts on devnet", async () => { + const aptos = new Aptos({ network: Network.DEVNET }); + const alice = generateAccount(); + const bob = generateAccount(); + + // Fund alice + const fundTxn = await aptos.faucet.fund(alice.accountAddress, 100_000_000); + expect(fundTxn.success).toBe(true); + + // Transfer + const txn = await aptos.transaction.buildSimple(alice.accountAddress, { + function: "0x1::aptos_account::transfer", + functionArguments: [AccountAddress.from(bob.accountAddress), new U64(1_000)], + }); + const pending = await aptos.transaction.signAndSubmit(alice, txn); + const committed = await aptos.transaction.waitForTransaction(pending.hash); + expect(committed.success).toBe(true); + + // Verify bob's balance + const [balance] = await aptos.general.view<[string]>({ + function: "0x1::coin::balance", + type_arguments: ["0x1::aptos_coin::AptosCoin"], + arguments: [bob.accountAddress.toString()], + }); + expect(Number(balance)).toBe(1_000); + }, 30_000); +}); +``` + +**Step 2: Run unit tests (e2e skipped by default)** + +Run: `cd /Users/greg/git/aptos-ts-sdk/v10/examples/simple-transfer && npx vitest run` +Expected: PASS (e2e test skipped) + +**Step 3: Commit** + +```bash +git add examples/simple-transfer/tests/e2e/transfer.e2e.test.ts +git commit -m "test(examples): add simple-transfer e2e test (devnet, behind APTOS_E2E)" +``` + +--- + +## Task 5: Sponsored Transaction Server — Project Scaffold + +**Files:** +- Create: `examples/sponsored-txn-server/package.json` +- Create: `examples/sponsored-txn-server/tsconfig.json` +- Create: `examples/sponsored-txn-server/vitest.config.ts` + +**Step 1: Create package.json** + +```json +{ + "name": "sponsored-txn-server-example", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "tsx src/main.ts", + "test": "vitest run", + "test:unit": "vitest run tests/unit", + "test:e2e": "APTOS_E2E=1 vitest run tests/e2e" + }, + "dependencies": { + "@aptos-labs/ts-sdk": "link:../..", + "hono": "^4.7.0", + "@hono/node-server": "^1.14.0" + }, + "devDependencies": { + "tsx": "^4.19.0", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + } +} +``` + +**Step 2: Create tsconfig.json** (same as simple-transfer) + +**Step 3: Create vitest.config.ts** (same as simple-transfer) + +**Step 4: Install dependencies** + +Run: `cd /Users/greg/git/aptos-ts-sdk/v10/examples/sponsored-txn-server && pnpm install` + +**Step 5: Commit** + +```bash +git add examples/sponsored-txn-server/ +git commit -m "feat(examples): scaffold sponsored-txn-server project" +``` + +--- + +## Task 6: Sponsored Transaction Server — Server Implementation + +**Files:** +- Create: `examples/sponsored-txn-server/src/server.ts` +- Create: `examples/sponsored-txn-server/src/main.ts` + +**Step 1: Write server.ts (Hono app, exported for testing)** + +```typescript +/** + * Sponsored Transaction Server — v10 SDK + * + * A Hono server that acts as a transaction relayer/sponsor. + * Clients POST transaction intents; the server builds, signs + * (using its own key), submits, and returns the result. + */ + +import { Hono } from "hono"; +import { + Aptos, + Network, + generateAccount, + AccountAddress, + U64, +} from "@aptos-labs/ts-sdk"; +import type { Account } from "@aptos-labs/ts-sdk"; +import type { TypeTag } from "@aptos-labs/ts-sdk"; + +export interface ServerConfig { + aptos: Aptos; + sponsorAccount: Account; +} + +interface SponsorRequest { + function: string; + functionArguments: (string | number)[]; + typeArguments?: string[]; +} + +export function createApp(config: ServerConfig) { + const { aptos, sponsorAccount } = config; + const app = new Hono(); + + app.get("/health", async (c) => { + const ledger = await aptos.general.getLedgerInfo(); + return c.json({ + status: "ok", + sponsor: sponsorAccount.accountAddress.toString(), + chain_id: ledger.chain_id, + ledger_version: ledger.ledger_version, + }); + }); + + app.post("/sponsor", async (c) => { + const body = await c.req.json(); + + if (!body.function) { + return c.json({ error: "Missing 'function' field" }, 400); + } + + // Convert string/number arguments to SDK types + const functionArguments = (body.functionArguments ?? []).map((arg) => { + if (typeof arg === "number") return new U64(arg); + // Try to parse as address, fall back to U64 for numeric strings + if (typeof arg === "string" && arg.startsWith("0x")) { + return AccountAddress.from(arg); + } + if (typeof arg === "string" && /^\d+$/.test(arg)) { + return new U64(BigInt(arg)); + } + return new U64(BigInt(arg)); + }); + + const txn = await aptos.transaction.buildSimple( + sponsorAccount.accountAddress, + { + function: body.function as `${string}::${string}::${string}`, + functionArguments, + }, + ); + + const pending = await aptos.transaction.signAndSubmit(sponsorAccount, txn); + const committed = await aptos.transaction.waitForTransaction(pending.hash); + + return c.json({ + hash: committed.hash, + version: committed.version, + success: committed.success, + }); + }); + + return app; +} +``` + +**Step 2: Write main.ts** + +```typescript +import { serve } from "@hono/node-server"; +import { Aptos, Network, generateAccount } from "@aptos-labs/ts-sdk"; +import { createApp } from "./server.js"; + +const network = (process.env.APTOS_NETWORK as Network) ?? Network.DEVNET; +const port = Number(process.env.PORT ?? 3000); + +async function main() { + const aptos = new Aptos({ network }); + const sponsor = generateAccount(); + + console.log(`Sponsor address: ${sponsor.accountAddress}`); + console.log(`Network: ${network}`); + + // Fund sponsor on devnet + if (network === Network.DEVNET || network === Network.LOCAL) { + console.log("Funding sponsor..."); + await aptos.faucet.fund(sponsor.accountAddress, 500_000_000); + console.log("Funded!"); + } + + const app = createApp({ aptos, sponsorAccount: sponsor }); + + console.log(`Server listening on http://localhost:${port}`); + serve({ fetch: app.fetch, port }); +} + +main().catch(console.error); +``` + +**Step 3: Commit** + +```bash +git add examples/sponsored-txn-server/src/ +git commit -m "feat(examples): add sponsored-txn-server Hono app" +``` + +--- + +## Task 7: Sponsored Transaction Server — Unit Test + +**Files:** +- Create: `examples/sponsored-txn-server/tests/unit/server.test.ts` + +**Step 1: Write the unit test** + +Tests the Hono app by calling routes with mocked SDK HTTP. + +```typescript +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@aptos-labs/aptos-client", () => ({ + jsonRequest: vi.fn(), + bcsRequest: vi.fn(), +})); + +import { jsonRequest, bcsRequest } from "@aptos-labs/aptos-client"; +import { Aptos, Network, generateAccount } from "@aptos-labs/ts-sdk"; +import { createApp } from "../../src/server.js"; + +const mockJson = vi.mocked(jsonRequest); +const mockBcs = vi.mocked(bcsRequest); + +function mockJsonResponse(data: unknown, status = 200) { + return { status, statusText: "OK", data, headers: {} }; +} + +describe("sponsored-txn-server", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + function setup() { + const aptos = new Aptos({ network: Network.LOCAL }); + const sponsor = generateAccount(); + const app = createApp({ aptos, sponsorAccount: sponsor }); + return { app, sponsor }; + } + + it("GET /health returns chain info", async () => { + mockJson.mockResolvedValueOnce( + mockJsonResponse({ + chain_id: 4, + epoch: "1", + ledger_version: "100", + oldest_ledger_version: "0", + ledger_timestamp: "0", + node_role: "full_node", + oldest_block_height: "0", + block_height: "50", + }), + ); + + const { app, sponsor } = setup(); + const res = await app.request("/health"); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.status).toBe("ok"); + expect(data.chain_id).toBe(4); + expect(data.sponsor).toBe(sponsor.accountAddress.toString()); + }); + + it("POST /sponsor builds, signs, submits, and waits", async () => { + const { app } = setup(); + + // buildSimple: ledgerInfo, gasEstimation, sequenceNumber + mockJson.mockResolvedValueOnce( + mockJsonResponse({ + chain_id: 4, epoch: "1", ledger_version: "100", + oldest_ledger_version: "0", ledger_timestamp: "0", + node_role: "full_node", oldest_block_height: "0", block_height: "50", + }), + ); + mockJson.mockResolvedValueOnce( + mockJsonResponse({ gas_estimate: 100, deprioritized_gas_estimate: 50, prioritized_gas_estimate: 200 }), + ); + mockJson.mockResolvedValueOnce( + mockJsonResponse({ sequence_number: "0", authentication_key: "0x" }), + ); + // signAndSubmit (BCS POST) + mockBcs.mockResolvedValueOnce( + mockJsonResponse({ hash: "0xsponsored" }), + ); + // waitForTransaction + mockJson.mockResolvedValueOnce( + mockJsonResponse({ + type: "user_transaction", version: "5", hash: "0xsponsored", + success: true, vm_status: "Executed successfully", + sender: "0x1", sequence_number: "0", max_gas_amount: "200000", + gas_unit_price: "100", expiration_timestamp_secs: "9999999999", + payload: {}, events: [], timestamp: "2000", + state_change_hash: "0x", event_root_hash: "0x", + accumulator_root_hash: "0x", gas_used: "50", changes: [], + }), + ); + + const res = await app.request("/sponsor", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + function: "0x1::aptos_account::transfer", + functionArguments: ["0x1234", 1000], + }), + }); + + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.success).toBe(true); + expect(data.hash).toBe("0xsponsored"); + expect(data.version).toBe("5"); + }); + + it("POST /sponsor returns 400 for missing function", async () => { + const { app } = setup(); + const res = await app.request("/sponsor", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ functionArguments: [] }), + }); + expect(res.status).toBe(400); + }); +}); +``` + +**Step 2: Run test** + +Run: `cd /Users/greg/git/aptos-ts-sdk/v10/examples/sponsored-txn-server && npx vitest run tests/unit` +Expected: PASS + +**Step 3: Commit** + +```bash +git add examples/sponsored-txn-server/tests/ +git commit -m "test(examples): add sponsored-txn-server unit tests" +``` + +--- + +## Task 8: Sponsored Transaction Server — E2E Test + +**Files:** +- Create: `examples/sponsored-txn-server/tests/e2e/server.e2e.test.ts` + +**Step 1: Write e2e test** + +```typescript +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { Aptos, Network, generateAccount } from "@aptos-labs/ts-sdk"; +import { createApp } from "../../src/server.js"; + +const SKIP = !process.env.APTOS_E2E; + +describe.skipIf(SKIP)("sponsored-txn-server e2e", () => { + let app: ReturnType; + + beforeAll(async () => { + const aptos = new Aptos({ network: Network.DEVNET }); + const sponsor = generateAccount(); + await aptos.faucet.fund(sponsor.accountAddress, 500_000_000); + app = createApp({ aptos, sponsorAccount: sponsor }); + }); + + it("GET /health returns live chain info", async () => { + const res = await app.request("/health"); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.status).toBe("ok"); + expect(data.chain_id).toBeGreaterThan(0); + }); + + it("POST /sponsor submits a real transfer", async () => { + const bob = generateAccount(); + const res = await app.request("/sponsor", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + function: "0x1::aptos_account::transfer", + functionArguments: [bob.accountAddress.toString(), 1000], + }), + }); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.success).toBe(true); + expect(data.hash).toBeDefined(); + }, 30_000); +}); +``` + +**Step 2: Run (skipped by default)** + +Run: `cd /Users/greg/git/aptos-ts-sdk/v10/examples/sponsored-txn-server && npx vitest run` +Expected: PASS (e2e skipped) + +**Step 3: Commit** + +```bash +git add examples/sponsored-txn-server/tests/e2e/ +git commit -m "test(examples): add sponsored-txn-server e2e test" +``` + +--- + +## Task 9: dApp with Wallet — Project Scaffold + +**Files:** +- Create: `examples/dapp-with-wallet/package.json` +- Create: `examples/dapp-with-wallet/tsconfig.json` +- Create: `examples/dapp-with-wallet/vite.config.ts` +- Create: `examples/dapp-with-wallet/index.html` + +**Step 1: Create package.json** + +```json +{ + "name": "dapp-with-wallet-example", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "test": "vitest run", + "test:unit": "vitest run tests/unit", + "test:e2e": "APTOS_E2E=1 vitest run tests/e2e" + }, + "dependencies": { + "@aptos-labs/ts-sdk": "link:../..", + "@aptos-labs/wallet-adapter-react": "^4.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@testing-library/react": "^16.3.0", + "@testing-library/jest-dom": "^6.6.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.4.0", + "jsdom": "^26.0.0", + "typescript": "^5.9.3", + "vite": "^6.3.0", + "vitest": "^4.0.18" + } +} +``` + +**Step 2: Create tsconfig.json** + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["src", "tests"] +} +``` + +**Step 3: Create vite.config.ts** + +```typescript +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], +}); +``` + +**Step 4: Create index.html** + +```html + + + + + + Aptos v10 dApp with Wallet + + +
+ + + +``` + +**Step 5: Install dependencies** + +Run: `cd /Users/greg/git/aptos-ts-sdk/v10/examples/dapp-with-wallet && pnpm install` + +**Step 6: Commit** + +```bash +git add examples/dapp-with-wallet/ +git commit -m "feat(examples): scaffold dapp-with-wallet project" +``` + +--- + +## Task 10: dApp with Wallet — React Components + +**Files:** +- Create: `examples/dapp-with-wallet/src/main.tsx` +- Create: `examples/dapp-with-wallet/src/App.tsx` +- Create: `examples/dapp-with-wallet/src/WalletConnect.tsx` +- Create: `examples/dapp-with-wallet/src/TransferForm.tsx` + +**Step 1: Write main.tsx** + +```tsx +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { AptosWalletAdapterProvider } from "@aptos-labs/wallet-adapter-react"; +import { App } from "./App.js"; + +createRoot(document.getElementById("root")!).render( + + + + + , +); +``` + +**Step 2: Write App.tsx** + +```tsx +import { WalletConnect } from "./WalletConnect.js"; +import { TransferForm } from "./TransferForm.js"; +import { useWallet } from "@aptos-labs/wallet-adapter-react"; + +export function App() { + const { connected } = useWallet(); + + return ( +
+

Aptos v10 dApp

+

Uses the compat layer with wallet adapter

+ + {connected && } +
+ ); +} +``` + +**Step 3: Write WalletConnect.tsx** + +```tsx +import { useWallet } from "@aptos-labs/wallet-adapter-react"; + +export function WalletConnect() { + const { connect, disconnect, account, connected, wallets } = useWallet(); + + if (connected && account) { + return ( +
+

Connected: {account.address}

+ +
+ ); + } + + return ( +
+

Connect a wallet to get started

+ {wallets?.map((wallet) => ( + + ))} +
+ ); +} +``` + +**Step 4: Write TransferForm.tsx** + +This component uses the compat layer to build and submit transactions through the wallet adapter. + +```tsx +import { useState } from "react"; +import { useWallet } from "@aptos-labs/wallet-adapter-react"; +import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk/compat"; + +const aptos = new Aptos(new AptosConfig({ network: Network.DEVNET })); + +export function TransferForm() { + const { signAndSubmitTransaction } = useWallet(); + const [recipient, setRecipient] = useState(""); + const [amount, setAmount] = useState("1000"); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setResult(null); + setLoading(true); + + try { + const response = await signAndSubmitTransaction({ + data: { + function: "0x1::aptos_account::transfer", + functionArguments: [recipient, Number(amount)], + }, + }); + + // Wait for confirmation using v10 SDK + const committed = await aptos.waitForTransaction({ + transactionHash: response.hash, + }); + setResult(`Success! Version: ${committed.version}, Hash: ${committed.hash}`); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setLoading(false); + } + } + + return ( +
+

Transfer APT

+
+ +
+
+ +
+ + {result &&

{result}

} + {error &&

{error}

} +
+ ); +} +``` + +**Step 5: Commit** + +```bash +git add examples/dapp-with-wallet/src/ +git commit -m "feat(examples): add dapp-with-wallet React components" +``` + +--- + +## Task 11: dApp with Wallet — Unit Test + +**Files:** +- Create: `examples/dapp-with-wallet/tests/unit/app.test.tsx` + +**Step 1: Write the unit test** + +Tests components render correctly with mocked wallet context. + +```tsx +import { describe, expect, it, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { App } from "../../src/App.js"; + +// Mock wallet adapter +vi.mock("@aptos-labs/wallet-adapter-react", () => ({ + useWallet: vi.fn(() => ({ + connect: vi.fn(), + disconnect: vi.fn(), + connected: false, + account: null, + wallets: [{ name: "Test Wallet" }], + signAndSubmitTransaction: vi.fn(), + })), +})); + +describe("App", () => { + it("renders the title", () => { + render(); + expect(screen.getByText("Aptos v10 dApp")).toBeInTheDocument(); + }); + + it("shows connect prompt when disconnected", () => { + render(); + expect(screen.getByText("Connect a wallet to get started")).toBeInTheDocument(); + }); + + it("shows wallet buttons", () => { + render(); + expect(screen.getByText("Test Wallet")).toBeInTheDocument(); + }); + + it("does not show transfer form when disconnected", () => { + render(); + expect(screen.queryByText("Transfer APT")).not.toBeInTheDocument(); + }); +}); + +describe("App (connected)", () => { + it("shows transfer form when connected", async () => { + const { useWallet } = await import("@aptos-labs/wallet-adapter-react"); + vi.mocked(useWallet).mockReturnValue({ + connect: vi.fn(), + disconnect: vi.fn(), + connected: true, + account: { address: "0x1234", publicKey: "0x" }, + wallets: [], + signAndSubmitTransaction: vi.fn(), + } as any); + + render(); + expect(screen.getByText("Transfer APT")).toBeInTheDocument(); + expect(screen.getByText("Connected:")).toBeInTheDocument(); + }); +}); +``` + +**Step 2: Add vitest config with jsdom** + +Create `examples/dapp-with-wallet/vitest.config.ts`: + +```typescript +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: "jsdom", + }, +}); +``` + +**Step 3: Run tests** + +Run: `cd /Users/greg/git/aptos-ts-sdk/v10/examples/dapp-with-wallet && npx vitest run tests/unit` +Expected: PASS + +**Step 4: Commit** + +```bash +git add examples/dapp-with-wallet/tests/ examples/dapp-with-wallet/vitest.config.ts +git commit -m "test(examples): add dapp-with-wallet unit tests" +``` + +--- + +## Task 12: dApp with Wallet — E2E Test (Playwright) + +**Files:** +- Create: `examples/dapp-with-wallet/tests/e2e/app.e2e.test.ts` + +**Step 1: Write e2e test** + +This is a basic smoke test that verifies the app renders in a browser via Playwright. + +```typescript +import { describe, expect, it } from "vitest"; + +const SKIP = !process.env.APTOS_E2E; + +describe.skipIf(SKIP)("dapp-with-wallet e2e", () => { + it("renders the app title in the browser", async () => { + // This test requires `pnpm dev` running in the background + // and playwright installed. It's a smoke test only. + const res = await fetch("http://localhost:5173"); + expect(res.ok).toBe(true); + const html = await res.text(); + expect(html).toContain("Aptos v10 dApp"); + }); +}); +``` + +**Step 2: Commit** + +```bash +git add examples/dapp-with-wallet/tests/e2e/ +git commit -m "test(examples): add dapp-with-wallet e2e smoke test" +``` + +--- + +## Task 13: Final Review and Combined Commit + +**Step 1: Build SDK to ensure all examples can resolve imports** + +Run: `cd /Users/greg/git/aptos-ts-sdk/v10 && pnpm build` + +**Step 2: Run all unit tests across all examples** + +Run: +```bash +cd /Users/greg/git/aptos-ts-sdk/v10/examples/simple-transfer && npx vitest run tests/unit +cd /Users/greg/git/aptos-ts-sdk/v10/examples/sponsored-txn-server && npx vitest run tests/unit +cd /Users/greg/git/aptos-ts-sdk/v10/examples/dapp-with-wallet && npx vitest run tests/unit +``` +Expected: All PASS + +**Step 3: Run SDK's own unit tests to verify no regressions** + +Run: `cd /Users/greg/git/aptos-ts-sdk/v10 && pnpm test:unit` +Expected: 407 tests pass + +**Step 4: Run biome check on examples** + +Run: `cd /Users/greg/git/aptos-ts-sdk/v10 && npx biome check examples/` + +Fix any issues, then commit: + +```bash +git add -A examples/ +git commit -m "chore(examples): fix lint issues" +``` + +**Step 5: Push** + +```bash +git push +``` diff --git a/v10/examples/bun-test/package.json b/v10/examples/bun-test/package.json new file mode 100644 index 000000000..571aab81f --- /dev/null +++ b/v10/examples/bun-test/package.json @@ -0,0 +1,13 @@ +{ + "name": "v10-bun-sdk-test", + "version": "1.0.0", + "description": "Test Aptos TypeScript SDK v10 with Bun runtime", + "type": "module", + "license": "Apache-2.0", + "scripts": { + "test": "bun run simple_transfer.ts" + }, + "dependencies": { + "@aptos-labs/ts-sdk": "file:../.." + } +} diff --git a/v10/examples/bun-test/simple_transfer.ts b/v10/examples/bun-test/simple_transfer.ts new file mode 100644 index 000000000..6750c8fbd --- /dev/null +++ b/v10/examples/bun-test/simple_transfer.ts @@ -0,0 +1,61 @@ +/** + * v10 Bun runtime test — account generation, funding, transfer + * Uses v10 native API (no compat layer). + */ + +import { Aptos, Network, generateAccount, AccountAddress, U64 } from "@aptos-labs/ts-sdk"; +import type { Ed25519Account } from "@aptos-labs/ts-sdk"; + +const ALICE_INITIAL_BALANCE = 100_000_000; +const TRANSFER_AMOUNT = 1_000; + +const APTOS_NETWORK: Network = (Bun.env.APTOS_NETWORK as Network) || Network.LOCAL; + +const example = async () => { + console.log("v10 Bun Runtime Test: account generation, funding, and transfer"); + console.log(`Bun version: ${Bun.version}, network: ${APTOS_NETWORK}`); + + const aptos = new Aptos({ network: APTOS_NETWORK }); + + // Generate accounts + const alice = generateAccount() as Ed25519Account; + const bob = generateAccount() as Ed25519Account; + console.log(`Alice: ${alice.accountAddress}`); + console.log(`Bob: ${bob.accountAddress}`); + + // Fund Alice + console.log("\n=== Funding Alice ==="); + const fundTxn = await aptos.faucet.fund(alice.accountAddress, ALICE_INITIAL_BALANCE); + console.log(`Fund tx: ${fundTxn.hash} (success: ${fundTxn.success})`); + + // Check balance via view function + const [balanceBefore] = await aptos.general.view<[string]>({ + function: "0x1::coin::balance", + type_arguments: ["0x1::aptos_coin::AptosCoin"], + arguments: [alice.accountAddress.toString()], + }); + console.log(`Alice balance: ${balanceBefore}`); + if (Number(balanceBefore) !== ALICE_INITIAL_BALANCE) { + throw new Error(`Expected ${ALICE_INITIAL_BALANCE}, got ${balanceBefore}`); + } + + // Transfer Alice → Bob + console.log("\n=== Transfer ==="); + const tx = await aptos.transaction.buildSimple(alice.accountAddress, { + function: "0x1::aptos_account::transfer", + typeArguments: [], + functionArguments: [AccountAddress.from(bob.accountAddress), new U64(TRANSFER_AMOUNT)], + }); + + const pending = await aptos.transaction.signAndSubmit(alice, tx); + const committed = await aptos.transaction.waitForTransaction(pending.hash, { checkSuccess: true }); + console.log(`Transfer tx: ${committed.hash} (success: ${"success" in committed && committed.success})`); + + // Verify Bob received funds + const bobInfo = await aptos.account.getInfo(bob.accountAddress); + console.log(`Bob account exists: sequence_number=${bobInfo.sequence_number}`); + + console.log("\n=== v10 Bun Runtime Test Passed! ==="); +}; + +example(); diff --git a/v10/examples/dapp-with-wallet/index.html b/v10/examples/dapp-with-wallet/index.html new file mode 100644 index 000000000..15e9b0a8c --- /dev/null +++ b/v10/examples/dapp-with-wallet/index.html @@ -0,0 +1,12 @@ + + + + + + Aptos v10 dApp with Wallet + + +
+ + + diff --git a/v10/examples/dapp-with-wallet/package.json b/v10/examples/dapp-with-wallet/package.json new file mode 100644 index 000000000..af88768a7 --- /dev/null +++ b/v10/examples/dapp-with-wallet/package.json @@ -0,0 +1,31 @@ +{ + "name": "dapp-with-wallet-example", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "test": "vitest run", + "test:unit": "vitest run tests/unit", + "test:e2e": "APTOS_E2E=1 vitest run tests/e2e" + }, + "dependencies": { + "@aptos-labs/ts-sdk": "link:../..", + "@aptos-labs/wallet-adapter-react": "^4.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@testing-library/react": "^16.3.0", + "@testing-library/jest-dom": "^6.6.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.4.0", + "jsdom": "^26.0.0", + "typescript": "^5.9.3", + "vite": "^6.3.0", + "vitest": "^4.0.18" + } +} diff --git a/v10/examples/dapp-with-wallet/pnpm-lock.yaml b/v10/examples/dapp-with-wallet/pnpm-lock.yaml new file mode 100644 index 000000000..b7d12a24b --- /dev/null +++ b/v10/examples/dapp-with-wallet/pnpm-lock.yaml @@ -0,0 +1,3105 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@aptos-labs/ts-sdk': + specifier: link:../.. + version: link:../.. + '@aptos-labs/wallet-adapter-react': + specifier: ^4.0.0 + version: 4.1.5(@aptos-labs/ts-sdk@+)(@mizuwallet-sdk/core@1.4.0(@aptos-labs/ts-sdk@+)(@mizuwallet-sdk/protocol@0.0.6)(graphql-request@7.4.0(graphql@16.13.1)))(@mizuwallet-sdk/protocol@0.0.6)(@telegram-apps/bridge@1.9.2)(@types/react@19.2.14)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6))(axios@1.13.6)(got@11.8.6)(react@19.2.4) + react: + specifier: ^19.0.0 + version: 19.2.4 + react-dom: + specifier: ^19.0.0 + version: 19.2.4(react@19.2.4) + devDependencies: + '@testing-library/jest-dom': + specifier: ^6.6.0 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@types/react': + specifier: ^19.0.0 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.0.0 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^4.4.0 + version: 4.7.0(vite@6.4.1(@types/node@25.4.0)) + jsdom: + specifier: ^26.0.0 + version: 26.1.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^6.3.0 + version: 6.4.1(@types/node@25.4.0) + vitest: + specifier: ^4.0.18 + version: 4.0.18(@types/node@25.4.0)(jsdom@26.1.0) + +packages: + + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + + '@aptos-connect/wallet-adapter-plugin@2.4.1': + resolution: {integrity: sha512-jFuOEtnNWvi8VjvrXrNFp4iI2dci8Jv0l1FIuC5khMZKj2sDHLNF1dbZtcXNTRbD32KDrzl0Lngi42K0gReX8Q==} + peerDependencies: + '@aptos-labs/ts-sdk': ^1.33.1 + '@aptos-labs/wallet-standard': ^0.3.0 + + '@aptos-connect/wallet-api@0.1.9': + resolution: {integrity: sha512-Olxvg/Jpf426uiEIUbxFuoRluhX3dja9EUqklY29yw/wOY7QDFv0+Es71xp8R2lgaU3gPFJxUwko1Jwz0XjswQ==} + peerDependencies: + '@aptos-labs/ts-sdk': ^1.33.1 + aptos: ^1.20.0 + + '@aptos-connect/wallet-api@0.2.0': + resolution: {integrity: sha512-szCOoEfns7mGbBJHpfchQfN/hF2QVzS3tqLvmOfMXZ4s6eYJQZu9+NkTqIRxeNGIJKObYWKhHahRMmrA8f2Vsw==} + peerDependencies: + '@aptos-labs/ts-sdk': ^1.33.1 + aptos: ^1.20.0 + + '@aptos-connect/wallet-api@0.5.0': + resolution: {integrity: sha512-fXwgCj0ZZoNGfXuwjsWtb/L9n6nkspxPBPrvdmNxjKi0nbxUUGShYHIAujJwkmOs5ODjjI6fk26zRMkXFoVxlA==} + peerDependencies: + '@aptos-labs/ts-sdk': ^1.33.1 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + aptos: ^1.20.0 + + '@aptos-connect/web-transport@0.2.0': + resolution: {integrity: sha512-PzkXlJnUOuoXkBkOtuX4FHP2yiCZ/ySh0SpeA30XZxz+cOdYoxKWxRt82xPdTLCe0YmYl5SCG0Yh7DBL5lUvEg==} + peerDependencies: + '@aptos-labs/ts-sdk': ^1.33.1 + '@aptos-labs/wallet-standard': ^0.3.0 + '@telegram-apps/bridge': ^1.0.0 + aptos: ^1.20.0 + + '@aptos-labs/aptos-cli@1.1.1': + resolution: {integrity: sha512-sB7CokCM6s76SLJmccysbnFR+MDik6udKfj2+9ZsmTLV0/t73veIeCDKbvWJmbW267ibx4HiGbPI7L+1+yjEbQ==} + hasBin: true + + '@aptos-labs/aptos-client@1.2.0': + resolution: {integrity: sha512-pBlIAT/W+Qa0TOr/318U8r0Gxw/jfyRLcvDGMEXgcVrPqO9Qhwsmozw6LPPIZ963FB7smwIaMeexWFDs3zijcg==} + engines: {node: '>=15.10.0'} + deprecated: This version is deprecated. Please upgrade to version 2.1.0 or later. + peerDependencies: + axios: ^1.8.4 + got: ^11.8.6 + + '@aptos-labs/aptos-client@2.2.0': + resolution: {integrity: sha512-lYgHI8ehgD+Ykhix0IwzLaTCknHp1KNmExbq2bPZk8IeTwQg79D5BOkD46MjW0jGbJbl+J/RBtVF9vM7Te/hWA==} + engines: {node: '>=20.0.0'} + peerDependencies: + got: ^11.8.6 + + '@aptos-labs/aptos-dynamic-transaction-composer@0.1.5': + resolution: {integrity: sha512-Xs8BSCcL4hnpYONFC2e2Kj/vqvMVJU9l70vN0RNMagYLsvyiyNgUo55I+5uCrhIgj68bgxFsSoKUh/XDpDqRiw==} + + '@aptos-labs/script-composer-pack@0.0.9': + resolution: {integrity: sha512-Y3kA1rgF65HETgoTn2omDymsgO+fnZouPLrKJZ9sbxTGdOekIIHtGee3A2gk84eCqa02ZKBumZmP+IDCXRtU/g==} + + '@aptos-labs/ts-sdk@1.39.0': + resolution: {integrity: sha512-VFEWZsqb8Mto8XbLK8lDRdUvyHjp+geiwFxRzRcQK6HftMGB4bYuEsOI2Vy6u/TqkUft8DvmpV5yX027R5/SMQ==} + engines: {node: '>=20.0.0'} + deprecated: This version is deprecated, please upgrade to 5.2.1, or the latest version + + '@aptos-labs/wallet-adapter-core@5.1.4': + resolution: {integrity: sha512-rVTN1bu//dBT2Pqv9R7lDSswlpx6G1FjVKLa0DU1zg6BG95Ckx7D1RqLF7bYcoFG8pcFPdAk9QPkBs9gRvy7ww==} + peerDependencies: + '@aptos-labs/ts-sdk': ^1.37.1 + + '@aptos-labs/wallet-adapter-react@4.1.5': + resolution: {integrity: sha512-uGMcTbJIvLKj/DVBOv25GsLb8ybBfatDEr//qcbXyCQzcaQx4RA5hkg6fCL2kGykLHne4N41OImYuGc7jMM1TQ==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + + '@aptos-labs/wallet-standard@0.0.11': + resolution: {integrity: sha512-8dygyPBby7TaMJjUSyeVP4R1WC9D/FPpX9gVMMLaqTKCXrSbkzhGDxcuwbMZ3ziEwRmx3zz+d6BIJbDhd0hm5g==} + + '@aptos-labs/wallet-standard@0.1.0-ms.1': + resolution: {integrity: sha512-3aWEmdqMcll8D2lzhBZuYUW1o49TDpqw4QRAkHk00tSC3SwAkuukoW8g/M9lB5nHFxaX7UzuxeaYv8l6/mhJVQ==} + peerDependencies: + '@aptos-labs/ts-sdk': ^1.17.0 + '@wallet-standard/core': ^1.0.3 + + '@aptos-labs/wallet-standard@0.3.1': + resolution: {integrity: sha512-1cSmPxKB8R5HIlYPTKej7LzKflWbkod5t8peZ+OOHA3LbB1KI39qQn6gLid8kFBluvYsmkE1XEIIUyKLx07TsA==} + peerDependencies: + '@aptos-labs/ts-sdk': ^1.37.1 + '@wallet-standard/core': ^1.0.3 + + '@aptos-labs/wallet-standard@0.5.2': + resolution: {integrity: sha512-aFQOMLLRSmEYXHBGzhLGSOICoQRKqrtRyaMR7kDdyKtEI+4Ocgf2QeW+5yNZEUKoeWlydcBZGIuO7xAQpWU7dw==} + peerDependencies: + '@aptos-labs/ts-sdk': ^3.1.3 || ^4.0.0 || ^5.0.0 + '@wallet-standard/core': ^1.0.3 + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@atomrigslab/aptos-wallet-adapter@0.1.21': + resolution: {integrity: sha512-LwT0OTOaGglctggMcihXLd4mzBFwRoJsR0aeFBHQRfTxZV1agNTgN/PxJl6N13+WYAvzc00j/WByxAmWgonorA==} + peerDependencies: + '@aptos-labs/ts-sdk': ^1.9.0 + + '@atomrigslab/dekey-web-wallet-provider@1.2.1': + resolution: {integrity: sha512-GMEGjARgle9lIRopvxm4uis+sRr/ih26HzBgFbnLsk8+G94Z5dE87EclAIGFQUSAxYj7SmSk6xpx7//qUJDW/A==} + engines: {node: '>=12.0.0'} + + '@atomrigslab/providers@1.1.0': + resolution: {integrity: sha512-QLYxSCVrxwlN1oZ7vLnZbKZxkbZ6QG77Bj4pmTEowIpTcq7qZdBtU9pn+vqJAso1nnA3+AkmPuE9Jnx7+Jo1zQ==} + engines: {node: '>=12.0.0'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@graphql-typed-document-node/core@3.2.0': + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@identity-connect/api@0.7.0': + resolution: {integrity: sha512-mn/LZGeb3xgBD644p67tYOjvYSSdZpwxiO4/ZjwjsJZ8eYvGha5FiZg+pqVH73lg1S36qikwbkA3HUQOAE5GKA==} + + '@identity-connect/crypto@0.2.11': + resolution: {integrity: sha512-vpJhHuHsttjYhCMhYNVBtDOhgM0nIH9tGg6S5g3kFCOll3OXqT8I5hsNFL4RtIjMBoS5wjn5S8GK2bARAUjaGA==} + peerDependencies: + '@aptos-labs/ts-sdk': ^1.33.1 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + + '@identity-connect/dapp-sdk@0.10.5': + resolution: {integrity: sha512-2OTOIiOVnxW7ZvTbTHfsfkyTsWqmzHLz/l2cykHhvM3oOolBC9FGjiAkfW597V8UjN+Uqx2c0exI0ZGI5APjdw==} + peerDependencies: + '@aptos-labs/ts-sdk': ^1.33.1 + '@aptos-labs/wallet-standard': ^0.3.0 + + '@identity-connect/wallet-api@0.1.4': + resolution: {integrity: sha512-nXEfUYl1t9mgQu1RRmqH7lcWnsxet5IWtnsno5Vx5jw8PibVZDD5kgy5sZ3k/Brvw8DlhKPIjDwcYNAgcNHhvg==} + peerDependencies: + '@aptos-labs/ts-sdk': ^1.33.1 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + aptos: ^1.20.0 + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@metamask/object-multiplex@1.3.0': + resolution: {integrity: sha512-czcQeVYdSNtabd+NcYQnrM69MciiJyd1qvKH8WM2Id3C0ZiUUX5Xa/MK+/VUk633DBhVOwdNzAKIQ33lGyA+eQ==} + engines: {node: '>=12.0.0'} + + '@metamask/safe-event-emitter@2.0.0': + resolution: {integrity: sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==} + + '@microsoft/fetch-event-source@2.0.1': + resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==} + + '@mizuwallet-sdk/aptos-wallet-adapter@0.3.2': + resolution: {integrity: sha512-YljOzWoaTTp+dGZ6p0vTQwVBYe9AQeCLNfLcmKzSWU14ktEflIeDk85xVD0WRgQUbfyW707/18JcmKA+7V12rg==} + peerDependencies: + '@mizuwallet-sdk/core': '>=1.4.0' + '@mizuwallet-sdk/protocol': 0.0.6 + + '@mizuwallet-sdk/core@1.4.0': + resolution: {integrity: sha512-03jKqKr+P4kCgcNQT2YNXmFBRVmeZ88vpEFKpQ9SaorCY4L9lF56kJS4Y+e/+A4Gb1bnqA7xuFmnEz13LjsZyg==} + peerDependencies: + '@aptos-labs/ts-sdk': '>=1.14.0' + '@mizuwallet-sdk/protocol': 0.0.2 + graphql-request: '>=7.0.1' + + '@mizuwallet-sdk/protocol@0.0.6': + resolution: {integrity: sha512-I6ibbdPmPqsqc4JfCfI9qplZ2RcqeUxawyYBNb3TNhibMqQhoVUUaczt9kLuML20ODTvvZW/ja+5S6PXSzWPiw==} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.3.3': + resolution: {integrity: sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==} + engines: {node: '>= 16'} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@scure/base@1.1.9': + resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==} + + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + + '@scure/bip39@1.2.1': + resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} + + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + + '@telegram-apps/bridge@1.9.2': + resolution: {integrity: sha512-SJLcNWLXhbbZr9MiqFH/g2ceuitSJKMxUIZysK4zUNyTUNuonrQG80Q/yrO+XiNbKUj8WdDNM86NBARhuyyinQ==} + deprecated: This package is not supported anymore. Use @tma.js/bridge instead + + '@telegram-apps/signals@1.1.2': + resolution: {integrity: sha512-1P1kdCLX7MfETGPxH7f3UZKIsdE7Tz5S7QmN4Km1sbYQMakD5Bi1NecSMR7/wnHp50gWMI1JzENcMtCEmouhSg==} + + '@telegram-apps/toolkit@1.1.1': + resolution: {integrity: sha512-+vhKx6ngfvjyTE6Xagl3z1TPVbfx5s7xAkcYzCdHYUo6T60jLIqLgyZMcI1UPoIAMuMu1pHoO+p8QNCj/+tFmw==} + + '@telegram-apps/transformers@1.2.2': + resolution: {integrity: sha512-vvMwXckd1D7Ozc0h66PSUwF5QLrRV9HlGJFFeBuUex8QEk5mSPtsJkLiqB8aBbwuFDa91+TUSM/CxqPZO/e9YQ==} + deprecated: This package is not supported anymore. Use @tma.js/transfomers instead + + '@telegram-apps/types@1.2.1': + resolution: {integrity: sha512-so4HLh7clur0YyMthi9KVIgWoGpZdXlFOuQjk3+Q5NAvJZ11nAheBSwPlGw/Ko92+zwvrSBE/lQyN2+p17RP+w==} + deprecated: This package is not supported anymore. Use @tma.js/types instead + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/chrome@0.0.136': + resolution: {integrity: sha512-XDEiRhLkMd+SB7Iw3ZUIj/fov3wLd4HyTdLltVszkgl1dBfc3Rb7oPMVZ2Mz2TLqnF7Ow+StbR8E7r9lqpb4DA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/filesystem@0.0.36': + resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==} + + '@types/filewriter@0.0.33': + resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==} + + '@types/har-format@1.2.16': + resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==} + + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/node@25.4.0': + resolution: {integrity: sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitest/expect@4.0.18': + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + + '@vitest/mocker@4.0.18': + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.18': + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + + '@vitest/runner@4.0.18': + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + + '@vitest/snapshot@4.0.18': + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + + '@vitest/spy@4.0.18': + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + + '@vitest/utils@4.0.18': + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + + '@wallet-standard/app@1.1.0': + resolution: {integrity: sha512-3CijvrO9utx598kjr45hTbbeeykQrQfKmSnxeWOgU25TOEpvcipD/bYDQWIqUv1Oc6KK4YStokSMu/FBNecGUQ==} + engines: {node: '>=16'} + + '@wallet-standard/base@1.1.0': + resolution: {integrity: sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ==} + engines: {node: '>=16'} + + '@wallet-standard/core@1.0.3': + resolution: {integrity: sha512-Jb33IIjC1wM1HoKkYD7xQ6d6PZ8EmMZvyc8R7dFgX66n/xkvksVTW04g9yLvQXrLFbcIjHrCxW6TXMhvpsAAzg==} + engines: {node: '>=16'} + + '@wallet-standard/features@1.1.0': + resolution: {integrity: sha512-hiEivWNztx73s+7iLxsuD1sOJ28xtRix58W7Xnz4XzzA/pF0+aicnWgjOdA10doVDEDZdUuZCIIqG96SFNlDUg==} + engines: {node: '>=16'} + + '@wallet-standard/wallet@1.1.0': + resolution: {integrity: sha512-Gt8TnSlDZpAl+RWOOAB/kuvC7RpcdWAlFbHNoi4gsXsfaWa1QCT6LBcfIYTPdOZC9OVZUDwqGuGAcqZejDmHjg==} + engines: {node: '>=16'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + aptos@1.22.1: + resolution: {integrity: sha512-zw8IbCkMOpXdeAxp106W6CLHR8i88QY+z5u912XIlwZ3AngUVKY55b3rG8KP3uKEeLAIcY9FVWzS5ndzV60grg==} + engines: {node: '>=20.0.0'} + deprecated: Please update to the newer '@aptos-labs/ts-sdk'. 'aptos' is deprecated + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001777: + resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-browser@5.3.0: + resolution: {integrity: sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ed2curve@0.3.0: + resolution: {integrity: sha512-8w2fmmq3hv9rCrcI7g9hms2pMunQr1JINfcjwR9tAyZqhtyaMN991lF/ZfHfr5tzZQ8c7y7aBgZbjfbd0fjFwQ==} + + electron-to-chromium@1.5.307: + resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eth-rpc-errors@4.0.3: + resolution: {integrity: sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg==} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + extension-port-stream@2.1.1: + resolution: {integrity: sha512-qknp5o5rj2J9CRKfVB8KJr+uXQlrojNZzdESUPhKYLXf97TPcGf6qWWKmpsNNtUyOdzFhab1ON0jzouNxHHvow==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@2.0.1: + resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + + graphql-request@7.4.0: + resolution: {integrity: sha512-xfr+zFb/QYbs4l4ty0dltqiXIp07U6sl+tOKAb0t50/EnQek6CVVBLjETXi+FghElytvgaAWtIOt3EV7zLzIAQ==} + peerDependencies: + graphql: 14 - 16 + + graphql@16.13.1: + resolution: {integrity: sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-rpc-engine@6.1.0: + resolution: {integrity: sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ==} + engines: {node: '>=10.0.0'} + + json-rpc-middleware-stream@3.0.0: + resolution: {integrity: sha512-JmZmlehE0xF3swwORpLHny/GvW3MZxCsb2uFNBrn8TOqMqivzCfz232NSDLLOtIQlrPlgyEjiYpyzyOPFOzClw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + poseidon-lite@0.2.1: + resolution: {integrity: sha512-xIr+G6HeYfOhCuswdqcFpSX47SPhm0EpisWJ6h7fHlWwaVIvH3dLnejpatrtw6Xc6HaLrpq05y7VRfvDmDGIog==} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tweetnacl-util@0.15.1: + resolution: {integrity: sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==} + + tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.0.18: + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.18 + '@vitest/browser-preview': 4.0.18 + '@vitest/browser-webdriverio': 4.0.18 + '@vitest/ui': 4.0.18 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webextension-polyfill-ts@0.25.0: + resolution: {integrity: sha512-ikQhwwHYkpBu00pFaUzIKY26I6L87DeRI+Q6jBT1daZUNuu8dSrg5U9l/ZbqdaQ1M/TTSPKeAa3kolP5liuedw==} + deprecated: This project has moved to @types/webextension-polyfill + + webextension-polyfill@0.12.0: + resolution: {integrity: sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q==} + + webextension-polyfill@0.7.0: + resolution: {integrity: sha512-su48BkMLxqzTTvPSE1eWxKToPS2Tv5DLGxKexLEVpwFd6Po6N8hhSLIvG6acPAg7qERoEaDL+Y5HQJeJeml5Aw==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@adobe/css-tools@4.4.4': {} + + '@aptos-connect/wallet-adapter-plugin@2.4.1(@aptos-labs/ts-sdk@+)(@aptos-labs/wallet-standard@0.3.1(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3))(@telegram-apps/bridge@1.9.2)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6))': + dependencies: + '@aptos-connect/wallet-api': 0.1.9(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6)) + '@aptos-labs/ts-sdk': link:../.. + '@aptos-labs/wallet-standard': 0.3.1(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3) + '@identity-connect/crypto': 0.2.11(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6)) + '@identity-connect/dapp-sdk': 0.10.5(@aptos-labs/ts-sdk@+)(@aptos-labs/wallet-standard@0.3.1(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3))(@telegram-apps/bridge@1.9.2)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6)) + transitivePeerDependencies: + - '@telegram-apps/bridge' + - '@wallet-standard/core' + - aptos + - debug + + '@aptos-connect/wallet-api@0.1.9(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6))': + dependencies: + '@aptos-labs/ts-sdk': link:../.. + '@aptos-labs/wallet-standard': 0.3.1(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3) + '@identity-connect/api': 0.7.0 + aptos: 1.22.1(got@11.8.6) + transitivePeerDependencies: + - '@wallet-standard/core' + + '@aptos-connect/wallet-api@0.2.0(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6))': + dependencies: + '@aptos-labs/ts-sdk': link:../.. + '@aptos-labs/wallet-standard': 0.3.1(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3) + '@identity-connect/api': 0.7.0 + aptos: 1.22.1(got@11.8.6) + transitivePeerDependencies: + - '@wallet-standard/core' + + '@aptos-connect/wallet-api@0.5.0(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6))': + dependencies: + '@aptos-labs/ts-sdk': link:../.. + '@aptos-labs/wallet-standard': 0.5.2(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3) + '@identity-connect/api': 0.7.0 + aptos: 1.22.1(got@11.8.6) + transitivePeerDependencies: + - '@wallet-standard/core' + + '@aptos-connect/web-transport@0.2.0(@aptos-labs/ts-sdk@+)(@aptos-labs/wallet-standard@0.3.1(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3))(@telegram-apps/bridge@1.9.2)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6))': + dependencies: + '@aptos-connect/wallet-api': 0.2.0(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6)) + '@aptos-labs/ts-sdk': link:../.. + '@aptos-labs/wallet-standard': 0.3.1(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3) + '@telegram-apps/bridge': 1.9.2 + aptos: 1.22.1(got@11.8.6) + uuid: 9.0.1 + transitivePeerDependencies: + - '@wallet-standard/core' + + '@aptos-labs/aptos-cli@1.1.1': + dependencies: + commander: 12.1.0 + + '@aptos-labs/aptos-client@1.2.0(axios@1.13.6)(got@11.8.6)': + dependencies: + axios: 1.13.6 + got: 11.8.6 + + '@aptos-labs/aptos-client@2.2.0(got@11.8.6)': + dependencies: + got: 11.8.6 + + '@aptos-labs/aptos-dynamic-transaction-composer@0.1.5': {} + + '@aptos-labs/script-composer-pack@0.0.9': + dependencies: + '@aptos-labs/aptos-dynamic-transaction-composer': 0.1.5 + + '@aptos-labs/ts-sdk@1.39.0(axios@1.13.6)(got@11.8.6)': + dependencies: + '@aptos-labs/aptos-cli': 1.1.1 + '@aptos-labs/aptos-client': 1.2.0(axios@1.13.6)(got@11.8.6) + '@aptos-labs/script-composer-pack': 0.0.9 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + eventemitter3: 5.0.4 + form-data: 4.0.5 + js-base64: 3.7.8 + jwt-decode: 4.0.0 + poseidon-lite: 0.2.1 + transitivePeerDependencies: + - axios + - got + + '@aptos-labs/wallet-adapter-core@5.1.4(@aptos-labs/ts-sdk@+)(@mizuwallet-sdk/core@1.4.0(@aptos-labs/ts-sdk@+)(@mizuwallet-sdk/protocol@0.0.6)(graphql-request@7.4.0(graphql@16.13.1)))(@mizuwallet-sdk/protocol@0.0.6)(@telegram-apps/bridge@1.9.2)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6))(axios@1.13.6)(got@11.8.6)': + dependencies: + '@aptos-connect/wallet-adapter-plugin': 2.4.1(@aptos-labs/ts-sdk@+)(@aptos-labs/wallet-standard@0.3.1(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3))(@telegram-apps/bridge@1.9.2)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6)) + '@aptos-labs/ts-sdk': link:../.. + '@aptos-labs/wallet-standard': 0.3.1(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3) + '@atomrigslab/aptos-wallet-adapter': 0.1.21(@aptos-labs/ts-sdk@+)(axios@1.13.6)(got@11.8.6) + '@mizuwallet-sdk/aptos-wallet-adapter': 0.3.2(@mizuwallet-sdk/core@1.4.0(@aptos-labs/ts-sdk@+)(@mizuwallet-sdk/protocol@0.0.6)(graphql-request@7.4.0(graphql@16.13.1)))(@mizuwallet-sdk/protocol@0.0.6)(@wallet-standard/core@1.0.3)(axios@1.13.6)(got@11.8.6) + buffer: 6.0.3 + eventemitter3: 4.0.7 + tweetnacl: 1.0.3 + transitivePeerDependencies: + - '@mizuwallet-sdk/core' + - '@mizuwallet-sdk/protocol' + - '@telegram-apps/bridge' + - '@wallet-standard/core' + - aptos + - axios + - debug + - got + + '@aptos-labs/wallet-adapter-react@4.1.5(@aptos-labs/ts-sdk@+)(@mizuwallet-sdk/core@1.4.0(@aptos-labs/ts-sdk@+)(@mizuwallet-sdk/protocol@0.0.6)(graphql-request@7.4.0(graphql@16.13.1)))(@mizuwallet-sdk/protocol@0.0.6)(@telegram-apps/bridge@1.9.2)(@types/react@19.2.14)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6))(axios@1.13.6)(got@11.8.6)(react@19.2.4)': + dependencies: + '@aptos-labs/wallet-adapter-core': 5.1.4(@aptos-labs/ts-sdk@+)(@mizuwallet-sdk/core@1.4.0(@aptos-labs/ts-sdk@+)(@mizuwallet-sdk/protocol@0.0.6)(graphql-request@7.4.0(graphql@16.13.1)))(@mizuwallet-sdk/protocol@0.0.6)(@telegram-apps/bridge@1.9.2)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6))(axios@1.13.6)(got@11.8.6) + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + transitivePeerDependencies: + - '@aptos-labs/ts-sdk' + - '@mizuwallet-sdk/core' + - '@mizuwallet-sdk/protocol' + - '@telegram-apps/bridge' + - '@types/react' + - '@wallet-standard/core' + - aptos + - axios + - debug + - got + + '@aptos-labs/wallet-standard@0.0.11(axios@1.13.6)(got@11.8.6)': + dependencies: + '@aptos-labs/ts-sdk': 1.39.0(axios@1.13.6)(got@11.8.6) + '@wallet-standard/core': 1.0.3 + transitivePeerDependencies: + - axios + - got + + '@aptos-labs/wallet-standard@0.1.0-ms.1(@aptos-labs/ts-sdk@1.39.0(axios@1.13.6)(got@11.8.6))(@wallet-standard/core@1.0.3)': + dependencies: + '@aptos-labs/ts-sdk': 1.39.0(axios@1.13.6)(got@11.8.6) + '@wallet-standard/core': 1.0.3 + + '@aptos-labs/wallet-standard@0.3.1(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3)': + dependencies: + '@aptos-labs/ts-sdk': link:../.. + '@wallet-standard/core': 1.0.3 + + '@aptos-labs/wallet-standard@0.5.2(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3)': + dependencies: + '@aptos-labs/ts-sdk': link:../.. + '@wallet-standard/core': 1.0.3 + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@atomrigslab/aptos-wallet-adapter@0.1.21(@aptos-labs/ts-sdk@+)(axios@1.13.6)(got@11.8.6)': + dependencies: + '@aptos-labs/ts-sdk': link:../.. + '@aptos-labs/wallet-standard': 0.0.11(axios@1.13.6)(got@11.8.6) + '@atomrigslab/dekey-web-wallet-provider': 1.2.1 + transitivePeerDependencies: + - axios + - got + + '@atomrigslab/dekey-web-wallet-provider@1.2.1': + dependencies: + '@atomrigslab/providers': 1.1.0 + + '@atomrigslab/providers@1.1.0': + dependencies: + '@metamask/object-multiplex': 1.3.0 + '@metamask/safe-event-emitter': 2.0.0 + '@types/chrome': 0.0.136 + detect-browser: 5.3.0 + eth-rpc-errors: 4.0.3 + extension-port-stream: 2.1.1 + fast-deep-equal: 2.0.1 + is-stream: 2.0.1 + json-rpc-engine: 6.1.0 + json-rpc-middleware-stream: 3.0.0 + pump: 3.0.4 + webextension-polyfill-ts: 0.25.0 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/runtime@7.28.6': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@graphql-typed-document-node/core@3.2.0(graphql@16.13.1)': + dependencies: + graphql: 16.13.1 + + '@identity-connect/api@0.7.0': {} + + '@identity-connect/crypto@0.2.11(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6))': + dependencies: + '@aptos-connect/wallet-api': 0.5.0(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6)) + '@aptos-labs/ts-sdk': link:../.. + '@noble/hashes': 1.8.0 + ed2curve: 0.3.0 + tweetnacl: 1.0.3 + transitivePeerDependencies: + - '@wallet-standard/core' + - aptos + + '@identity-connect/dapp-sdk@0.10.5(@aptos-labs/ts-sdk@+)(@aptos-labs/wallet-standard@0.3.1(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3))(@telegram-apps/bridge@1.9.2)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6))': + dependencies: + '@aptos-connect/wallet-api': 0.2.0(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6)) + '@aptos-connect/web-transport': 0.2.0(@aptos-labs/ts-sdk@+)(@aptos-labs/wallet-standard@0.3.1(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3))(@telegram-apps/bridge@1.9.2)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6)) + '@aptos-labs/ts-sdk': link:../.. + '@aptos-labs/wallet-standard': 0.3.1(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3) + '@identity-connect/api': 0.7.0 + '@identity-connect/crypto': 0.2.11(@aptos-labs/ts-sdk@+)(@wallet-standard/core@1.0.3)(aptos@1.22.1(got@11.8.6)) + '@identity-connect/wallet-api': 0.1.4(@aptos-labs/ts-sdk@+)(aptos@1.22.1(got@11.8.6)) + axios: 1.13.6 + uuid: 9.0.1 + transitivePeerDependencies: + - '@telegram-apps/bridge' + - '@wallet-standard/core' + - aptos + - debug + + '@identity-connect/wallet-api@0.1.4(@aptos-labs/ts-sdk@+)(aptos@1.22.1(got@11.8.6))': + dependencies: + '@aptos-labs/ts-sdk': link:../.. + aptos: 1.22.1(got@11.8.6) + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@metamask/object-multiplex@1.3.0': + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + readable-stream: 2.3.8 + + '@metamask/safe-event-emitter@2.0.0': {} + + '@microsoft/fetch-event-source@2.0.1': {} + + '@mizuwallet-sdk/aptos-wallet-adapter@0.3.2(@mizuwallet-sdk/core@1.4.0(@aptos-labs/ts-sdk@+)(@mizuwallet-sdk/protocol@0.0.6)(graphql-request@7.4.0(graphql@16.13.1)))(@mizuwallet-sdk/protocol@0.0.6)(@wallet-standard/core@1.0.3)(axios@1.13.6)(got@11.8.6)': + dependencies: + '@aptos-labs/ts-sdk': 1.39.0(axios@1.13.6)(got@11.8.6) + '@aptos-labs/wallet-standard': 0.1.0-ms.1(@aptos-labs/ts-sdk@1.39.0(axios@1.13.6)(got@11.8.6))(@wallet-standard/core@1.0.3) + '@mizuwallet-sdk/core': 1.4.0(@aptos-labs/ts-sdk@+)(@mizuwallet-sdk/protocol@0.0.6)(graphql-request@7.4.0(graphql@16.13.1)) + '@mizuwallet-sdk/protocol': 0.0.6 + buffer: 6.0.3 + transitivePeerDependencies: + - '@wallet-standard/core' + - axios + - got + + '@mizuwallet-sdk/core@1.4.0(@aptos-labs/ts-sdk@+)(@mizuwallet-sdk/protocol@0.0.6)(graphql-request@7.4.0(graphql@16.13.1))': + dependencies: + '@aptos-labs/ts-sdk': link:../.. + '@mizuwallet-sdk/protocol': 0.0.6 + buffer: 6.0.3 + graphql-request: 7.4.0(graphql@16.13.1) + jwt-decode: 4.0.0 + + '@mizuwallet-sdk/protocol@0.0.6': + dependencies: + '@microsoft/fetch-event-source': 2.0.1 + tweetnacl: 1.0.3 + tweetnacl-util: 0.15.1 + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.3.3': {} + + '@noble/hashes@1.8.0': {} + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@scure/base@1.1.9': {} + + '@scure/base@1.2.6': {} + + '@scure/bip32@1.7.0': + dependencies: + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@scure/bip39@1.2.1': + dependencies: + '@noble/hashes': 1.3.3 + '@scure/base': 1.1.9 + + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@sindresorhus/is@4.6.0': {} + + '@standard-schema/spec@1.1.0': {} + + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + + '@telegram-apps/bridge@1.9.2': + dependencies: + '@telegram-apps/signals': 1.1.2 + '@telegram-apps/toolkit': 1.1.1 + '@telegram-apps/transformers': 1.2.2 + '@telegram-apps/types': 1.2.1 + + '@telegram-apps/signals@1.1.2': {} + + '@telegram-apps/toolkit@1.1.1': {} + + '@telegram-apps/transformers@1.2.2': + dependencies: + '@telegram-apps/toolkit': 1.1.1 + '@telegram-apps/types': 1.2.1 + + '@telegram-apps/types@1.2.1': {} + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.28.6 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + '@testing-library/dom': 10.4.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 25.4.0 + '@types/responselike': 1.0.3 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/chrome@0.0.136': + dependencies: + '@types/filesystem': 0.0.36 + '@types/har-format': 1.2.16 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/filesystem@0.0.36': + dependencies: + '@types/filewriter': 0.0.33 + + '@types/filewriter@0.0.33': {} + + '@types/har-format@1.2.16': {} + + '@types/http-cache-semantics@4.2.0': {} + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 25.4.0 + + '@types/node@25.4.0': + dependencies: + undici-types: 7.18.2 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 25.4.0 + + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.4.0))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.1(@types/node@25.4.0) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@4.0.18': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + chai: 6.2.2 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.18(vite@6.4.1(@types/node@25.4.0))': + dependencies: + '@vitest/spy': 4.0.18 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.1(@types/node@25.4.0) + + '@vitest/pretty-format@4.0.18': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.18': + dependencies: + '@vitest/utils': 4.0.18 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.18': {} + + '@vitest/utils@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + tinyrainbow: 3.0.3 + + '@wallet-standard/app@1.1.0': + dependencies: + '@wallet-standard/base': 1.1.0 + + '@wallet-standard/base@1.1.0': {} + + '@wallet-standard/core@1.0.3': + dependencies: + '@wallet-standard/app': 1.1.0 + '@wallet-standard/base': 1.1.0 + '@wallet-standard/features': 1.1.0 + '@wallet-standard/wallet': 1.1.0 + + '@wallet-standard/features@1.1.0': + dependencies: + '@wallet-standard/base': 1.1.0 + + '@wallet-standard/wallet@1.1.0': + dependencies: + '@wallet-standard/base': 1.1.0 + + agent-base@7.1.4: {} + + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + + aptos@1.22.1(got@11.8.6): + dependencies: + '@aptos-labs/aptos-client': 2.2.0(got@11.8.6) + '@noble/hashes': 1.3.3 + '@scure/bip39': 1.2.1 + eventemitter3: 5.0.4 + tweetnacl: 1.0.3 + transitivePeerDependencies: + - got + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + + asynckit@0.4.0: {} + + axios@1.13.6: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.0: {} + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001777 + electron-to-chromium: 1.5.307 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + caniuse-lite@1.0.30001777: {} + + chai@6.2.2: {} + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@12.1.0: {} + + convert-source-map@2.0.0: {} + + core-util-is@1.0.3: {} + + css.escape@1.5.1: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + csstype@3.2.3: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + defer-to-connect@2.0.1: {} + + delayed-stream@1.0.0: {} + + dequal@2.0.3: {} + + detect-browser@5.3.0: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ed2curve@0.3.0: + dependencies: + tweetnacl: 1.0.3 + + electron-to-chromium@1.5.307: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + entities@6.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + eth-rpc-errors@4.0.3: + dependencies: + fast-safe-stringify: 2.1.1 + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.4: {} + + expect-type@1.3.0: {} + + extension-port-stream@2.1.1: + dependencies: + webextension-polyfill: 0.12.0 + + fast-deep-equal@2.0.1: {} + + fast-safe-stringify@2.1.1: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + gopd@1.2.0: {} + + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + + graphql-request@7.4.0(graphql@16.13.1): + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.13.1) + graphql: 16.13.1 + + graphql@16.13.1: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-cache-semantics@4.2.0: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + indent-string@4.0.0: {} + + inherits@2.0.4: {} + + is-potential-custom-element-name@1.0.1: {} + + is-stream@2.0.1: {} + + isarray@1.0.0: {} + + js-base64@3.7.8: {} + + js-tokens@4.0.0: {} + + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-rpc-engine@6.1.0: + dependencies: + '@metamask/safe-event-emitter': 2.0.0 + eth-rpc-errors: 4.0.3 + + json-rpc-middleware-stream@3.0.0: + dependencies: + '@metamask/safe-event-emitter': 2.0.0 + readable-stream: 2.3.8 + + json5@2.2.3: {} + + jwt-decode@4.0.0: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + lowercase-keys@2.0.0: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + + min-indent@1.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + node-releases@2.0.36: {} + + normalize-url@6.1.0: {} + + nwsapi@2.2.23: {} + + obug@2.1.1: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + p-cancelable@2.1.1: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + poseidon-lite@0.2.1: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + process-nextick-args@2.0.1: {} + + proxy-from-env@1.1.0: {} + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + quick-lru@5.1.1: {} + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-is@17.0.2: {} + + react-refresh@0.17.0: {} + + react@19.2.4: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + resolve-alpn@1.2.1: {} + + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + rrweb-cssom@0.8.0: {} + + safe-buffer@5.1.2: {} + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + symbol-tree@3.2.4: {} + + tinybench@2.9.0: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyrainbow@3.0.3: {} + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tweetnacl-util@0.15.1: {} + + tweetnacl@1.0.3: {} + + typescript@5.9.3: {} + + undici-types@7.18.2: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + uuid@9.0.1: {} + + vite@6.4.1(@types/node@25.4.0): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.4.0 + fsevents: 2.3.3 + + vitest@4.0.18(@types/node@25.4.0)(jsdom@26.1.0): + dependencies: + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(vite@6.4.1(@types/node@25.4.0)) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 6.4.1(@types/node@25.4.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.4.0 + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webextension-polyfill-ts@0.25.0: + dependencies: + webextension-polyfill: 0.7.0 + + webextension-polyfill@0.12.0: {} + + webextension-polyfill@0.7.0: {} + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrappy@1.0.2: {} + + ws@8.19.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} diff --git a/v10/examples/dapp-with-wallet/src/App.tsx b/v10/examples/dapp-with-wallet/src/App.tsx new file mode 100644 index 000000000..f70eacd51 --- /dev/null +++ b/v10/examples/dapp-with-wallet/src/App.tsx @@ -0,0 +1,15 @@ +import { useWallet } from "@aptos-labs/wallet-adapter-react"; +import { WalletConnect } from "./WalletConnect.js"; +import { TransferForm } from "./TransferForm.js"; + +export function App() { + const { connected } = useWallet(); + return ( +
+

Aptos v10 dApp

+

Uses the compat layer with wallet adapter

+ + {connected && } +
+ ); +} diff --git a/v10/examples/dapp-with-wallet/src/TransferForm.tsx b/v10/examples/dapp-with-wallet/src/TransferForm.tsx new file mode 100644 index 000000000..142d1a5c1 --- /dev/null +++ b/v10/examples/dapp-with-wallet/src/TransferForm.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; +import { useWallet } from "@aptos-labs/wallet-adapter-react"; +import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk/compat"; + +const aptos = new Aptos(new AptosConfig({ network: Network.DEVNET })); + +export function TransferForm() { + const { signAndSubmitTransaction } = useWallet(); + const [recipient, setRecipient] = useState(""); + const [amount, setAmount] = useState("1000"); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setResult(null); + setLoading(true); + try { + const response = await signAndSubmitTransaction({ + data: { + function: "0x1::aptos_account::transfer", + functionArguments: [recipient, Number(amount)], + }, + }); + const committed = await aptos.waitForTransaction({ + transactionHash: response.hash, + }); + setResult( + `Success! Version: ${committed.version}, Hash: ${committed.hash}`, + ); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setLoading(false); + } + } + + return ( +
+

Transfer APT

+
+ +
+
+ +
+ + {result &&

{result}

} + {error &&

{error}

} +
+ ); +} diff --git a/v10/examples/dapp-with-wallet/src/WalletConnect.tsx b/v10/examples/dapp-with-wallet/src/WalletConnect.tsx new file mode 100644 index 000000000..69e299ce9 --- /dev/null +++ b/v10/examples/dapp-with-wallet/src/WalletConnect.tsx @@ -0,0 +1,34 @@ +import { useWallet } from "@aptos-labs/wallet-adapter-react"; + +export function WalletConnect() { + const { connect, disconnect, account, connected, wallets } = useWallet(); + + if (connected && account) { + return ( +
+

+ Connected: {account.address} +

+ +
+ ); + } + + return ( +
+

Connect a wallet to get started

+ {wallets?.map((wallet) => ( + + ))} +
+ ); +} diff --git a/v10/examples/dapp-with-wallet/src/main.tsx b/v10/examples/dapp-with-wallet/src/main.tsx new file mode 100644 index 000000000..766ef9888 --- /dev/null +++ b/v10/examples/dapp-with-wallet/src/main.tsx @@ -0,0 +1,12 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { AptosWalletAdapterProvider } from "@aptos-labs/wallet-adapter-react"; +import { App } from "./App.js"; + +createRoot(document.getElementById("root")!).render( + + + + + , +); diff --git a/v10/examples/dapp-with-wallet/tests/e2e/app.e2e.test.ts b/v10/examples/dapp-with-wallet/tests/e2e/app.e2e.test.ts new file mode 100644 index 000000000..71afb3d1b --- /dev/null +++ b/v10/examples/dapp-with-wallet/tests/e2e/app.e2e.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; + +const SKIP = !process.env.APTOS_E2E; + +describe.skipIf(SKIP)("dapp-with-wallet e2e", () => { + it("renders the app title in the browser", async () => { + const res = await fetch("http://localhost:5173"); + expect(res.ok).toBe(true); + const html = await res.text(); + expect(html).toContain("Aptos v10 dApp"); + }); +}); diff --git a/v10/examples/dapp-with-wallet/tests/unit/app.test.tsx b/v10/examples/dapp-with-wallet/tests/unit/app.test.tsx new file mode 100644 index 000000000..49fb1cdf0 --- /dev/null +++ b/v10/examples/dapp-with-wallet/tests/unit/app.test.tsx @@ -0,0 +1,58 @@ +import { describe, expect, it, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { App } from "../../src/App.js"; + +// Mock wallet adapter +vi.mock("@aptos-labs/wallet-adapter-react", () => ({ + useWallet: vi.fn(() => ({ + connect: vi.fn(), + disconnect: vi.fn(), + connected: false, + account: null, + wallets: [{ name: "Test Wallet" }], + signAndSubmitTransaction: vi.fn(), + })), +})); + +describe("App", () => { + it("renders the title", () => { + render(); + expect(screen.getByText("Aptos v10 dApp")).toBeInTheDocument(); + }); + + it("shows connect prompt when disconnected", () => { + render(); + expect( + screen.getByText("Connect a wallet to get started"), + ).toBeInTheDocument(); + }); + + it("shows wallet buttons", () => { + render(); + expect(screen.getByText("Test Wallet")).toBeInTheDocument(); + }); + + it("does not show transfer form when disconnected", () => { + render(); + expect(screen.queryByText("Transfer APT")).not.toBeInTheDocument(); + }); +}); + +describe("App (connected)", () => { + it("shows transfer form when connected", async () => { + const { useWallet } = await import("@aptos-labs/wallet-adapter-react"); + vi.mocked(useWallet).mockReturnValue({ + connect: vi.fn(), + disconnect: vi.fn(), + connected: true, + account: { address: "0x1234", publicKey: "0x" }, + wallets: [], + signAndSubmitTransaction: vi.fn(), + } as any); + + render(); + expect(screen.getByText("Transfer APT")).toBeInTheDocument(); + expect(screen.getByText(/Connected:/)).toBeInTheDocument(); + }); +}); diff --git a/v10/examples/dapp-with-wallet/tsconfig.json b/v10/examples/dapp-with-wallet/tsconfig.json new file mode 100644 index 000000000..163d51ed9 --- /dev/null +++ b/v10/examples/dapp-with-wallet/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["src", "tests"] +} diff --git a/v10/examples/dapp-with-wallet/vite.config.ts b/v10/examples/dapp-with-wallet/vite.config.ts new file mode 100644 index 000000000..081c8d9f6 --- /dev/null +++ b/v10/examples/dapp-with-wallet/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], +}); diff --git a/v10/examples/dapp-with-wallet/vitest-setup.ts b/v10/examples/dapp-with-wallet/vitest-setup.ts new file mode 100644 index 000000000..d0de870dc --- /dev/null +++ b/v10/examples/dapp-with-wallet/vitest-setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom"; diff --git a/v10/examples/dapp-with-wallet/vitest.config.ts b/v10/examples/dapp-with-wallet/vitest.config.ts new file mode 100644 index 000000000..71d928837 --- /dev/null +++ b/v10/examples/dapp-with-wallet/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: "jsdom", + setupFiles: ["./vitest-setup.ts"], + }, +}); diff --git a/v10/examples/deno-test/deno.json b/v10/examples/deno-test/deno.json new file mode 100644 index 000000000..edffe99f3 --- /dev/null +++ b/v10/examples/deno-test/deno.json @@ -0,0 +1,9 @@ +{ + "name": "v10-deno-sdk-test", + "version": "1.0.0", + "exports": "./simple_transfer.ts", + "nodeModulesDir": "manual", + "tasks": { + "test": "deno run --allow-net --allow-env --allow-read --node-modules-dir=manual simple_transfer.ts" + } +} diff --git a/v10/examples/deno-test/package.json b/v10/examples/deno-test/package.json new file mode 100644 index 000000000..cfa46432e --- /dev/null +++ b/v10/examples/deno-test/package.json @@ -0,0 +1,10 @@ +{ + "name": "v10-deno-sdk-test", + "version": "1.0.0", + "description": "Test Aptos TypeScript SDK v10 with Deno runtime", + "type": "module", + "license": "Apache-2.0", + "dependencies": { + "@aptos-labs/ts-sdk": "file:../.." + } +} diff --git a/v10/examples/deno-test/simple_transfer.ts b/v10/examples/deno-test/simple_transfer.ts new file mode 100644 index 000000000..3d3201732 --- /dev/null +++ b/v10/examples/deno-test/simple_transfer.ts @@ -0,0 +1,61 @@ +/** + * v10 Deno runtime test — account generation, funding, transfer + * Uses v10 native API (no compat layer). + */ + +import { Aptos, Network, generateAccount, AccountAddress, U64 } from "@aptos-labs/ts-sdk"; +import type { Ed25519Account } from "@aptos-labs/ts-sdk"; + +const ALICE_INITIAL_BALANCE = 100_000_000; +const TRANSFER_AMOUNT = 1_000; + +const APTOS_NETWORK: Network = (Deno.env.get("APTOS_NETWORK") as Network) || Network.LOCAL; + +const example = async () => { + console.log("v10 Deno Runtime Test: account generation, funding, and transfer"); + console.log(`Deno version: ${Deno.version.deno}, network: ${APTOS_NETWORK}`); + + const aptos = new Aptos({ network: APTOS_NETWORK }); + + // Generate accounts + const alice = generateAccount() as Ed25519Account; + const bob = generateAccount() as Ed25519Account; + console.log(`Alice: ${alice.accountAddress}`); + console.log(`Bob: ${bob.accountAddress}`); + + // Fund Alice + console.log("\n=== Funding Alice ==="); + const fundTxn = await aptos.faucet.fund(alice.accountAddress, ALICE_INITIAL_BALANCE); + console.log(`Fund tx: ${fundTxn.hash} (success: ${fundTxn.success})`); + + // Check balance via view function + const [balanceBefore] = await aptos.general.view<[string]>({ + function: "0x1::coin::balance", + type_arguments: ["0x1::aptos_coin::AptosCoin"], + arguments: [alice.accountAddress.toString()], + }); + console.log(`Alice balance: ${balanceBefore}`); + if (Number(balanceBefore) !== ALICE_INITIAL_BALANCE) { + throw new Error(`Expected ${ALICE_INITIAL_BALANCE}, got ${balanceBefore}`); + } + + // Transfer Alice → Bob + console.log("\n=== Transfer ==="); + const tx = await aptos.transaction.buildSimple(alice.accountAddress, { + function: "0x1::aptos_account::transfer", + typeArguments: [], + functionArguments: [AccountAddress.from(bob.accountAddress), new U64(TRANSFER_AMOUNT)], + }); + + const pending = await aptos.transaction.signAndSubmit(alice, tx); + const committed = await aptos.transaction.waitForTransaction(pending.hash, { checkSuccess: true }); + console.log(`Transfer tx: ${committed.hash} (success: ${"success" in committed && committed.success})`); + + // Verify Bob received funds + const bobInfo = await aptos.account.getInfo(bob.accountAddress); + console.log(`Bob account exists: sequence_number=${bobInfo.sequence_number}`); + + console.log("\n=== v10 Deno Runtime Test Passed! ==="); +}; + +await example(); diff --git a/v10/examples/simple-transfer/package.json b/v10/examples/simple-transfer/package.json new file mode 100644 index 000000000..fd304f8eb --- /dev/null +++ b/v10/examples/simple-transfer/package.json @@ -0,0 +1,20 @@ +{ + "name": "simple-transfer-example", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "tsx src/main.ts", + "test": "vitest run", + "test:unit": "vitest run tests/unit", + "test:e2e": "APTOS_E2E=1 vitest run tests/e2e" + }, + "dependencies": { + "@aptos-labs/ts-sdk": "link:../.." + }, + "devDependencies": { + "tsx": "^4.19.0", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + } +} diff --git a/v10/examples/simple-transfer/pnpm-lock.yaml b/v10/examples/simple-transfer/pnpm-lock.yaml new file mode 100644 index 000000000..863325059 --- /dev/null +++ b/v10/examples/simple-transfer/pnpm-lock.yaml @@ -0,0 +1,945 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@aptos-labs/ts-sdk': + specifier: link:../.. + version: link:../.. + devDependencies: + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(tsx@4.21.0) + +packages: + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@vitest/expect@4.0.18': + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + + '@vitest/mocker@4.0.18': + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.18': + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + + '@vitest/runner@4.0.18': + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + + '@vitest/snapshot@4.0.18': + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + + '@vitest/spy@4.0.18': + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + + '@vitest/utils@4.0.18': + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.0.18: + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.18 + '@vitest/browser-preview': 4.0.18 + '@vitest/browser-webdriverio': 4.0.18 + '@vitest/ui': 4.0.18 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + +snapshots: + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@standard-schema/spec@1.1.0': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@vitest/expect@4.0.18': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + chai: 6.2.2 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.18(vite@7.3.1(tsx@4.21.0))': + dependencies: + '@vitest/spy': 4.0.18 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(tsx@4.21.0) + + '@vitest/pretty-format@4.0.18': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.18': + dependencies: + '@vitest/utils': 4.0.18 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.18': {} + + '@vitest/utils@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + tinyrainbow: 3.0.3 + + assertion-error@2.0.1: {} + + chai@6.2.2: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + nanoid@3.3.11: {} + + obug@2.1.1: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + resolve-pkg-maps@1.0.0: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyrainbow@3.0.3: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + vite@7.3.1(tsx@4.21.0): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 + tsx: 4.21.0 + + vitest@4.0.18(tsx@4.21.0): + dependencies: + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(vite@7.3.1(tsx@4.21.0)) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.3.1(tsx@4.21.0) + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 diff --git a/v10/examples/simple-transfer/src/main.ts b/v10/examples/simple-transfer/src/main.ts new file mode 100644 index 000000000..da722fab0 --- /dev/null +++ b/v10/examples/simple-transfer/src/main.ts @@ -0,0 +1,82 @@ +/** + * Simple Transfer Example + * + * Demonstrates the core Aptos transaction flow: + * 1. Generate two accounts (Alice and Bob) + * 2. Fund Alice via the faucet + * 3. Check Alice's balance + * 4. Build a transfer transaction + * 5. Sign and submit the transaction + * 6. Wait for confirmation + * 7. Check Bob's balance + */ + +import { Aptos, Network, generateAccount, U64 } from "@aptos-labs/ts-sdk"; + +const TRANSFER_AMOUNT = 1_000_000; // 0.01 APT in Octas + +async function main() { + // Initialize the Aptos client for devnet + const aptos = new Aptos({ network: Network.DEVNET }); + + // Step 1: Generate two accounts + const alice = generateAccount(); + const bob = generateAccount(); + + console.log("=== Account Addresses ==="); + console.log(`Alice: ${alice.accountAddress}`); + console.log(`Bob: ${bob.accountAddress}`); + + // Step 2: Fund Alice's account from the faucet (1 APT = 100_000_000 Octas) + console.log("\n=== Funding Alice ==="); + const fundTxn = await aptos.faucet.fund(alice.accountAddress, 100_000_000); + console.log(`Faucet transaction committed: ${fundTxn.hash}`); + + // Step 3: Check Alice's balance using a view function + console.log("\n=== Checking Balances ==="); + const [aliceBalance] = await aptos.general.view({ + function: "0x1::coin::balance", + type_arguments: ["0x1::aptos_coin::AptosCoin"], + arguments: [alice.accountAddress.toString()], + }); + console.log(`Alice's balance: ${aliceBalance} Octas`); + + // Step 4: Build a transfer transaction from Alice to Bob + console.log("\n=== Building Transfer ==="); + const transaction = await aptos.transaction.buildSimple( + alice.accountAddress, + { + function: "0x1::aptos_account::transfer", + typeArguments: [], + functionArguments: [bob.accountAddress, new U64(TRANSFER_AMOUNT)], + }, + ); + console.log("Transaction built successfully"); + + // Step 5: Sign and submit the transaction + console.log("\n=== Signing & Submitting ==="); + const pendingTxn = await aptos.transaction.signAndSubmit(alice, transaction); + console.log(`Submitted transaction hash: ${pendingTxn.hash}`); + + // Step 6: Wait for the transaction to be committed on-chain + console.log("\n=== Waiting for Confirmation ==="); + const committedTxn = await aptos.transaction.waitForTransaction( + pendingTxn.hash, + ); + console.log( + `Transaction confirmed! Version: ${committedTxn.version}, Gas used: ${committedTxn.gas_used}`, + ); + + // Step 7: Check Bob's balance + console.log("\n=== Final Balances ==="); + const [bobBalance] = await aptos.general.view({ + function: "0x1::coin::balance", + type_arguments: ["0x1::aptos_coin::AptosCoin"], + arguments: [bob.accountAddress.toString()], + }); + console.log(`Bob's balance: ${bobBalance} Octas`); + + console.log("\nDone! Transferred", TRANSFER_AMOUNT, "Octas from Alice to Bob."); +} + +main().catch(console.error); diff --git a/v10/examples/simple-transfer/tests/e2e/transfer.e2e.test.ts b/v10/examples/simple-transfer/tests/e2e/transfer.e2e.test.ts new file mode 100644 index 000000000..5adf24d27 --- /dev/null +++ b/v10/examples/simple-transfer/tests/e2e/transfer.e2e.test.ts @@ -0,0 +1,60 @@ +/** + * E2E test for the simple transfer flow. + * + * Hits real devnet — only runs when APTOS_E2E=1 is set. + * Usage: APTOS_E2E=1 vitest run tests/e2e + */ + +import { describe, expect, it } from "vitest"; +import { Aptos, Network, generateAccount, U64 } from "@aptos-labs/ts-sdk"; + +const SKIP = !process.env.APTOS_E2E; + +describe.skipIf(SKIP)("simple transfer e2e", () => { + it("funds alice, transfers to bob, and verifies bob's balance", { timeout: 30_000 }, async () => { + const aptos = new Aptos({ network: Network.DEVNET }); + + // Generate fresh accounts + const alice = generateAccount(); + const bob = generateAccount(); + + // Fund alice with 1 APT + const fundTxn = await aptos.faucet.fund( + alice.accountAddress, + 100_000_000, + ); + expect(fundTxn.success).toBe(true); + + // Build a transfer of 0.01 APT from alice to bob + const transferAmount = 1_000_000; + const transaction = await aptos.transaction.buildSimple( + alice.accountAddress, + { + function: "0x1::aptos_account::transfer", + typeArguments: [], + functionArguments: [bob.accountAddress, new U64(transferAmount)], + }, + ); + + // Sign and submit + const pendingTxn = await aptos.transaction.signAndSubmit( + alice, + transaction, + ); + expect(pendingTxn.hash).toBeDefined(); + + // Wait for confirmation + const committedTxn = await aptos.transaction.waitForTransaction( + pendingTxn.hash, + ); + expect(committedTxn.success).toBe(true); + + // Verify bob received the funds + const [bobBalance] = await aptos.general.view({ + function: "0x1::coin::balance", + type_arguments: ["0x1::aptos_coin::AptosCoin"], + arguments: [bob.accountAddress.toString()], + }); + expect(Number(bobBalance)).toBe(transferAmount); + }); +}); diff --git a/v10/examples/simple-transfer/tests/unit/transfer.test.ts b/v10/examples/simple-transfer/tests/unit/transfer.test.ts new file mode 100644 index 000000000..cd579ee7b --- /dev/null +++ b/v10/examples/simple-transfer/tests/unit/transfer.test.ts @@ -0,0 +1,166 @@ +/** + * Unit tests for the simple transfer flow. + * + * Mocks `@aptos-labs/aptos-client` so no real network calls are made. + * Verifies: fund -> check balance -> build -> signAndSubmit -> wait -> check balance. + */ + +import { afterEach, describe, expect, it, vi } from "vitest"; + +// Mock the HTTP transport before importing anything from the SDK +vi.mock("@aptos-labs/aptos-client", () => ({ + jsonRequest: vi.fn(), + bcsRequest: vi.fn(), +})); + +import { jsonRequest } from "@aptos-labs/aptos-client"; +import { Aptos, Network, generateAccount, U64 } from "@aptos-labs/ts-sdk"; + +const mockJson = vi.mocked(jsonRequest); + +/** Helper: build a mock AptosClientResponse. */ +function mockResponse(data: unknown, status = 200) { + return { status, statusText: "OK", data, headers: {} }; +} + +/** Helper: build a committed user transaction response. */ +function committedTxnResponse(hash: string) { + return { + type: "user_transaction", + version: "1", + hash, + state_change_hash: "0x0", + event_root_hash: "0x0", + state_checkpoint_hash: null, + gas_used: "100", + success: true, + vm_status: "Executed successfully", + accumulator_root_hash: "0x0", + changes: [], + sender: "0x1", + sequence_number: "0", + max_gas_amount: "200000", + gas_unit_price: "100", + expiration_timestamp_secs: "99999999999", + payload: { type: "entry_function_payload", function: "0x1::aptos_account::transfer", type_arguments: [], arguments: [] }, + signature: { type: "ed25519_signature", public_key: "0x00", signature: "0x00" }, + events: [], + timestamp: "1000000", + }; +} + +describe("simple transfer (unit)", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("completes the full fund -> transfer -> verify flow", async () => { + const alice = generateAccount(); + const bob = generateAccount(); + + const aptos = new Aptos({ network: Network.LOCAL }); + + // ---- 1. Fund alice ---- + // faucet.fund makes a POST to faucet, then calls waitForTransaction + + // POST /fund (faucet) + mockJson.mockResolvedValueOnce( + mockResponse({ txn_hashes: ["0xfaucethash"] }), + ); + // GET /transactions/wait_by_hash/0xfaucethash (waitForTransaction long-poll) + mockJson.mockResolvedValueOnce( + mockResponse(committedTxnResponse("0xfaucethash")), + ); + + const fundResult = await aptos.faucet.fund(alice.accountAddress, 100_000_000); + expect(fundResult.hash).toBe("0xfaucethash"); + expect(fundResult.success).toBe(true); + + // ---- 2. Check alice's balance via view ---- + // POST /view + mockJson.mockResolvedValueOnce(mockResponse(["100000000"])); + + const [aliceBalance] = await aptos.general.view({ + function: "0x1::coin::balance", + type_arguments: ["0x1::aptos_coin::AptosCoin"], + arguments: [alice.accountAddress.toString()], + }); + expect(aliceBalance).toBe("100000000"); + + // ---- 3. Build transfer transaction ---- + // buildSimple fires 3 parallel requests: ledgerInfo, gasEstimation, account + + // GET / (ledgerInfo) + mockJson.mockResolvedValueOnce( + mockResponse({ + chain_id: 4, + epoch: "1", + ledger_version: "100", + oldest_ledger_version: "0", + ledger_timestamp: "0", + node_role: "full_node", + oldest_block_height: "0", + block_height: "50", + }), + ); + // GET /estimate_gas_price + mockJson.mockResolvedValueOnce( + mockResponse({ + gas_estimate: 100, + deprioritized_gas_estimate: 50, + prioritized_gas_estimate: 200, + }), + ); + // GET /accounts/ + mockJson.mockResolvedValueOnce( + mockResponse({ + sequence_number: "0", + authentication_key: alice.accountAddress.toString(), + }), + ); + + const transaction = await aptos.transaction.buildSimple( + alice.accountAddress, + { + function: "0x1::aptos_account::transfer", + typeArguments: [], + functionArguments: [bob.accountAddress, new U64(1_000_000)], + }, + ); + + expect(transaction.rawTransaction).toBeDefined(); + expect(transaction.rawTransaction.chain_id.chainId).toBe(4); + + // ---- 4. Sign and submit ---- + // POST /transactions (BCS content-type, but JSON accept => jsonRequest) + mockJson.mockResolvedValueOnce( + mockResponse({ hash: "0xtransferhash" }), + ); + + const pendingTxn = await aptos.transaction.signAndSubmit(alice, transaction); + expect(pendingTxn.hash).toBe("0xtransferhash"); + + // ---- 5. Wait for transaction ---- + // GET /transactions/wait_by_hash/0xtransferhash + mockJson.mockResolvedValueOnce( + mockResponse(committedTxnResponse("0xtransferhash")), + ); + + const committedTxn = await aptos.transaction.waitForTransaction(pendingTxn.hash); + expect(committedTxn.success).toBe(true); + + // ---- 6. Check bob's balance ---- + // POST /view + mockJson.mockResolvedValueOnce(mockResponse(["1000000"])); + + const [bobBalance] = await aptos.general.view({ + function: "0x1::coin::balance", + type_arguments: ["0x1::aptos_coin::AptosCoin"], + arguments: [bob.accountAddress.toString()], + }); + expect(bobBalance).toBe("1000000"); + + // Verify total number of mock calls: 2 (fund) + 1 (view) + 3 (build) + 1 (submit) + 1 (wait) + 1 (view) = 9 + expect(mockJson).toHaveBeenCalledTimes(9); + }); +}); diff --git a/v10/examples/simple-transfer/tsconfig.json b/v10/examples/simple-transfer/tsconfig.json new file mode 100644 index 000000000..e968634d0 --- /dev/null +++ b/v10/examples/simple-transfer/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/v10/examples/simple-transfer/vitest.config.ts b/v10/examples/simple-transfer/vitest.config.ts new file mode 100644 index 000000000..e2ec33294 --- /dev/null +++ b/v10/examples/simple-transfer/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + }, +}); diff --git a/v10/examples/sponsored-txn-server/package.json b/v10/examples/sponsored-txn-server/package.json new file mode 100644 index 000000000..1abe50fd7 --- /dev/null +++ b/v10/examples/sponsored-txn-server/package.json @@ -0,0 +1,22 @@ +{ + "name": "sponsored-txn-server-example", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "tsx src/main.ts", + "test": "vitest run", + "test:unit": "vitest run tests/unit", + "test:e2e": "APTOS_E2E=1 vitest run tests/e2e" + }, + "dependencies": { + "@aptos-labs/ts-sdk": "link:../..", + "hono": "^4.7.0", + "@hono/node-server": "^1.14.0" + }, + "devDependencies": { + "tsx": "^4.19.0", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + } +} diff --git a/v10/examples/sponsored-txn-server/pnpm-lock.yaml b/v10/examples/sponsored-txn-server/pnpm-lock.yaml new file mode 100644 index 000000000..c9497408a --- /dev/null +++ b/v10/examples/sponsored-txn-server/pnpm-lock.yaml @@ -0,0 +1,967 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@aptos-labs/ts-sdk': + specifier: link:../.. + version: link:../.. + '@hono/node-server': + specifier: ^1.14.0 + version: 1.19.11(hono@4.12.6) + hono: + specifier: ^4.7.0 + version: 4.12.6 + devDependencies: + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(tsx@4.21.0) + +packages: + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@vitest/expect@4.0.18': + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + + '@vitest/mocker@4.0.18': + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.18': + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + + '@vitest/runner@4.0.18': + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + + '@vitest/snapshot@4.0.18': + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + + '@vitest/spy@4.0.18': + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + + '@vitest/utils@4.0.18': + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + hono@4.12.6: + resolution: {integrity: sha512-KljEp+MeEEEIOT75qBo1UjqqB29fRMtlDEwCxcexOzdkUq6LR/vRvHk5pdROcxyOYyW1niq7Gb5pFVGy5R1eBw==} + engines: {node: '>=16.9.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.0.18: + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.18 + '@vitest/browser-preview': 4.0.18 + '@vitest/browser-webdriverio': 4.0.18 + '@vitest/ui': 4.0.18 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + +snapshots: + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@hono/node-server@1.19.11(hono@4.12.6)': + dependencies: + hono: 4.12.6 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@standard-schema/spec@1.1.0': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@vitest/expect@4.0.18': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + chai: 6.2.2 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.18(vite@7.3.1(tsx@4.21.0))': + dependencies: + '@vitest/spy': 4.0.18 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(tsx@4.21.0) + + '@vitest/pretty-format@4.0.18': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.18': + dependencies: + '@vitest/utils': 4.0.18 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.18': {} + + '@vitest/utils@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + tinyrainbow: 3.0.3 + + assertion-error@2.0.1: {} + + chai@6.2.2: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + hono@4.12.6: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + nanoid@3.3.11: {} + + obug@2.1.1: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + resolve-pkg-maps@1.0.0: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyrainbow@3.0.3: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + vite@7.3.1(tsx@4.21.0): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 + tsx: 4.21.0 + + vitest@4.0.18(tsx@4.21.0): + dependencies: + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(vite@7.3.1(tsx@4.21.0)) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.3.1(tsx@4.21.0) + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 diff --git a/v10/examples/sponsored-txn-server/src/main.ts b/v10/examples/sponsored-txn-server/src/main.ts new file mode 100644 index 000000000..9c07e06c8 --- /dev/null +++ b/v10/examples/sponsored-txn-server/src/main.ts @@ -0,0 +1,38 @@ +/** + * Sponsored Transaction Server — Entry Point + * + * Starts a Hono HTTP server that sponsors transactions using a funded account. + * On devnet/local, the sponsor is automatically funded via the faucet. + * + * Environment variables: + * APTOS_NETWORK - "devnet" | "local" | "testnet" | "mainnet" (default: "devnet") + * PORT - HTTP port (default: 3000) + */ + +import { serve } from "@hono/node-server"; +import { Aptos, Network, generateAccount } from "@aptos-labs/ts-sdk"; +import { createApp } from "./server.js"; + +// Read config from env +const network = (process.env.APTOS_NETWORK as Network) ?? Network.DEVNET; +const port = Number(process.env.PORT ?? 3000); + +async function main() { + const aptos = new Aptos({ network }); + const sponsor = generateAccount(); + console.log(`Sponsor: ${sponsor.accountAddress}`); + console.log(`Network: ${network}`); + + // Fund on devnet/local + if (network === Network.DEVNET || network === Network.LOCAL) { + console.log("Funding sponsor..."); + await aptos.faucet.fund(sponsor.accountAddress, 500_000_000); + console.log("Funded!"); + } + + const app = createApp({ aptos, sponsorAccount: sponsor }); + console.log(`Listening on http://localhost:${port}`); + serve({ fetch: app.fetch, port }); +} + +main().catch(console.error); diff --git a/v10/examples/sponsored-txn-server/src/server.ts b/v10/examples/sponsored-txn-server/src/server.ts new file mode 100644 index 000000000..972dd7db5 --- /dev/null +++ b/v10/examples/sponsored-txn-server/src/server.ts @@ -0,0 +1,105 @@ +/** + * Sponsored Transaction Server + * + * A Hono-based HTTP server that sponsors and submits Move entry-function + * transactions on behalf of callers. The server holds a funded sponsor + * account and pays the gas for every submitted transaction. + * + * Endpoints: + * GET /health — liveness check + chain info + * POST /sponsor — build, sign, submit, and wait for a transaction + */ + +import { Hono } from "hono"; +import { + AccountAddress, + U64, + type Aptos, + type Account, +} from "@aptos-labs/ts-sdk"; + +// ── Config ── + +export interface AppConfig { + aptos: Aptos; + sponsorAccount: Account; +} + +// ── App factory ── + +export function createApp(config: AppConfig) { + const { aptos, sponsorAccount } = config; + const app = new Hono(); + + // ── GET /health ── + + app.get("/health", async (c) => { + const info = await aptos.general.getLedgerInfo(); + return c.json({ + status: "ok", + sponsor: sponsorAccount.accountAddress.toString(), + chain_id: info.chain_id, + ledger_version: info.ledger_version, + }); + }); + + // ── POST /sponsor ── + + app.post("/sponsor", async (c) => { + const body = await c.req.json(); + + // Validate required field + if (!body.function || typeof body.function !== "string") { + return c.json({ error: "Missing or invalid 'function' field" }, 400); + } + + const fnName: string = body.function; + const rawArgs: (string | number)[] = body.functionArguments ?? []; + const typeArguments: string[] = body.typeArguments ?? []; + + // Convert arguments: "0x..." strings → AccountAddress, numbers → U64 + const functionArguments = rawArgs.map((arg) => { + if (typeof arg === "string" && arg.startsWith("0x")) { + return AccountAddress.from(arg); + } + if (typeof arg === "number" || (typeof arg === "string" && /^\d+$/.test(arg))) { + return new U64(BigInt(arg)); + } + return arg; + }); + + try { + // Build the transaction from the sponsor account + const txn = await aptos.transaction.buildSimple( + sponsorAccount.accountAddress, + { + function: fnName, + functionArguments, + typeArguments, + }, + ); + + // Sign and submit + const pending = await aptos.transaction.signAndSubmit( + sponsorAccount, + txn, + ); + + // Wait for on-chain confirmation + const committed = await aptos.transaction.waitForTransaction( + pending.hash, + ); + + return c.json({ + hash: committed.hash, + version: committed.version, + success: committed.success, + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return c.json({ error: message }, 500); + } + }); + + return app; +} diff --git a/v10/examples/sponsored-txn-server/tests/e2e/server.e2e.test.ts b/v10/examples/sponsored-txn-server/tests/e2e/server.e2e.test.ts new file mode 100644 index 000000000..5be83ab24 --- /dev/null +++ b/v10/examples/sponsored-txn-server/tests/e2e/server.e2e.test.ts @@ -0,0 +1,63 @@ +/** + * E2E tests for the sponsored-txn-server. + * + * Hits real devnet -- only runs when APTOS_E2E=1 is set. + * Usage: APTOS_E2E=1 vitest run tests/e2e + */ + +import { describe, expect, it } from "vitest"; +import { Aptos, Network, generateAccount } from "@aptos-labs/ts-sdk"; +import { createApp } from "../../src/server.js"; + +const SKIP = !process.env.APTOS_E2E; + +describe.skipIf(SKIP)("sponsored-txn-server e2e", () => { + it("GET /health returns live chain info", { timeout: 30_000 }, async () => { + const aptos = new Aptos({ network: Network.DEVNET }); + const sponsor = generateAccount(); + + // Fund the sponsor so it exists on-chain + await aptos.faucet.fund(sponsor.accountAddress, 500_000_000); + + const app = createApp({ aptos, sponsorAccount: sponsor }); + + const res = await app.request("/health"); + expect(res.status).toBe(200); + + const body = await res.json(); + expect(body.status).toBe("ok"); + expect(body.sponsor).toBe(sponsor.accountAddress.toString()); + expect(typeof body.chain_id).toBe("number"); + expect(body.ledger_version).toBeDefined(); + }); + + it("POST /sponsor submits a real transfer", { timeout: 30_000 }, async () => { + const aptos = new Aptos({ network: Network.DEVNET }); + const sponsor = generateAccount(); + const recipient = generateAccount(); + + // Fund the sponsor + await aptos.faucet.fund(sponsor.accountAddress, 500_000_000); + + const app = createApp({ aptos, sponsorAccount: sponsor }); + + const res = await app.request("/sponsor", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + function: "0x1::aptos_account::transfer", + functionArguments: [ + recipient.accountAddress.toString(), + 1_000_000, + ], + }), + }); + + expect(res.status).toBe(200); + + const body = await res.json(); + expect(body.hash).toBeDefined(); + expect(body.version).toBeDefined(); + expect(body.success).toBe(true); + }); +}); diff --git a/v10/examples/sponsored-txn-server/tests/unit/server.test.ts b/v10/examples/sponsored-txn-server/tests/unit/server.test.ts new file mode 100644 index 000000000..654c41a68 --- /dev/null +++ b/v10/examples/sponsored-txn-server/tests/unit/server.test.ts @@ -0,0 +1,185 @@ +/** + * Unit tests for the sponsored-txn-server Hono app. + * + * Mocks `@aptos-labs/aptos-client` so no real network calls are made. + * Tests /health, /sponsor, and validation. + */ + +import { afterEach, describe, expect, it, vi } from "vitest"; + +// Mock the HTTP transport before importing anything from the SDK +vi.mock("@aptos-labs/aptos-client", () => ({ + jsonRequest: vi.fn(), + bcsRequest: vi.fn(), +})); + +import { jsonRequest } from "@aptos-labs/aptos-client"; +import { Aptos, Network, generateAccount } from "@aptos-labs/ts-sdk"; +import { createApp } from "../../src/server.js"; + +const mockJson = vi.mocked(jsonRequest); + +/** Helper: build a mock AptosClientResponse. */ +function mockResponse(data: unknown, status = 200) { + return { status, statusText: "OK", data, headers: {} }; +} + +/** Helper: build a committed user transaction response. */ +function committedTxnResponse(hash: string) { + return { + type: "user_transaction", + version: "42", + hash, + state_change_hash: "0x0", + event_root_hash: "0x0", + state_checkpoint_hash: null, + gas_used: "100", + success: true, + vm_status: "Executed successfully", + accumulator_root_hash: "0x0", + changes: [], + sender: "0x1", + sequence_number: "0", + max_gas_amount: "200000", + gas_unit_price: "100", + expiration_timestamp_secs: "99999999999", + payload: { + type: "entry_function_payload", + function: "0x1::aptos_account::transfer", + type_arguments: [], + arguments: [], + }, + signature: { + type: "ed25519_signature", + public_key: "0x00", + signature: "0x00", + }, + events: [], + timestamp: "1000000", + }; +} + +describe("sponsored-txn-server (unit)", () => { + const aptos = new Aptos({ network: Network.LOCAL }); + const sponsor = generateAccount(); + const app = createApp({ aptos, sponsorAccount: sponsor }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + // ── GET /health ── + + it("GET /health returns status, sponsor address, and chain info", async () => { + // getLedgerInfo → 1 jsonRequest call + mockJson.mockResolvedValueOnce( + mockResponse({ + chain_id: 4, + epoch: "1", + ledger_version: "200", + oldest_ledger_version: "0", + ledger_timestamp: "0", + node_role: "full_node", + oldest_block_height: "0", + block_height: "100", + }), + ); + + const res = await app.request("/health"); + expect(res.status).toBe(200); + + const body = await res.json(); + expect(body.status).toBe("ok"); + expect(body.sponsor).toBe(sponsor.accountAddress.toString()); + expect(body.chain_id).toBe(4); + expect(body.ledger_version).toBe("200"); + + expect(mockJson).toHaveBeenCalledTimes(1); + }); + + // ── POST /sponsor ── + + it("POST /sponsor builds, submits, and waits for a transaction", async () => { + // buildSimple fires 3 parallel requests: ledgerInfo, gasEstimation, account + // 1. GET / (ledgerInfo) + mockJson.mockResolvedValueOnce( + mockResponse({ + chain_id: 4, + epoch: "1", + ledger_version: "100", + oldest_ledger_version: "0", + ledger_timestamp: "0", + node_role: "full_node", + oldest_block_height: "0", + block_height: "50", + }), + ); + // 2. GET /estimate_gas_price + mockJson.mockResolvedValueOnce( + mockResponse({ + gas_estimate: 100, + deprioritized_gas_estimate: 50, + prioritized_gas_estimate: 200, + }), + ); + // 3. GET /accounts/ + mockJson.mockResolvedValueOnce( + mockResponse({ + sequence_number: "0", + authentication_key: sponsor.accountAddress.toString(), + }), + ); + + // 4. POST /transactions (signAndSubmit — goes through jsonRequest) + mockJson.mockResolvedValueOnce( + mockResponse({ hash: "0xsponsorhash" }), + ); + + // 5. GET /transactions/wait_by_hash/0xsponsorhash (waitForTransaction) + mockJson.mockResolvedValueOnce( + mockResponse(committedTxnResponse("0xsponsorhash")), + ); + + const res = await app.request("/sponsor", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + function: "0x1::aptos_account::transfer", + functionArguments: [ + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + 1000000, + ], + }), + }); + + expect(res.status).toBe(200); + + const body = await res.json(); + expect(body.hash).toBe("0xsponsorhash"); + expect(body.version).toBe("42"); + expect(body.success).toBe(true); + + // 3 (build) + 1 (submit) + 1 (wait) = 5 + expect(mockJson).toHaveBeenCalledTimes(5); + }); + + // ── POST /sponsor — missing function field ── + + it("POST /sponsor returns 400 when function field is missing", async () => { + const res = await app.request("/sponsor", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + functionArguments: ["0x1", 100], + }), + }); + + expect(res.status).toBe(400); + + const body = await res.json(); + expect(body.error).toMatch(/function/i); + + // No SDK calls should have been made + expect(mockJson).not.toHaveBeenCalled(); + }); +}); diff --git a/v10/examples/sponsored-txn-server/tsconfig.json b/v10/examples/sponsored-txn-server/tsconfig.json new file mode 100644 index 000000000..e968634d0 --- /dev/null +++ b/v10/examples/sponsored-txn-server/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/v10/examples/sponsored-txn-server/vitest.config.ts b/v10/examples/sponsored-txn-server/vitest.config.ts new file mode 100644 index 000000000..e2ec33294 --- /dev/null +++ b/v10/examples/sponsored-txn-server/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + }, +}); diff --git a/v10/examples/web-test/index.html b/v10/examples/web-test/index.html new file mode 100644 index 000000000..0a31fbd98 --- /dev/null +++ b/v10/examples/web-test/index.html @@ -0,0 +1,12 @@ + + + + + v10 SDK Browser Test + + +

Aptos SDK v10 Browser Test

+

Loading SDK...

+ + + diff --git a/v10/examples/web-test/package.json b/v10/examples/web-test/package.json new file mode 100644 index 000000000..6ac4e999c --- /dev/null +++ b/v10/examples/web-test/package.json @@ -0,0 +1,18 @@ +{ + "name": "v10-web-sdk-test", + "version": "1.0.0", + "description": "Test Aptos TypeScript SDK v10 in browsers using Playwright", + "type": "module", + "license": "Apache-2.0", + "scripts": { + "test": "playwright test", + "test:headed": "playwright test --headed" + }, + "dependencies": { + "@aptos-labs/ts-sdk": "file:../.." + }, + "devDependencies": { + "@playwright/test": "^1.58.2", + "vite": "^7.3.1" + } +} diff --git a/v10/examples/web-test/playwright.config.ts b/v10/examples/web-test/playwright.config.ts new file mode 100644 index 000000000..0634ba716 --- /dev/null +++ b/v10/examples/web-test/playwright.config.ts @@ -0,0 +1,27 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: "html", + timeout: 120_000, + use: { + baseURL: "http://localhost:5174", + trace: "on-first-retry", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + webServer: { + command: "pnpm exec vite", + url: "http://localhost:5174", + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, +}); diff --git a/v10/examples/web-test/src/main.ts b/v10/examples/web-test/src/main.ts new file mode 100644 index 000000000..38002c530 --- /dev/null +++ b/v10/examples/web-test/src/main.ts @@ -0,0 +1,118 @@ +/** + * v10 browser test — exposes SDK functionality to window for Playwright tests. + * Uses v10 native API. + */ + +import { + Aptos, + Network, + generateAccount, + accountFromPrivateKey, + AccountAddress, + Hex, + Ed25519PrivateKey, + U64, +} from "@aptos-labs/ts-sdk"; +import type { Ed25519Account } from "@aptos-labs/ts-sdk"; + +// Get network from URL params +const urlParams = new URLSearchParams(window.location.search); +const networkParam = urlParams.get("network") || "devnet"; +const APTOS_NETWORK = (networkParam as Network) || Network.DEVNET; + +const aptos = new Aptos({ network: APTOS_NETWORK }); + +declare global { + interface Window { + aptosSDK: { + generateAccount: () => { address: string; publicKey: string; privateKey: string }; + createAccountFromPrivateKey: (privateKeyHex: string) => string; + parseAddress: (address: string) => { short: string; long: string }; + parseHex: (hex: string) => string; + getLedgerInfo: () => Promise<{ chainId: number; epoch: string; ledgerVersion: string }>; + getChainId: () => Promise; + fundAccount: (address: string, amount: number) => Promise; + getBalance: (address: string) => Promise; + transfer: (fromPrivateKey: string, toAddress: string, amount: number) => Promise<{ hash: string; success: boolean }>; + }; + } +} + +window.aptosSDK = { + generateAccount: () => { + const account = generateAccount() as Ed25519Account; + return { + address: account.accountAddress.toString(), + publicKey: account.publicKey.toString(), + privateKey: account.privateKey.toString(), + }; + }, + + createAccountFromPrivateKey: (privateKeyHex: string) => { + const privateKey = new Ed25519PrivateKey(privateKeyHex); + const account = accountFromPrivateKey({ privateKey }); + return account.accountAddress.toString(); + }, + + parseAddress: (address: string) => { + const addr = AccountAddress.from(address); + return { + short: addr.toString(), + long: addr.toStringLong(), + }; + }, + + parseHex: (hex: string) => { + const h = Hex.fromHexInput(hex); + return h.toString(); + }, + + getLedgerInfo: async () => { + const info = await aptos.general.getLedgerInfo(); + return { + chainId: info.chain_id, + epoch: info.epoch, + ledgerVersion: info.ledger_version, + }; + }, + + getChainId: async () => { + return await aptos.general.getChainId(); + }, + + fundAccount: async (address: string, amount: number) => { + const txn = await aptos.faucet.fund(AccountAddress.from(address), amount); + return txn.hash; + }, + + getBalance: async (address: string) => { + const [balance] = await aptos.general.view<[string]>({ + function: "0x1::coin::balance", + type_arguments: ["0x1::aptos_coin::AptosCoin"], + arguments: [address], + }); + return Number(balance); + }, + + transfer: async (fromPrivateKey: string, toAddress: string, amount: number) => { + const privateKey = new Ed25519PrivateKey(fromPrivateKey); + const sender = accountFromPrivateKey({ privateKey }) as Ed25519Account; + + const tx = await aptos.transaction.buildSimple(sender.accountAddress, { + function: "0x1::aptos_account::transfer", + typeArguments: [], + functionArguments: [AccountAddress.from(toAddress), new U64(amount)], + }); + + const pending = await aptos.transaction.signAndSubmit(sender, tx); + const result = await aptos.transaction.waitForTransaction(pending.hash, { checkSuccess: true }); + + return { + hash: pending.hash, + success: "success" in result && result.success === true, + }; + }, +}; + +document.getElementById("results")!.textContent = "SDK loaded successfully!"; +console.log("Aptos SDK v10 loaded and exposed to window.aptosSDK"); diff --git a/v10/examples/web-test/tests/sdk.spec.ts b/v10/examples/web-test/tests/sdk.spec.ts new file mode 100644 index 000000000..be64e65ca --- /dev/null +++ b/v10/examples/web-test/tests/sdk.spec.ts @@ -0,0 +1,97 @@ +import { test, expect } from "@playwright/test"; + +const network = process.env.APTOS_NETWORK || "local"; + +test.describe("v10 SDK Browser Tests", () => { + test.beforeEach(async ({ page }) => { + await page.goto(`/?network=${network}`); + await page.waitForFunction(() => window.aptosSDK !== undefined, { timeout: 30000 }); + }); + + test.describe("Account Operations", () => { + test("should generate a new account", async ({ page }) => { + const account = await page.evaluate(() => window.aptosSDK.generateAccount()); + expect(account.address).toMatch(/^0x[a-fA-F0-9]+$/); + expect(account.publicKey).toBeDefined(); + expect(account.privateKey).toBeDefined(); + }); + + test("should create account from private key", async ({ page }) => { + const result = await page.evaluate(() => { + const account = window.aptosSDK.generateAccount(); + const recreated = window.aptosSDK.createAccountFromPrivateKey(account.privateKey); + return { original: account.address, recreated }; + }); + expect(result.recreated).toBe(result.original); + }); + }); + + test.describe("Hex and Address Operations", () => { + test("should parse hex values", async ({ page }) => { + const result = await page.evaluate(() => window.aptosSDK.parseHex("0x1234567890abcdef")); + expect(result).toBe("0x1234567890abcdef"); + }); + + test("should parse account addresses", async ({ page }) => { + const result = await page.evaluate(() => window.aptosSDK.parseAddress("0x1")); + expect(result.short).toBe("0x1"); + expect(result.long).toBe("0x0000000000000000000000000000000000000000000000000000000000000001"); + }); + }); + + test.describe("Network Connectivity", () => { + test("should fetch ledger info", async ({ page }) => { + const info = await page.evaluate(async () => window.aptosSDK.getLedgerInfo()); + expect(info.chainId).toBeDefined(); + expect(typeof info.chainId).toBe("number"); + expect(info.ledgerVersion).toBeDefined(); + }); + + test("should get chain ID", async ({ page }) => { + const chainId = await page.evaluate(async () => window.aptosSDK.getChainId()); + expect(typeof chainId).toBe("number"); + expect(chainId).toBeGreaterThan(0); + }); + }); + + test.describe("Account Funding and Balance", () => { + test("should fund an account and check balance", async ({ page }) => { + const fundAmount = 100_000_000; + const result = await page.evaluate(async (amount) => { + const account = window.aptosSDK.generateAccount(); + const txnHash = await window.aptosSDK.fundAccount(account.address, amount); + const balance = await window.aptosSDK.getBalance(account.address); + return { txnHash, balance }; + }, fundAmount); + + expect(result.txnHash).toBeDefined(); + expect(result.balance).toBe(fundAmount); + }); + }); + + test.describe("Full Transfer Flow", () => { + test("should complete a full transfer between accounts", async ({ page }) => { + const result = await page.evaluate(async () => { + const alice = window.aptosSDK.generateAccount(); + const bob = window.aptosSDK.generateAccount(); + const transferAmount = 1000; + + await window.aptosSDK.fundAccount(alice.address, 100_000_000); + await window.aptosSDK.fundAccount(bob.address, 100); + + const txnResult = await window.aptosSDK.transfer(alice.privateKey, bob.address, transferAmount); + const bobBalance = await window.aptosSDK.getBalance(bob.address); + + return { + hash: txnResult.hash, + success: txnResult.success, + bobBalance, + expectedBalance: 100 + transferAmount, + }; + }); + + expect(result.success).toBe(true); + expect(result.bobBalance).toBe(result.expectedBalance); + }); + }); +}); diff --git a/v10/examples/web-test/tsconfig.json b/v10/examples/web-test/tsconfig.json new file mode 100644 index 000000000..51c1a716c --- /dev/null +++ b/v10/examples/web-test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/v10/examples/web-test/vite.config.ts b/v10/examples/web-test/vite.config.ts new file mode 100644 index 000000000..db43d4519 --- /dev/null +++ b/v10/examples/web-test/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + server: { + port: 5174, + }, + build: { + target: "esnext", + }, + optimizeDeps: { + include: ["@aptos-labs/ts-sdk"], + }, +}); diff --git a/v10/package.json b/v10/package.json new file mode 100644 index 000000000..e48d2b63f --- /dev/null +++ b/v10/package.json @@ -0,0 +1,96 @@ +{ + "name": "@aptos-labs/ts-sdk", + "version": "10.0.0", + "description": "Aptos TypeScript SDK v10 — ESM-only, tree-shakeable, function-first", + "type": "module", + "sideEffects": false, + "engines": { + "node": ">=22.1.2" + }, + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "./bcs": { + "types": "./dist/types/bcs/index.d.ts", + "default": "./dist/esm/bcs/index.js" + }, + "./hex": { + "types": "./dist/types/hex/index.d.ts", + "default": "./dist/esm/hex/index.js" + }, + "./crypto": { + "types": "./dist/types/crypto/index.d.ts", + "default": "./dist/esm/crypto/index.js" + }, + "./core": { + "types": "./dist/types/core/index.d.ts", + "default": "./dist/esm/core/index.js" + }, + "./transactions": { + "types": "./dist/types/transactions/index.d.ts", + "default": "./dist/esm/transactions/index.js" + }, + "./account": { + "types": "./dist/types/account/index.d.ts", + "default": "./dist/esm/account/index.js" + }, + "./client": { + "types": "./dist/types/client/index.d.ts", + "default": "./dist/esm/client/index.js" + }, + "./api": { + "types": "./dist/types/api/index.d.ts", + "default": "./dist/esm/api/index.js" + }, + "./compat": { + "types": "./dist/types/compat/index.d.ts", + "require": "./dist/esm/compat/index.cjs", + "default": "./dist/esm/compat/index.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "rm -rf dist && tsc -p tsconfig.build.json && cp src/compat/index.cjs dist/esm/compat/index.cjs", + "fmt": "biome format --write src tests", + "check": "biome check src tests", + "lint": "biome lint src tests", + "test": "vitest run", + "test:unit": "vitest run tests/unit", + "test:e2e": "vitest run tests/e2e", + "test:compat": "vitest run tests/e2e/compat" + }, + "dependencies": { + "@aptos-labs/aptos-client": "^2.1.0", + "@noble/curves": "^2.0.1", + "@noble/hashes": "^2.0.1", + "@scure/bip32": "^2.0.1", + "@scure/bip39": "^2.0.1", + "poseidon-lite": "^0.3.0" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.6", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + }, + "peerDependencies": { + "@aptos-labs/aptos-cli": "^1.1.1" + }, + "peerDependenciesMeta": { + "@aptos-labs/aptos-cli": { + "optional": true + } + }, + "pnpm": { + "ignoredBuiltDependencies": [ + "esbuild" + ] + } +} diff --git a/v10/pnpm-lock.yaml b/v10/pnpm-lock.yaml new file mode 100644 index 000000000..28c79b277 --- /dev/null +++ b/v10/pnpm-lock.yaml @@ -0,0 +1,1321 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@aptos-labs/aptos-cli': + specifier: ^1.1.1 + version: 1.1.1 + '@aptos-labs/aptos-client': + specifier: ^2.1.0 + version: 2.2.0(got@11.8.6) + '@noble/curves': + specifier: ^2.0.1 + version: 2.0.1 + '@noble/hashes': + specifier: ^2.0.1 + version: 2.0.1 + '@scure/bip32': + specifier: ^2.0.1 + version: 2.0.1 + '@scure/bip39': + specifier: ^2.0.1 + version: 2.0.1 + poseidon-lite: + specifier: ^0.3.0 + version: 0.3.0 + devDependencies: + '@biomejs/biome': + specifier: ^2.4.6 + version: 2.4.6 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@types/node@25.3.5) + +packages: + + '@aptos-labs/aptos-cli@1.1.1': + resolution: {integrity: sha512-sB7CokCM6s76SLJmccysbnFR+MDik6udKfj2+9ZsmTLV0/t73veIeCDKbvWJmbW267ibx4HiGbPI7L+1+yjEbQ==} + hasBin: true + + '@aptos-labs/aptos-client@2.2.0': + resolution: {integrity: sha512-lYgHI8ehgD+Ykhix0IwzLaTCknHp1KNmExbq2bPZk8IeTwQg79D5BOkD46MjW0jGbJbl+J/RBtVF9vM7Te/hWA==} + engines: {node: '>=20.0.0'} + peerDependencies: + got: ^11.8.6 + + '@biomejs/biome@2.4.6': + resolution: {integrity: sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.6': + resolution: {integrity: sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.6': + resolution: {integrity: sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.6': + resolution: {integrity: sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.4.6': + resolution: {integrity: sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.4.6': + resolution: {integrity: sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.4.6': + resolution: {integrity: sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.4.6': + resolution: {integrity: sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.6': + resolution: {integrity: sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@noble/curves@2.0.1': + resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@scure/base@2.0.0': + resolution: {integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==} + + '@scure/bip32@2.0.1': + resolution: {integrity: sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==} + + '@scure/bip39@2.0.1': + resolution: {integrity: sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/node@25.3.5': + resolution: {integrity: sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==} + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + + '@vitest/expect@4.0.18': + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + + '@vitest/mocker@4.0.18': + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.18': + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + + '@vitest/runner@4.0.18': + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + + '@vitest/snapshot@4.0.18': + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + + '@vitest/spy@4.0.18': + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + + '@vitest/utils@4.0.18': + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + poseidon-lite@0.3.0: + resolution: {integrity: sha512-ilJj4MIve4uBEG7SrtPqUUNkvpJ/pLVbndxa0WvebcQqeIhe+h72JR4g0EvwchUzm9sOQDlOjiDNmRAgxNZl4A==} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.0.18: + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.18 + '@vitest/browser-preview': 4.0.18 + '@vitest/browser-webdriverio': 4.0.18 + '@vitest/ui': 4.0.18 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + +snapshots: + + '@aptos-labs/aptos-cli@1.1.1': + dependencies: + commander: 12.1.0 + + '@aptos-labs/aptos-client@2.2.0(got@11.8.6)': + dependencies: + got: 11.8.6 + + '@biomejs/biome@2.4.6': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.6 + '@biomejs/cli-darwin-x64': 2.4.6 + '@biomejs/cli-linux-arm64': 2.4.6 + '@biomejs/cli-linux-arm64-musl': 2.4.6 + '@biomejs/cli-linux-x64': 2.4.6 + '@biomejs/cli-linux-x64-musl': 2.4.6 + '@biomejs/cli-win32-arm64': 2.4.6 + '@biomejs/cli-win32-x64': 2.4.6 + + '@biomejs/cli-darwin-arm64@2.4.6': + optional: true + + '@biomejs/cli-darwin-x64@2.4.6': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.6': + optional: true + + '@biomejs/cli-linux-arm64@2.4.6': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.6': + optional: true + + '@biomejs/cli-linux-x64@2.4.6': + optional: true + + '@biomejs/cli-win32-arm64@2.4.6': + optional: true + + '@biomejs/cli-win32-x64@2.4.6': + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@noble/curves@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + + '@noble/hashes@2.0.1': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@scure/base@2.0.0': {} + + '@scure/bip32@2.0.1': + dependencies: + '@noble/curves': 2.0.1 + '@noble/hashes': 2.0.1 + '@scure/base': 2.0.0 + + '@scure/bip39@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + '@scure/base': 2.0.0 + + '@sindresorhus/is@4.6.0': {} + + '@standard-schema/spec@1.1.0': {} + + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 25.3.5 + '@types/responselike': 1.0.3 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/http-cache-semantics@4.2.0': {} + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 25.3.5 + + '@types/node@25.3.5': + dependencies: + undici-types: 7.18.2 + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 25.3.5 + + '@vitest/expect@4.0.18': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + chai: 6.2.2 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.5))': + dependencies: + '@vitest/spy': 4.0.18 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@25.3.5) + + '@vitest/pretty-format@4.0.18': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.18': + dependencies: + '@vitest/utils': 4.0.18 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.18': {} + + '@vitest/utils@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + tinyrainbow: 3.0.3 + + assertion-error@2.0.1: {} + + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + + chai@6.2.2: {} + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + commander@12.1.0: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + defer-to-connect@2.0.1: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + es-module-lexer@1.7.0: {} + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fsevents@2.3.3: + optional: true + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + + http-cache-semantics@4.2.0: {} + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + json-buffer@3.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + lowercase-keys@2.0.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + + nanoid@3.3.11: {} + + normalize-url@6.1.0: {} + + obug@2.1.1: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + p-cancelable@2.1.1: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + poseidon-lite@0.3.0: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + quick-lru@5.1.1: {} + + resolve-alpn@1.2.1: {} + + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyrainbow@3.0.3: {} + + typescript@5.9.3: {} + + undici-types@7.18.2: {} + + vite@7.3.1(@types/node@25.3.5): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.3.5 + fsevents: 2.3.3 + + vitest@4.0.18(@types/node@25.3.5): + dependencies: + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.5)) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.3.1(@types/node@25.3.5) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.3.5 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrappy@1.0.2: {} diff --git a/v10/src/account/abstract-keyless-account.ts b/v10/src/account/abstract-keyless-account.ts new file mode 100644 index 000000000..0088c826b --- /dev/null +++ b/v10/src/account/abstract-keyless-account.ts @@ -0,0 +1,634 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; +import { AccountAddress } from "../core/account-address.js"; +import { KeylessError, KeylessErrorType } from "../core/errors.js"; +import type { FederatedKeylessPublicKey } from "../crypto/federated-keyless.js"; +import { + EphemeralCertificate, + type KeylessPublicKey, + KeylessSignature, + MAX_AUD_VAL_BYTES, + MAX_ISS_VAL_BYTES, + MAX_UID_VAL_BYTES, + ZeroKnowledgeSig, + type ZkProof, +} from "../crypto/keyless.js"; +import { AnyPublicKey, AnySignature } from "../crypto/single-key.js"; +import { EphemeralCertificateVariant, SigningScheme } from "../crypto/types.js"; +import type { HexInput } from "../hex/index.js"; +import { Hex } from "../hex/index.js"; +import { AccountAuthenticatorSingleKey } from "../transactions/authenticator.js"; +import { deriveTransactionType, generateSigningMessage } from "../transactions/signing-message.js"; +import type { AnyRawTransaction, AnyRawTransactionInstance } from "../transactions/types.js"; +import { EphemeralKeyPair } from "./ephemeral-key-pair.js"; +import type { Account, SingleKeySigner } from "./types.js"; + +// ── JWT Helpers (replaces jwt-decode dependency) ── + +const MAX_JWT_SEGMENT_BYTES = 8192; +const TEXT_ENCODER = new TextEncoder(); + +function base64UrlDecode(input: string): string { + const base64 = input.replace(/-/g, "+").replace(/_/g, "/"); + const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "="); + return atob(padded); +} + +function decodeJwtPayload(jwt: string): Record { + const parts = jwt.split("."); + if (parts.length !== 3) { + throw new Error("Invalid JWT format"); + } + const decoded = base64UrlDecode(parts[1]); + if (decoded.length > MAX_JWT_SEGMENT_BYTES) { + throw new Error(`JWT payload exceeds maximum size of ${MAX_JWT_SEGMENT_BYTES} bytes`); + } + return JSON.parse(decoded); +} + +function decodeJwtHeader(jwt: string): Record { + const parts = jwt.split("."); + if (parts.length !== 3) { + throw new Error("Invalid JWT format"); + } + const decoded = base64UrlDecode(parts[0]); + if (decoded.length > MAX_JWT_SEGMENT_BYTES) { + throw new Error(`JWT header exceeds maximum size of ${MAX_JWT_SEGMENT_BYTES} bytes`); + } + return JSON.parse(decoded); +} + +/** + * Extracts the `iss`, `aud`, and the uid value identified by `uidKey` from a + * raw JWT string without requiring a network request. + * + * @param args.jwt - The raw JSON Web Token string. + * @param args.uidKey - The JWT payload claim to use as the user identifier. + * Defaults to `"sub"`. + * @returns An object with `iss`, `aud`, and `uidVal` extracted from the JWT payload. + * + * @throws {@link KeylessError} with type `JWT_PARSING_ERROR` if the JWT is + * malformed or missing required claims. + */ +export function getIssAudAndUidVal(args: { jwt: string; uidKey?: string }): { + iss: string; + aud: string; + uidVal: string; +} { + const { jwt, uidKey = "sub" } = args; + let jwtPayload: Record; + try { + jwtPayload = decodeJwtPayload(jwt); + } catch { + throw KeylessError.fromErrorType({ + type: KeylessErrorType.JWT_PARSING_ERROR, + details: "Invalid JWT format", + }); + } + if (typeof jwtPayload.iss !== "string") { + throw KeylessError.fromErrorType({ + type: KeylessErrorType.JWT_PARSING_ERROR, + details: "Invalid JWT: missing required claim", + }); + } + if (typeof jwtPayload.aud !== "string") { + const details = Array.isArray(jwtPayload.aud) + ? "Invalid JWT: 'aud' claim is an array; only a single string audience is supported" + : "Invalid JWT: missing or malformed 'aud' claim"; + throw KeylessError.fromErrorType({ + type: KeylessErrorType.JWT_PARSING_ERROR, + details, + }); + } + const uidVal = jwtPayload[uidKey]; + if (typeof uidVal !== "string") { + throw KeylessError.fromErrorType({ + type: KeylessErrorType.JWT_PARSING_ERROR, + details: `Invalid JWT: claim '${uidKey}' is missing or not a string`, + }); + } + if (TEXT_ENCODER.encode(jwtPayload.iss).length > MAX_ISS_VAL_BYTES) { + throw KeylessError.fromErrorType({ + type: KeylessErrorType.JWT_PARSING_ERROR, + details: `Invalid JWT: 'iss' exceeds maximum length of ${MAX_ISS_VAL_BYTES} bytes`, + }); + } + if (TEXT_ENCODER.encode(jwtPayload.aud).length > MAX_AUD_VAL_BYTES) { + throw KeylessError.fromErrorType({ + type: KeylessErrorType.JWT_PARSING_ERROR, + details: `Invalid JWT: 'aud' exceeds maximum length of ${MAX_AUD_VAL_BYTES} bytes`, + }); + } + if (TEXT_ENCODER.encode(uidVal).length > MAX_UID_VAL_BYTES) { + throw KeylessError.fromErrorType({ + type: KeylessErrorType.JWT_PARSING_ERROR, + details: `Invalid JWT: '${uidKey}' exceeds maximum length of ${MAX_UID_VAL_BYTES} bytes`, + }); + } + return { iss: jwtPayload.iss, aud: jwtPayload.aud, uidVal }; +} + +// ── Proof fetch types ── + +/** Indicates that a background proof fetch completed successfully. */ +export type ProofFetchSuccess = { status: "Success" }; + +/** Indicates that a background proof fetch failed, along with an error description. */ +export type ProofFetchFailure = { status: "Failed"; error: string }; + +/** Union of the two possible outcomes of an asynchronous proof fetch. */ +export type ProofFetchStatus = ProofFetchSuccess | ProofFetchFailure; + +/** + * Callback invoked once a background zero-knowledge proof fetch resolves. + * + * @param status - The final {@link ProofFetchStatus} of the fetch attempt. + * @returns A promise that the caller may await. + */ +export type ProofFetchCallback = (status: ProofFetchStatus) => Promise; + +// ── AbstractKeylessAccount ── + +/** + * Abstract base class shared by {@link KeylessAccount} and + * {@link FederatedKeylessAccount}. + * + * Manages the ephemeral key pair, zero-knowledge proof, JWT, and pepper that + * are common to all keyless signing flows. Subclasses supply the concrete + * public key type and serialization details. + * + * Proof can be provided either eagerly (as a resolved {@link ZeroKnowledgeSig}) + * or lazily (as a `Promise` with a `proofFetchCallback`). + */ +export abstract class AbstractKeylessAccount extends Serializable implements Account, SingleKeySigner { + /** Length in bytes of the pepper value used to blind the user identifier. */ + static readonly PEPPER_LENGTH: number = 31; + + /** The keyless public key (either standard or federated). */ + readonly publicKey: KeylessPublicKey | FederatedKeylessPublicKey; + /** The short-lived ephemeral key pair used to produce the inner signature. */ + readonly ephemeralKeyPair: EphemeralKeyPair; + /** The JWT payload claim used as the user identifier (e.g. `"sub"`). */ + readonly uidKey: string; + /** The value of the {@link uidKey} claim from the JWT payload. */ + readonly uidVal: string; + /** The `aud` (audience) claim from the JWT payload. */ + readonly aud: string; + /** The 31-byte pepper that blinds the user identifier in the public key. */ + readonly pepper: Uint8Array; + /** The on-chain address of this account. */ + readonly accountAddress: AccountAddress; + /** + * The resolved zero-knowledge proof, or `undefined` while an async fetch is + * still in progress. + */ + proof: ZeroKnowledgeSig | undefined; + /** + * Either the resolved proof or the promise that will resolve to it. + * Use {@link waitForProofFetch} to await a pending promise. + */ + readonly proofOrPromise: ZeroKnowledgeSig | Promise; + /** Always `SigningScheme.SingleKey` for keyless accounts. */ + readonly signingScheme: SigningScheme = SigningScheme.SingleKey; + /** + * The raw JWT string used to derive the public key. + * + * **Security note:** The JWT payload may contain personally identifiable + * information (email, name, etc.) and is serialized to BCS when persisting + * the account. Call {@link clearSensitiveData} when the account is no longer + * needed to minimize exposure. Note that JavaScript strings are immutable + * and cannot be zeroed in memory — the runtime may retain copies until + * garbage collection. + */ + readonly jwt: string; + /** + * Optional 32-byte hash of the Groth16 verification key that was used to + * generate the proof. When present, it is included in signatures to allow + * on-chain verification key rotation. + */ + readonly verificationKeyHash?: Uint8Array; + + /** Whether sensitive data (pepper, etc.) has been cleared from memory. */ + private sensitiveDataCleared = false; + + // Use native EventTarget instead of eventemitter3 + private readonly eventTarget: EventTarget; + + protected constructor(args: { + address?: AccountAddress; + publicKey: KeylessPublicKey | FederatedKeylessPublicKey; + ephemeralKeyPair: EphemeralKeyPair; + iss: string; + uidKey: string; + uidVal: string; + aud: string; + pepper: HexInput; + proof: ZeroKnowledgeSig | Promise; + proofFetchCallback?: ProofFetchCallback; + jwt: string; + verificationKeyHash?: HexInput; + }) { + super(); + const { + address, + ephemeralKeyPair, + publicKey, + uidKey, + uidVal, + aud, + pepper, + proof, + proofFetchCallback, + jwt, + verificationKeyHash, + } = args; + this.ephemeralKeyPair = ephemeralKeyPair; + this.publicKey = publicKey; + this.accountAddress = address + ? AccountAddress.from(address) + : (new AnyPublicKey(this.publicKey).authKey() as { derivedAddress(): AccountAddress }).derivedAddress(); + this.uidKey = uidKey; + this.uidVal = uidVal; + this.aud = aud; + this.jwt = jwt; + this.eventTarget = new EventTarget(); + this.proofOrPromise = proof; + + if (proof instanceof ZeroKnowledgeSig) { + this.proof = proof; + } else { + if (proofFetchCallback === undefined) { + throw new Error("Must provide callback for async proof fetch"); + } + this.eventTarget.addEventListener( + "proofFetchFinish", + async (e) => { + const status = (e as CustomEvent).detail; + await proofFetchCallback(status); + }, + { once: true }, + ); + this.init(proof); + } + + const pepperBytes = Hex.fromHexInput(pepper).toUint8Array(); + if (pepperBytes.length !== AbstractKeylessAccount.PEPPER_LENGTH) { + throw new Error(`Pepper length in bytes should be ${AbstractKeylessAccount.PEPPER_LENGTH}`); + } + this.pepper = pepperBytes; + + if (verificationKeyHash !== undefined) { + if (Hex.hexInputToUint8Array(verificationKeyHash).length !== 32) { + throw new Error("verificationKeyHash must be 32 bytes"); + } + this.verificationKeyHash = Hex.hexInputToUint8Array(verificationKeyHash); + } + } + + /** + * Returns the {@link AnyPublicKey} wrapper around this account's keyless public key. + * + * @returns An {@link AnyPublicKey} wrapping the underlying keyless public key. + */ + getAnyPublicKey(): AnyPublicKey { + return new AnyPublicKey(this.publicKey); + } + + /** + * Overwrites the pepper bytes with random and zero data, then marks the + * account's sensitive material as cleared. + * + * After calling this method, the account can no longer sign transactions or + * be serialized. Use this when the account is no longer needed to reduce the + * window during which sensitive data resides in memory. + * + * **Limitations:** + * - The JWT string (`this.jwt`) is a JavaScript string and cannot be zeroed. + * The runtime may retain it until garbage collection. + * - The ephemeral key pair should be cleared separately via + * `ephemeralKeyPair.clear()` if supported. + */ + clearSensitiveData(): void { + if (!this.sensitiveDataCleared) { + crypto.getRandomValues(this.pepper); + this.pepper.fill(0); + this.sensitiveDataCleared = true; + } + } + + /** + * Returns whether {@link clearSensitiveData} has been called. + */ + isSensitiveDataCleared(): boolean { + return this.sensitiveDataCleared; + } + + /** + * Awaits a pending proof fetch promise and stores the resolved proof. + * + * Dispatches a `proofFetchFinish` event on success or failure, which triggers + * the registered {@link ProofFetchCallback}. + * + * @param promise - The promise that will resolve to a {@link ZeroKnowledgeSig}. + * @returns A promise that resolves once the proof has been stored (or the fetch fails). + */ + async init(promise: Promise): Promise { + try { + this.proof = await promise; + this.eventTarget.dispatchEvent(new CustomEvent("proofFetchFinish", { detail: { status: "Success" } })); + } catch (error) { + const errorMsg = error instanceof Error ? error.toString() : "Unknown"; + this.eventTarget.dispatchEvent( + new CustomEvent("proofFetchFinish", { detail: { status: "Failed", error: errorMsg } }), + ); + } + } + + /** + * Serializes this account into BCS bytes. + * + * Throws if the proof has not yet been resolved (i.e. async fetch is still pending). + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + if (this.sensitiveDataCleared) { + throw new Error("Cannot serialize a KeylessAccount whose sensitive data has been cleared"); + } + this.accountAddress.serialize(serializer); + serializer.serializeStr(this.jwt); + serializer.serializeStr(this.uidKey); + serializer.serializeFixedBytes(this.pepper); + this.ephemeralKeyPair.serialize(serializer); + if (this.proof === undefined) { + throw new Error("Cannot serialize - proof undefined"); + } + this.proof.serialize(serializer); + serializer.serializeOption(this.verificationKeyHash, 32); + } + + /** + * Deserializes the fields that are common to all keyless account types from a + * BCS byte stream. + * + * Concrete subclasses call this method and then deserialize any additional + * type-specific fields before constructing themselves. + * + * @param deserializer - The BCS deserializer to read from. + * @returns An object containing `address`, `jwt`, `uidKey`, `pepper`, + * `ephemeralKeyPair`, `proof`, and an optional `verificationKeyHash`. + */ + static partialDeserialize(deserializer: Deserializer): { + address: AccountAddress; + jwt: string; + uidKey: string; + pepper: Uint8Array; + ephemeralKeyPair: EphemeralKeyPair; + proof: ZeroKnowledgeSig; + verificationKeyHash?: Uint8Array; + } { + const address = AccountAddress.deserialize(deserializer); + const jwt = deserializer.deserializeStr(); + const uidKey = deserializer.deserializeStr(); + const pepper = deserializer.deserializeFixedBytes(31); + const ephemeralKeyPair = EphemeralKeyPair.deserialize(deserializer); + const proof = ZeroKnowledgeSig.deserialize(deserializer); + const verificationKeyHash = deserializer.deserializeOption("fixedBytes", 32); + return { address, jwt, uidKey, pepper, ephemeralKeyPair, proof, verificationKeyHash }; + } + + /** + * Returns whether this account's ephemeral key pair has passed its expiry date. + * + * @returns `true` if the ephemeral key pair is expired, `false` otherwise. + */ + isExpired(): boolean { + return this.ephemeralKeyPair.isExpired(); + } + + /** + * Signs a message and returns an {@link AccountAuthenticatorSingleKey} wrapping + * the keyless public key and the {@link KeylessSignature}. + * + * @param message - The message bytes to sign, in any supported hex input format. + * @returns An {@link AccountAuthenticatorSingleKey} ready for use in a transaction. + */ + signWithAuthenticator(message: HexInput): AccountAuthenticatorSingleKey { + return new AccountAuthenticatorSingleKey(new AnyPublicKey(this.publicKey), new AnySignature(this.sign(message))); + } + + /** + * Signs a raw transaction and returns an {@link AccountAuthenticatorSingleKey}. + * + * @param transaction - The raw transaction to sign. + * @returns An {@link AccountAuthenticatorSingleKey} containing the keyless signature. + */ + signTransactionWithAuthenticator(transaction: AnyRawTransaction): AccountAuthenticatorSingleKey { + return new AccountAuthenticatorSingleKey( + new AnyPublicKey(this.publicKey), + new AnySignature(this.signTransaction(transaction)), + ); + } + + /** + * Waits for a pending background proof fetch to complete. + * + * If the proof was supplied eagerly, this resolves immediately. + * + * @returns A promise that resolves once {@link proofOrPromise} has settled. + */ + async waitForProofFetch(): Promise { + if (this.proofOrPromise instanceof Promise) { + await this.proofOrPromise; + } + } + + /** + * Validates the account state prior to signing a transaction. + * + * Checks that: + * - The ephemeral key pair has not expired. + * - The zero-knowledge proof has been resolved (waits if needed). + * - The JWT header contains a `kid` field. + * + * @returns A promise that resolves when the account is ready to sign. + * @throws {@link KeylessError} if the account is expired, the proof is missing, + * or the JWT is malformed. + */ + async checkKeylessAccountValidity(..._args: unknown[]): Promise { + if (this.isExpired()) { + throw KeylessError.fromErrorType({ type: KeylessErrorType.EPHEMERAL_KEY_PAIR_EXPIRED }); + } + await this.waitForProofFetch(); + if (this.proof === undefined) { + throw KeylessError.fromErrorType({ type: KeylessErrorType.ASYNC_PROOF_FETCH_FAILED }); + } + const header = decodeJwtHeader(this.jwt); + if (header.kid === undefined) { + throw KeylessError.fromErrorType({ + type: KeylessErrorType.JWT_PARSING_ERROR, + details: "checkKeylessAccountValidity failed. JWT is missing 'kid' in header.", + }); + } + // Full JWK verification requires network access (API layer). + // Additional checks can be added in the API layer's checkKeylessAccountValidity wrapper. + } + + /** + * Signs a raw message and returns a {@link KeylessSignature}. + * + * The signature includes the JWT header, an ephemeral certificate wrapping the + * zero-knowledge proof, the ephemeral public key, and the inner ephemeral + * signature over the message. + * + * @param message - The message bytes to sign, in any supported hex input format. + * @returns A {@link KeylessSignature} over the message. + * + * @throws {@link KeylessError} if the ephemeral key pair is expired or the + * proof has not yet been resolved. + */ + sign(message: HexInput): KeylessSignature { + const { expiryDateSecs } = this.ephemeralKeyPair; + if (this.isExpired()) { + throw KeylessError.fromErrorType({ type: KeylessErrorType.EPHEMERAL_KEY_PAIR_EXPIRED }); + } + if (this.proof === undefined) { + throw KeylessError.fromErrorType({ + type: KeylessErrorType.PROOF_NOT_FOUND, + details: "Proof not found - make sure to call `await account.checkKeylessAccountValidity()` before signing.", + }); + } + const ephemeralPublicKey = this.ephemeralKeyPair.getPublicKey(); + const ephemeralSignature = this.ephemeralKeyPair.sign(message); + + return new KeylessSignature({ + jwtHeader: base64UrlDecode(this.jwt.split(".")[0]), + ephemeralCertificate: new EphemeralCertificate(this.proof, EphemeralCertificateVariant.ZkProof), + expiryDateSecs, + ephemeralPublicKey, + ephemeralSignature, + }); + } + + /** + * Signs a raw transaction and returns a {@link KeylessSignature}. + * + * The signing message is derived by hashing the transaction together with the + * zero-knowledge proof to prevent proof replay. + * + * @param transaction - The raw transaction to sign. + * @returns A {@link KeylessSignature} over the combined transaction-and-proof message. + * + * @throws {@link KeylessError} if the proof has not yet been resolved. + */ + signTransaction(transaction: AnyRawTransaction): KeylessSignature { + if (this.proof === undefined) { + throw KeylessError.fromErrorType({ + type: KeylessErrorType.PROOF_NOT_FOUND, + details: "Proof not found - make sure to call `await account.checkKeylessAccountValidity()` before signing.", + }); + } + const raw = deriveTransactionType(transaction); + const txnAndProof = new TransactionAndProof(raw, this.proof.proof); + const signMess = txnAndProof.hash(); + return this.sign(signMess); + } + + /** + * Computes the signing message for a transaction combined with the + * zero-knowledge proof. + * + * This is the message that is passed to the ephemeral key's inner signing + * operation and allows the proof to be bound to the specific transaction. + * + * @param transaction - The raw transaction. + * @returns The 32-byte signing message (SHA3-256 hash of the BCS-encoded + * {@link TransactionAndProof}). + * + * @throws {@link KeylessError} if the proof has not yet been resolved. + */ + getSigningMessage(transaction: AnyRawTransaction): Uint8Array { + if (this.proof === undefined) { + throw KeylessError.fromErrorType({ + type: KeylessErrorType.PROOF_NOT_FOUND, + details: "Proof not found - make sure to call `await account.checkKeylessAccountValidity()` before signing.", + }); + } + const raw = deriveTransactionType(transaction); + const txnAndProof = new TransactionAndProof(raw, this.proof.proof); + return txnAndProof.hash(); + } + + /** + * Verifies that a {@link KeylessSignature} is valid for the given message. + * + * **Note:** Keyless signature verification requires on-chain ZK proof + * verification and cannot be performed client-side. This method always + * throws an error. Use on-chain transaction submission to verify keyless + * signatures. + * + * @param _args - An object with the `message` (hex input) and the + * `signature` ({@link KeylessSignature}) to verify. + * @returns Never — always throws. + * @throws Always throws because client-side keyless verification is not supported. + */ + verifySignature(_args: { message: HexInput; signature: KeylessSignature; [key: string]: unknown }): boolean { + throw new Error( + "Keyless signature verification is not supported client-side. Keyless signatures are verified on-chain.", + ); + } +} + +// ── TransactionAndProof ── + +/** + * A BCS-serializable container that binds a raw transaction to an optional + * zero-knowledge proof. + * + * The hash of this structure is the actual bytes signed by the ephemeral key + * inside a keyless signature, ensuring the proof cannot be replayed across + * different transactions. + */ +export class TransactionAndProof extends Serializable { + /** The raw transaction instance to be signed. */ + transaction: AnyRawTransactionInstance; + /** The optional zero-knowledge proof to bind to the transaction. */ + proof?: ZkProof; + /** The domain separator used when hashing this structure. */ + readonly domainSeparator = "APTOS::TransactionAndProof"; + + /** + * Creates a {@link TransactionAndProof}. + * + * @param transaction - The raw transaction to include. + * @param proof - An optional {@link ZkProof} to bind to the transaction. + */ + constructor(transaction: AnyRawTransactionInstance, proof?: ZkProof) { + super(); + this.transaction = transaction; + this.proof = proof; + } + + /** + * BCS-serializes the transaction and the optional proof into the given serializer. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeFixedBytes(this.transaction.bcsToBytes()); + serializer.serializeOption(this.proof); + } + + /** + * Computes the signing message for this structure by hashing its BCS bytes + * with the {@link domainSeparator}. + * + * @returns A 32-byte `Uint8Array` representing the signing message. + */ + hash(): Uint8Array { + return generateSigningMessage(this.bcsToBytes(), this.domainSeparator); + } +} diff --git a/v10/src/account/abstracted-account.ts b/v10/src/account/abstracted-account.ts new file mode 100644 index 000000000..7fbee23fe --- /dev/null +++ b/v10/src/account/abstracted-account.ts @@ -0,0 +1,330 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { sha3_256 } from "@noble/hashes/sha3.js"; +import { Serializer } from "../bcs/serializer.js"; +import { AccountAddress } from "../core/account-address.js"; +import { ACCOUNT_ABSTRACTION_SIGNING_DATA_SALT } from "../core/constants.js"; +import { AbstractPublicKey, AbstractSignature } from "../crypto/abstraction.js"; +import { SigningScheme } from "../crypto/types.js"; +import { Hex, type HexInput } from "../hex/index.js"; +import { AccountAbstractionMessage, AccountAuthenticatorAbstraction } from "../transactions/authenticator.js"; +import { generateSigningMessage, generateSigningMessageForTransaction } from "../transactions/signing-message.js"; +import type { AnyRawTransaction } from "../transactions/types.js"; +import type { Ed25519Account } from "./ed25519-account.js"; +import type { Account } from "./types.js"; + +function isValidFunctionInfo(functionInfo: string): boolean { + const parts = functionInfo.split("::"); + return parts.length === 3 && AccountAddress.isValid({ input: parts[0] }).valid; +} + +/** + * An account that delegates authentication to an arbitrary on-chain Move function. + * + * Account Abstraction (AA) allows any smart contract to define the authentication + * logic for an account. Instead of verifying a fixed signature scheme on-chain, + * the Aptos framework calls a user-supplied `authenticationFunction` with the + * signing digest and the bytes returned by the `signer` callback. + * + * The `authenticationFunction` must be a fully-qualified Move function of the + * form `
::::` and must match a Move function that + * accepts a `&signer`, a digest, and the abstraction signature bytes. + * + * @example + * ```typescript + * const account = new AbstractedAccount({ + * accountAddress: AccountAddress.fromString("0x1"), + * signer: (digest) => myCustomSigner(digest), + * authenticationFunction: "0x1::my_auth::authenticate", + * }); + * ``` + */ +export class AbstractedAccount implements Account { + /** The abstract public key that holds the account address. */ + readonly publicKey: AbstractPublicKey; + /** The on-chain address of the abstracted account. */ + readonly accountAddress: AccountAddress; + /** + * The fully-qualified Move function used for authentication + * (e.g. `"0x1::permissioned_delegation::authenticate"`). + */ + readonly authenticationFunction: string; + /** Always `SigningScheme.SingleKey` for abstracted accounts. */ + readonly signingScheme = SigningScheme.SingleKey; + + /** + * The signing function. It receives a digest and returns the raw bytes that + * will be passed to the on-chain `authenticationFunction`. + * + * Can be replaced at runtime via {@link setSigner}. + */ + sign: (message: HexInput) => AbstractSignature; + + /** + * Creates an {@link AbstractedAccount}. + * + * @param args.accountAddress - The on-chain address of the account. + * @param args.signer - A function that takes a digest and returns the raw + * authentication bytes expected by `authenticationFunction`. + * @param args.authenticationFunction - Fully-qualified Move function for + * on-chain authentication (format: `
::::`). + * + * @throws Error if `authenticationFunction` is not a valid fully-qualified + * Move function identifier. + */ + constructor(args: { + accountAddress: AccountAddress; + signer: (digest: HexInput) => Uint8Array; + authenticationFunction: string; + }) { + const { signer, accountAddress, authenticationFunction } = args; + + if (!isValidFunctionInfo(authenticationFunction)) { + throw new Error(`Invalid authentication function ${authenticationFunction} passed into AbstractedAccount`); + } + + this.authenticationFunction = authenticationFunction; + this.accountAddress = accountAddress; + this.publicKey = new AbstractPublicKey(this.accountAddress); + this.sign = (digest: HexInput) => new AbstractSignature(signer(digest)); + } + + /** + * Creates an {@link AbstractedAccount} pre-configured to use the + * `0x1::permissioned_delegation::authenticate` authentication function. + * + * The signer serializes the Ed25519 public key followed by the Ed25519 + * signature, which is the format expected by the permissioned delegation + * Move module. + * + * @param args.signer - An {@link Ed25519Account} used to produce the inner signature. + * @param args.accountAddress - Optional explicit account address; defaults to + * the `signer`'s own address. + * @returns A new {@link AbstractedAccount} backed by permissioned delegation. + */ + static fromPermissionedSigner(args: { signer: Ed25519Account; accountAddress?: AccountAddress }): AbstractedAccount { + const { signer, accountAddress } = args; + return new AbstractedAccount({ + signer: (digest: HexInput) => { + const serializer = new Serializer(); + signer.publicKey.serialize(serializer); + signer.sign(digest).serialize(serializer); + return serializer.toUint8Array(); + }, + accountAddress: accountAddress ?? signer.accountAddress, + authenticationFunction: "0x1::permissioned_delegation::authenticate", + }); + } + + /** + * Constructs the final signing message for account abstraction by wrapping + * the transaction signing message inside an {@link AccountAbstractionMessage} + * envelope and hashing with the domain separator. + * + * @param message - The inner signing message (e.g. from + * `generateSigningMessageForTransaction`). + * @param functionInfo - The fully-qualified authentication function identifier. + * @returns The bytes that the `signer` callback should sign. + */ + static generateAccountAbstractionMessage(message: HexInput, functionInfo: string): HexInput { + const accountAbstractionMessage = new AccountAbstractionMessage(message, functionInfo); + return generateSigningMessage(accountAbstractionMessage.bcsToBytes(), ACCOUNT_ABSTRACTION_SIGNING_DATA_SALT); + } + + /** + * Signs a message and returns an {@link AccountAuthenticatorAbstraction}. + * + * The message is first hashed with SHA3-256 before being passed to the signer. + * + * @param message - The message bytes to sign, in any supported hex input format. + * @returns An {@link AccountAuthenticatorAbstraction} ready for use in a transaction. + */ + signWithAuthenticator(message: HexInput): AccountAuthenticatorAbstraction { + const messageBytes = Hex.fromHexInput(message).toUint8Array(); + return new AccountAuthenticatorAbstraction( + this.authenticationFunction, + messageBytes, + this.sign(messageBytes).toUint8Array(), + ); + } + + /** + * Signs a raw transaction and returns an {@link AccountAuthenticatorAbstraction}. + * + * The signing message is wrapped in an account abstraction envelope before + * being passed to the signer. + * + * @param transaction - The raw transaction to sign. + * @returns An {@link AccountAuthenticatorAbstraction} containing the signature. + */ + signTransactionWithAuthenticator(transaction: AnyRawTransaction): AccountAuthenticatorAbstraction { + const digest = Hex.fromHexInput( + AbstractedAccount.generateAccountAbstractionMessage( + generateSigningMessageForTransaction(transaction), + this.authenticationFunction, + ), + ).toUint8Array(); + return new AccountAuthenticatorAbstraction(this.authenticationFunction, digest, this.sign(digest).toUint8Array()); + } + + /** + * Signs a raw transaction and returns the raw {@link AbstractSignature}. + * + * @param transaction - The raw transaction to sign. + * @returns The {@link AbstractSignature} over the transaction signing message. + */ + signTransaction(transaction: AnyRawTransaction): AbstractSignature { + const digest = Hex.fromHexInput( + AbstractedAccount.generateAccountAbstractionMessage( + generateSigningMessageForTransaction(transaction), + this.authenticationFunction, + ), + ).toUint8Array(); + return this.sign(digest); + } + + /** + * Replaces the signer function at runtime. + * + * Useful when the underlying credential needs to be rotated without + * re-creating the account instance. + * + * @param signer - The new signing function. Receives a digest and returns + * raw authentication bytes. + */ + setSigner(signer: (digest: HexInput) => HexInput): void { + this.sign = (digest: HexInput) => new AbstractSignature(signer(digest)); + } +} + +// ── DerivableAbstractedAccount ── + +/** + * An abstracted account whose on-chain address is deterministically derived + * from the authentication function and an abstract public key. + * + * Unlike {@link AbstractedAccount}, which requires an explicit account address, + * `DerivableAbstractedAccount` computes the address from the function identifier + * and a byte-string "abstract public key" using the domain-separated hash + * defined by the Aptos account abstraction specification. + * + * @example + * ```typescript + * const account = new DerivableAbstractedAccount({ + * abstractPublicKey: myPublicKeyBytes, + * authenticationFunction: "0x1::my_auth::authenticate", + * signer: (digest) => myCustomSigner(digest), + * }); + * ``` + */ +export class DerivableAbstractedAccount extends AbstractedAccount { + /** + * The abstract public key bytes that, together with the authentication + * function, uniquely identify and address this account. + */ + readonly abstractPublicKey: Uint8Array; + /** + * The domain separator byte appended to the hash input during address + * derivation. Fixed at `5` by the Aptos account abstraction specification. + */ + static readonly ADDRESS_DOMAIN_SEPARATOR: number = 5; + + /** + * Creates a {@link DerivableAbstractedAccount}. + * + * The on-chain address is computed from `authenticationFunction` and + * `abstractPublicKey` using {@link DerivableAbstractedAccount.computeAccountAddress}. + * + * @param args.signer - A function that takes a digest and returns authentication bytes. + * @param args.authenticationFunction - Fully-qualified Move function for + * on-chain authentication. + * @param args.abstractPublicKey - Byte string used to derive the account address. + * + * @throws Error if `authenticationFunction` is not a valid fully-qualified + * Move function identifier. + */ + constructor(args: { + signer: (digest: HexInput) => Uint8Array; + authenticationFunction: string; + abstractPublicKey: Uint8Array; + }) { + const { signer, authenticationFunction, abstractPublicKey } = args; + const daaAccountAddress = new AccountAddress( + DerivableAbstractedAccount.computeAccountAddress(authenticationFunction, abstractPublicKey), + ); + super({ accountAddress: daaAccountAddress, signer, authenticationFunction }); + this.abstractPublicKey = abstractPublicKey; + } + + /** + * Computes the deterministic on-chain address for a derivable abstracted account. + * + * The address is derived as: + * ``` + * SHA3-256(BCS(moduleAddress) || BCS(moduleName) || BCS(functionName) + * || BCS(abstractPublicKey) || [ADDRESS_DOMAIN_SEPARATOR]) + * ``` + * + * @param functionInfo - Fully-qualified Move function identifier + * (e.g. `"0x1::my_auth::authenticate"`). + * @param accountIdentifier - The abstract public key bytes. + * @returns A 32-byte `Uint8Array` representing the derived account address. + * + * @throws Error if `functionInfo` is not a valid fully-qualified Move function identifier. + */ + static computeAccountAddress(functionInfo: string, accountIdentifier: Uint8Array): Uint8Array { + if (!isValidFunctionInfo(functionInfo)) { + throw new Error(`Invalid authentication function ${functionInfo} passed into DerivableAbstractedAccount`); + } + const [moduleAddress, moduleName, functionName] = functionInfo.split("::"); + + const hash = sha3_256.create(); + const serializer = new Serializer(); + AccountAddress.fromString(moduleAddress).serialize(serializer); + serializer.serializeStr(moduleName); + serializer.serializeStr(functionName); + hash.update(serializer.toUint8Array()); + + const s2 = new Serializer(); + s2.serializeBytes(accountIdentifier); + hash.update(s2.toUint8Array()); + + hash.update(new Uint8Array([DerivableAbstractedAccount.ADDRESS_DOMAIN_SEPARATOR])); + + return hash.digest(); + } + + /** + * Signs a message and returns an {@link AccountAuthenticatorAbstraction} that + * includes the abstract public key bytes. + * + * @param message - The message bytes to sign, in any supported hex input format. + * @returns An {@link AccountAuthenticatorAbstraction} including the + * `abstractPublicKey` required for on-chain verification. + */ + signWithAuthenticator(message: HexInput): AccountAuthenticatorAbstraction { + const messageBytes = Hex.fromHexInput(message).toUint8Array(); + return new AccountAuthenticatorAbstraction( + this.authenticationFunction, + messageBytes, + this.sign(messageBytes).value, + this.abstractPublicKey, + ); + } + + signTransactionWithAuthenticator(transaction: AnyRawTransaction): AccountAuthenticatorAbstraction { + const digest = Hex.fromHexInput( + AbstractedAccount.generateAccountAbstractionMessage( + generateSigningMessageForTransaction(transaction), + this.authenticationFunction, + ), + ).toUint8Array(); + return new AccountAuthenticatorAbstraction( + this.authenticationFunction, + digest, + this.sign(digest).value, + this.abstractPublicKey, + ); + } +} diff --git a/v10/src/account/ed25519-account.ts b/v10/src/account/ed25519-account.ts new file mode 100644 index 000000000..cade5ecf3 --- /dev/null +++ b/v10/src/account/ed25519-account.ts @@ -0,0 +1,170 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { AccountAddress, type AccountAddressInput } from "../core/account-address.js"; +import { AuthenticationKey } from "../core/authentication-key.js"; +import { Ed25519PrivateKey, type Ed25519PublicKey, type Ed25519Signature } from "../crypto/ed25519.js"; +import { SigningScheme } from "../crypto/types.js"; +import type { HexInput } from "../hex/index.js"; +import { AccountAuthenticatorEd25519 } from "../transactions/authenticator.js"; +import { generateSigningMessageForTransaction } from "../transactions/signing-message.js"; +import type { AnyRawTransaction } from "../transactions/types.js"; +import type { Account } from "./types.js"; + +/** + * Constructor arguments for {@link Ed25519Account}. + */ +export interface Ed25519SignerConstructorArgs { + /** The Ed25519 private key used for signing. */ + privateKey: Ed25519PrivateKey; + /** + * Optional explicit on-chain address. When omitted the address is derived + * from the Ed25519 authentication key. + */ + address?: AccountAddressInput; +} + +/** + * Arguments for {@link Ed25519Account.fromDerivationPath}. + */ +export interface Ed25519SignerFromDerivationPathArgs { + /** BIP-44 derivation path string (e.g. `"m/44'/637'/0'/0'/0'"`). */ + path: string; + /** Space-separated BIP-39 mnemonic phrase. */ + mnemonic: string; +} + +/** + * A legacy Ed25519 account that signs using the `Ed25519` scheme. + * + * This is the original Aptos signing scheme. For accounts created after the + * `SingleKey` scheme was introduced, prefer {@link SingleKeyAccount} instead. + * + * @example + * ```typescript + * // Generate a brand-new account + * const account = Ed25519Account.generate(); + * + * // Reconstruct from an existing private key + * const key = new Ed25519PrivateKey("0xabc123..."); + * const account2 = new Ed25519Account({ privateKey: key }); + * ``` + */ +export class Ed25519Account implements Account { + /** The Ed25519 private key held by this account. */ + readonly privateKey: Ed25519PrivateKey; + /** The Ed25519 public key derived from {@link privateKey}. */ + readonly publicKey: Ed25519PublicKey; + /** The on-chain address of this account. */ + readonly accountAddress: AccountAddress; + /** Always `SigningScheme.Ed25519` for this account type. */ + readonly signingScheme = SigningScheme.Ed25519; + + /** + * Creates an {@link Ed25519Account} from an existing private key and an + * optional explicit address. + * + * @param args - {@link Ed25519SignerConstructorArgs} + */ + constructor(args: Ed25519SignerConstructorArgs) { + const { privateKey, address } = args; + this.privateKey = privateKey; + this.publicKey = privateKey.publicKey(); + this.accountAddress = address + ? AccountAddress.from(address) + : AuthenticationKey.fromSchemeAndBytes({ + scheme: SigningScheme.Ed25519, + input: this.publicKey.toUint8Array(), + }).derivedAddress(); + } + + /** + * Generates a new {@link Ed25519Account} with a randomly generated private key. + * + * @returns A new {@link Ed25519Account} instance. + * + * @example + * ```typescript + * const account = Ed25519Account.generate(); + * ``` + */ + static generate(): Ed25519Account { + return new Ed25519Account({ privateKey: Ed25519PrivateKey.generate() }); + } + + /** + * Derives an {@link Ed25519Account} from a BIP-44 derivation path and a + * BIP-39 mnemonic phrase. + * + * @param args - {@link Ed25519SignerFromDerivationPathArgs} + * @returns A deterministic {@link Ed25519Account} for the given path and mnemonic. + * + * @example + * ```typescript + * const account = Ed25519Account.fromDerivationPath({ + * path: "m/44'/637'/0'/0'/0'", + * mnemonic: "word1 word2 ... word12", + * }); + * ``` + */ + static fromDerivationPath(args: Ed25519SignerFromDerivationPathArgs): Ed25519Account { + const { path, mnemonic } = args; + return new Ed25519Account({ privateKey: Ed25519PrivateKey.fromDerivationPath(path, mnemonic) }); + } + + /** + * Verifies that the given signature is valid for the given message under this + * account's public key. + * + * @param args - An object containing the `message` (hex input) and the + * `signature` ({@link Ed25519Signature}) to verify. + * @returns `true` if the signature is valid, `false` otherwise. + */ + verifySignature(args: { message: HexInput; signature: Ed25519Signature }): boolean { + return this.publicKey.verifySignature(args); + } + + /** + * Signs a message and returns an {@link AccountAuthenticatorEd25519} wrapping + * the public key and the signature. + * + * @param message - The message bytes to sign, in any supported hex input format. + * @returns An {@link AccountAuthenticatorEd25519} ready for use in a transaction. + */ + signWithAuthenticator(message: HexInput): AccountAuthenticatorEd25519 { + return new AccountAuthenticatorEd25519(this.publicKey, this.privateKey.sign(message)); + } + + /** + * Signs a raw transaction and returns an {@link AccountAuthenticatorEd25519}. + * + * @param transaction - The raw transaction to sign. + * @returns An {@link AccountAuthenticatorEd25519} containing the signature. + */ + signTransactionWithAuthenticator(transaction: AnyRawTransaction): AccountAuthenticatorEd25519 { + return new AccountAuthenticatorEd25519(this.publicKey, this.signTransaction(transaction)); + } + + /** + * Signs a message and returns the raw {@link Ed25519Signature}. + * + * @param message - The message bytes to sign, in any supported hex input format. + * @returns The {@link Ed25519Signature} over the message. + */ + sign(message: HexInput): Ed25519Signature { + return this.privateKey.sign(message); + } + + /** + * Signs a raw transaction and returns the raw {@link Ed25519Signature}. + * + * The signing message is derived from the transaction according to the Aptos + * signing message specification. + * + * @param transaction - The raw transaction to sign. + * @returns The {@link Ed25519Signature} over the transaction signing message. + */ + signTransaction(transaction: AnyRawTransaction): Ed25519Signature { + return this.sign(generateSigningMessageForTransaction(transaction)); + } +} diff --git a/v10/src/account/ephemeral-key-pair.ts b/v10/src/account/ephemeral-key-pair.ts new file mode 100644 index 000000000..fd143823a --- /dev/null +++ b/v10/src/account/ephemeral-key-pair.ts @@ -0,0 +1,251 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { randomBytes } from "@noble/hashes/utils.js"; +import { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; +import { Ed25519PrivateKey } from "../crypto/ed25519.js"; +import { EphemeralPublicKey, EphemeralSignature } from "../crypto/ephemeral.js"; +import { bytesToBigIntLE, padAndPackBytesWithLen, poseidonHash } from "../crypto/poseidon.js"; +import type { PrivateKey } from "../crypto/private-key.js"; +import { EphemeralPublicKeyVariant } from "../crypto/types.js"; +import type { HexInput } from "../hex/index.js"; +import { Hex } from "../hex/index.js"; + +const TWO_WEEKS_IN_SECONDS = 1_209_600; + +function floorToWholeHour(timestampInSeconds: number): number { + return Math.floor(timestampInSeconds / 3600) * 3600; +} + +function nowInSeconds(): number { + return Math.floor(Date.now() / 1000); +} + +/** + * A short-lived key pair used as the inner signing key for keyless accounts. + * + * In the keyless flow the user's OAuth/OIDC provider issues a JWT whose `nonce` + * field commits to the ephemeral public key and an expiry timestamp. The + * ephemeral private key is used to sign the actual transaction, while a + * zero-knowledge proof binds the ephemeral key to the user's OIDC identity. + * + * The private key is automatically wiped from memory via {@link clear} once it + * is no longer needed; after clearing the pair cannot be used for signing. + * + * @example + * ```typescript + * // Generate a key pair expiring in two weeks (default) + * const ekp = EphemeralKeyPair.generate(); + * console.log(ekp.nonce); // pass this as the OIDC nonce parameter + * + * // Sign a message (throws if expired or cleared) + * const sig = ekp.sign(message); + * + * // Securely erase the private key + * ekp.clear(); + * ``` + */ +export class EphemeralKeyPair extends Serializable { + /** Length in bytes of the blinder value used in the nonce computation. */ + static readonly BLINDER_LENGTH: number = 31; + + /** + * The random 31-byte blinder included in the Poseidon hash that produces + * the JWT nonce. + */ + readonly blinder: Uint8Array; + /** + * Unix timestamp (seconds) after which this key pair is considered expired + * and can no longer be used for signing. + */ + readonly expiryDateSecs: number; + /** + * The string nonce value that must be embedded in the OIDC authentication + * request so that the JWT commits to this ephemeral public key. + * + * Not zeroed on {@link clear} because the nonce is a public Poseidon hash + * derived from the ephemeral public key, expiry, and blinder — it does not + * reveal private key material. + */ + readonly nonce: string; + + private privateKey: PrivateKey; + private publicKey: EphemeralPublicKey; + private cleared: boolean = false; + + /** + * Creates an {@link EphemeralKeyPair} from an existing private key. + * + * @param args.privateKey - The underlying private key. + * @param args.expiryDateSecs - Unix timestamp (seconds) for the expiry. + * Defaults to two weeks from now, floored to the nearest whole hour. + * @param args.blinder - Optional 31-byte blinder. A random blinder is + * generated if omitted. + */ + constructor(args: { privateKey: PrivateKey; expiryDateSecs?: number; blinder?: HexInput }) { + super(); + const { privateKey, expiryDateSecs, blinder } = args; + this.privateKey = privateKey; + this.publicKey = new EphemeralPublicKey(privateKey.publicKey()); + this.expiryDateSecs = + expiryDateSecs !== undefined ? expiryDateSecs : floorToWholeHour(nowInSeconds() + TWO_WEEKS_IN_SECONDS); + this.blinder = + blinder !== undefined ? Hex.fromHexInput(blinder).toUint8Array() : randomBytes(EphemeralKeyPair.BLINDER_LENGTH); + + // Calculate the nonce + const fields = padAndPackBytesWithLen(this.publicKey.bcsToBytes(), 93); + fields.push(BigInt(this.expiryDateSecs)); + fields.push(bytesToBigIntLE(this.blinder)); + this.nonce = poseidonHash(fields).toString(); + } + + /** + * Returns the {@link EphemeralPublicKey} for this key pair. + * + * @returns The {@link EphemeralPublicKey} derived from the underlying private key. + */ + getPublicKey(): EphemeralPublicKey { + return this.publicKey; + } + + /** + * Returns whether this key pair has passed its expiry date. + * + * @returns `true` if the current time is past {@link expiryDateSecs}. + */ + isExpired(): boolean { + return Math.floor(Date.now() / 1000) > this.expiryDateSecs; + } + + /** + * Securely erases the private key and blinder from memory. + * + * After calling this method, the key pair can no longer be used for signing. + * Subsequent calls to {@link sign} will throw an error. This method is + * idempotent; calling it more than once has no additional effect. + */ + clear(): void { + if (!this.cleared) { + if ("clear" in this.privateKey && typeof this.privateKey.clear === "function") { + this.privateKey.clear(); + } else { + const keyBytes = this.privateKey.toUint8Array(); + crypto.getRandomValues(keyBytes); + keyBytes.fill(0xff); + crypto.getRandomValues(keyBytes); + keyBytes.fill(0); + } + crypto.getRandomValues(this.blinder); + this.blinder.fill(0xff); + crypto.getRandomValues(this.blinder); + this.blinder.fill(0); + this.cleared = true; + } + } + + /** + * Returns whether this key pair has been cleared from memory. + * + * @returns `true` if {@link clear} has been called at least once. + */ + isCleared(): boolean { + return this.cleared; + } + + /** + * Serializes this key pair into BCS bytes. + * + * **Security warning:** The serialized output includes the private key material. + * Only persist the result in secure storage (e.g. encrypted at rest) and avoid + * logging or transmitting it over insecure channels. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + if (this.cleared) { + throw new Error("EphemeralKeyPair has been cleared from memory and cannot be serialized"); + } + serializer.serializeU32AsUleb128(this.publicKey.variant); + serializer.serializeBytes(this.privateKey.toUint8Array()); + serializer.serializeU64(this.expiryDateSecs); + serializer.serializeFixedBytes(this.blinder); + } + + /** + * Deserializes an {@link EphemeralKeyPair} from a BCS byte stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A fully-constructed {@link EphemeralKeyPair}. + * + * @throws Error if the key variant is unsupported. + */ + static deserialize(deserializer: Deserializer): EphemeralKeyPair { + const variantIndex = deserializer.deserializeUleb128AsU32(); + let privateKey: PrivateKey; + switch (variantIndex) { + case EphemeralPublicKeyVariant.Ed25519: + privateKey = Ed25519PrivateKey.deserialize(deserializer); + break; + default: + throw new Error(`Unknown variant index for EphemeralPublicKey: ${variantIndex}`); + } + const expiryDateSecsBig = deserializer.deserializeU64(); + if (expiryDateSecsBig > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new Error(`expiryDateSecs ${expiryDateSecsBig} exceeds safe integer range`); + } + const blinder = deserializer.deserializeFixedBytes(31); + return new EphemeralKeyPair({ privateKey, expiryDateSecs: Number(expiryDateSecsBig), blinder }); + } + + /** + * Deserializes an {@link EphemeralKeyPair} from raw bytes. + * + * @param bytes - BCS bytes previously produced by {@link EphemeralKeyPair.serialize}. + * @returns A fully-constructed {@link EphemeralKeyPair}. + */ + static fromBytes(bytes: Uint8Array): EphemeralKeyPair { + return EphemeralKeyPair.deserialize(new Deserializer(bytes)); + } + + /** + * Generates a new {@link EphemeralKeyPair} with a randomly generated Ed25519 + * private key. + * + * @param args - Optional generation parameters. + * @param args.scheme - The key variant to generate. Currently only Ed25519 + * is supported. + * @param args.expiryDateSecs - Optional explicit expiry timestamp (seconds). + * Defaults to two weeks from now, floored to the nearest whole hour. + * @returns A new {@link EphemeralKeyPair}. + * + * @example + * ```typescript + * const ekp = EphemeralKeyPair.generate(); + * ``` + */ + static generate(args?: { scheme?: EphemeralPublicKeyVariant; expiryDateSecs?: number }): EphemeralKeyPair { + // Only Ed25519 is supported for now + return new EphemeralKeyPair({ privateKey: Ed25519PrivateKey.generate(), expiryDateSecs: args?.expiryDateSecs }); + } + + /** + * Signs a message with the ephemeral private key and returns an + * {@link EphemeralSignature}. + * + * @param data - The message bytes to sign, in any supported hex input format. + * @returns An {@link EphemeralSignature} over the message. + * + * @throws Error if the key pair has been cleared from memory. + * @throws Error if the key pair has expired. + */ + sign(data: HexInput): EphemeralSignature { + if (this.cleared) { + throw new Error("EphemeralKeyPair has been cleared from memory and can no longer be used"); + } + if (this.isExpired()) { + throw new Error("EphemeralKeyPair has expired"); + } + return new EphemeralSignature(this.privateKey.sign(data)); + } +} diff --git a/v10/src/account/factory.ts b/v10/src/account/factory.ts new file mode 100644 index 000000000..4cfca34ed --- /dev/null +++ b/v10/src/account/factory.ts @@ -0,0 +1,158 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { AuthenticationKey } from "../core/authentication-key.js"; +import { Ed25519PrivateKey } from "../crypto/ed25519.js"; +import type { AccountPublicKey } from "../crypto/public-key.js"; +import { SigningSchemeInput } from "../crypto/types.js"; +import { Ed25519Account } from "./ed25519-account.js"; +import { SingleKeyAccount } from "./single-key-account.js"; +import type { + Account, + CreateAccountFromPrivateKeyArgs, + CreateEd25519AccountFromPrivateKeyArgs, + CreateSingleKeyAccountFromPrivateKeyArgs, + GenerateAccountArgs, + GenerateEd25519AccountArgs, + GenerateEd25519SingleKeyAccountArgs, + GenerateSingleKeyAccountArgs, + PrivateKeyFromDerivationPathArgs, +} from "./types.js"; + +// ── generateAccount ── + +/** + * Generates a new Aptos account with a randomly generated private key. + * + * The overloads allow callers to obtain the most specific return type based on + * the combination of `scheme` and `legacy` flags provided: + * + * - No args / `legacy: true` → legacy {@link Ed25519Account} + * - `scheme: Ed25519, legacy: false` → {@link SingleKeyAccount} (Ed25519 key) + * - `scheme: Secp256k1Ecdsa` → {@link SingleKeyAccount} (Secp256k1 key) + * + * @param args - Optional generation arguments controlling scheme and legacy mode. + * @returns A newly generated {@link Ed25519Account} or {@link SingleKeyAccount}. + * + * @example + * ```typescript + * // Legacy Ed25519 account (default) + * const a = generateAccount(); + * + * // SingleKey Ed25519 account + * const b = generateAccount({ scheme: SigningSchemeInput.Ed25519, legacy: false }); + * + * // SingleKey Secp256k1 account + * const c = generateAccount({ scheme: SigningSchemeInput.Secp256k1Ecdsa }); + * ``` + */ +export function generateAccount(args?: GenerateEd25519AccountArgs): Ed25519Account; +export function generateAccount(args: GenerateEd25519SingleKeyAccountArgs): SingleKeyAccount; +export function generateAccount(args: GenerateSingleKeyAccountArgs): SingleKeyAccount; +export function generateAccount(args: GenerateAccountArgs): Account; +export function generateAccount(args: GenerateAccountArgs = {}): Account { + const { scheme = SigningSchemeInput.Ed25519, legacy = true } = args; + if (scheme === SigningSchemeInput.Ed25519 && legacy) { + return Ed25519Account.generate(); + } + return SingleKeyAccount.generate({ scheme }); +} + +// ── accountFromPrivateKey ── + +/** + * Creates an Aptos account from an existing private key. + * + * The overloads allow callers to obtain the most specific return type based on + * the key type and `legacy` flag: + * + * - `Ed25519PrivateKey` + `legacy: true` (default) → legacy {@link Ed25519Account} + * - `Ed25519PrivateKey` + `legacy: false` → {@link SingleKeyAccount} + * - Any other key type → {@link SingleKeyAccount} + * + * @param args - Arguments including the private key, optional address, and legacy flag. + * @returns An {@link Ed25519Account} or {@link SingleKeyAccount}. + * + * @example + * ```typescript + * const key = new Ed25519PrivateKey("0xabc..."); + * + * // Legacy Ed25519 account (default) + * const a = accountFromPrivateKey({ privateKey: key }); + * + * // SingleKey account from the same Ed25519 key + * const b = accountFromPrivateKey({ privateKey: key, legacy: false }); + * ``` + */ +export function accountFromPrivateKey(args: CreateEd25519AccountFromPrivateKeyArgs): Ed25519Account; +export function accountFromPrivateKey(args: CreateSingleKeyAccountFromPrivateKeyArgs): SingleKeyAccount; +export function accountFromPrivateKey(args: CreateAccountFromPrivateKeyArgs): Account; +export function accountFromPrivateKey(args: CreateAccountFromPrivateKeyArgs): Account { + const { privateKey, address, legacy = true } = args; + if (privateKey instanceof Ed25519PrivateKey && legacy) { + return new Ed25519Account({ privateKey, address }); + } + return new SingleKeyAccount({ privateKey, address }); +} + +// ── accountFromDerivationPath ── + +/** + * Derives an Aptos account from a BIP-44 derivation path and a BIP-39 mnemonic phrase. + * + * The overloads allow callers to obtain the most specific return type based on + * the combination of `scheme` and `legacy` flags: + * + * - No scheme / `legacy: true` → legacy {@link Ed25519Account} + * - `scheme: Ed25519, legacy: false` → {@link SingleKeyAccount} (Ed25519 key) + * - `scheme: Secp256k1Ecdsa` → {@link SingleKeyAccount} (Secp256k1 key) + * + * @param args - Derivation arguments including `path`, `mnemonic`, and optional + * `scheme` / `legacy` flags. + * @returns A deterministic {@link Ed25519Account} or {@link SingleKeyAccount}. + * + * @example + * ```typescript + * const account = accountFromDerivationPath({ + * path: "m/44'/637'/0'/0'/0'", + * mnemonic: "word1 word2 ... word12", + * }); + * ``` + */ +export function accountFromDerivationPath( + args: GenerateEd25519AccountArgs & PrivateKeyFromDerivationPathArgs, +): Ed25519Account; +export function accountFromDerivationPath( + args: GenerateEd25519SingleKeyAccountArgs & PrivateKeyFromDerivationPathArgs, +): SingleKeyAccount; +export function accountFromDerivationPath( + args: GenerateSingleKeyAccountArgs & PrivateKeyFromDerivationPathArgs, +): SingleKeyAccount; +export function accountFromDerivationPath(args: GenerateAccountArgs & PrivateKeyFromDerivationPathArgs): Account; +export function accountFromDerivationPath(args: GenerateAccountArgs & PrivateKeyFromDerivationPathArgs): Account { + const { scheme = SigningSchemeInput.Ed25519, mnemonic, path, legacy = true } = args; + if (scheme === SigningSchemeInput.Ed25519 && legacy) { + return Ed25519Account.fromDerivationPath({ mnemonic, path }); + } + return SingleKeyAccount.fromDerivationPath({ scheme, mnemonic, path }); +} + +// ── authKey ── + +/** + * Computes the {@link AuthenticationKey} for a given public key. + * + * This is a thin convenience wrapper around `publicKey.authKey()`. + * + * @param args.publicKey - Any {@link AccountPublicKey} (Ed25519, SingleKey, MultiKey, etc.). + * @returns The corresponding {@link AuthenticationKey}. + * + * @example + * ```typescript + * const account = Ed25519Account.generate(); + * const key = authKey({ publicKey: account.publicKey }); + * ``` + */ +export function authKey(args: { publicKey: AccountPublicKey }): AuthenticationKey { + return args.publicKey.authKey() as AuthenticationKey; +} diff --git a/v10/src/account/federated-keyless-account.ts b/v10/src/account/federated-keyless-account.ts new file mode 100644 index 000000000..1d3cf5889 --- /dev/null +++ b/v10/src/account/federated-keyless-account.ts @@ -0,0 +1,211 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { Deserializer } from "../bcs/deserializer.js"; +import type { Serializer } from "../bcs/serializer.js"; +import { AccountAddress, type AccountAddressInput } from "../core/account-address.js"; +import { FederatedKeylessPublicKey } from "../crypto/federated-keyless.js"; +import type { Groth16VerificationKey, ZeroKnowledgeSig } from "../crypto/keyless.js"; +import type { HexInput } from "../hex/index.js"; +import { Hex } from "../hex/index.js"; +import { AbstractKeylessAccount, getIssAudAndUidVal, type ProofFetchCallback } from "./abstract-keyless-account.js"; +import type { EphemeralKeyPair } from "./ephemeral-key-pair.js"; + +/** + * A keyless account whose JSON Web Keys (JWKs) are stored at an arbitrary + * on-chain address rather than the well-known provider registry. + * + * Federated keyless accounts allow organizations to operate their own OIDC + * provider and register the corresponding JWKs under a custom on-chain address. + * This enables keyless signing for any OIDC-compatible identity provider, not + * just those already supported by the Aptos framework. + * + * Use the static {@link FederatedKeylessAccount.create} factory for the most + * ergonomic construction. + * + * @example + * ```typescript + * const ekp = EphemeralKeyPair.generate(); + * const account = FederatedKeylessAccount.create({ + * jwt, + * ephemeralKeyPair: ekp, + * pepper, + * proof: zkProof, + * jwkAddress: "0xabc...", + * }); + * await account.checkKeylessAccountValidity(); + * ``` + */ +export class FederatedKeylessAccount extends AbstractKeylessAccount { + /** The {@link FederatedKeylessPublicKey} that includes the JWK provider address. */ + readonly publicKey: FederatedKeylessPublicKey; + /** Whether the account is configured in audience-less mode. */ + readonly audless: boolean; + + /** + * Creates a {@link FederatedKeylessAccount} from explicit OIDC claim values + * and a JWK provider address. + * + * Prefer {@link FederatedKeylessAccount.create} for a higher-level API that + * extracts claims directly from the JWT. + * + * @param args.address - Optional explicit on-chain address; derived from the public key when omitted. + * @param args.ephemeralKeyPair - The short-lived key pair whose public key is committed to in the JWT nonce. + * @param args.iss - The OIDC issuer claim (`iss`) from the JWT. + * @param args.uidKey - The JWT payload claim used as the user identifier. + * @param args.uidVal - The value of `uidKey` in the JWT payload. + * @param args.aud - The audience claim (`aud`) from the JWT. + * @param args.pepper - A 31-byte random value used to blind the user identifier. + * @param args.jwkAddress - The on-chain address where the JWKs for the OIDC provider are stored. + * @param args.proof - The zero-knowledge proof, or a promise that will resolve to one. + * @param args.proofFetchCallback - Required when `proof` is a promise; called on completion. + * @param args.jwt - The raw JWT string. + * @param args.verificationKeyHash - Optional 32-byte hash of the Groth16 verification key. + * @param args.audless - Whether the account is audience-less. Defaults to `false`. + */ + constructor(args: { + address?: AccountAddress; + ephemeralKeyPair: EphemeralKeyPair; + iss: string; + uidKey: string; + uidVal: string; + aud: string; + pepper: HexInput; + jwkAddress: AccountAddress; + proof: ZeroKnowledgeSig | Promise; + proofFetchCallback?: ProofFetchCallback; + jwt: string; + verificationKeyHash?: HexInput; + audless?: boolean; + }) { + const publicKey = FederatedKeylessPublicKey.create(args); + super({ publicKey, ...args }); + this.publicKey = publicKey; + this.audless = args.audless ?? false; + } + + /** + * Serializes this account into BCS bytes, including the JWK provider address. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + super.serialize(serializer); + (this.publicKey.jwkAddress as AccountAddress).serialize(serializer); + } + + /** + * Deserializes a {@link FederatedKeylessAccount} from a BCS byte stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A fully-constructed {@link FederatedKeylessAccount}. + */ + static deserialize(deserializer: Deserializer): FederatedKeylessAccount { + const { address, proof, ephemeralKeyPair, jwt, uidKey, pepper, verificationKeyHash } = + AbstractKeylessAccount.partialDeserialize(deserializer); + const jwkAddress = AccountAddress.deserialize(deserializer); + const { iss, aud, uidVal } = getIssAudAndUidVal({ jwt, uidKey }); + return new FederatedKeylessAccount({ + address, + proof, + ephemeralKeyPair, + iss, + uidKey, + uidVal, + aud, + pepper, + jwt, + verificationKeyHash, + jwkAddress, + }); + } + + /** + * Deserializes a {@link FederatedKeylessAccount} from a hex-encoded byte string + * or `Uint8Array`. + * + * @param bytes - BCS bytes previously produced by + * {@link FederatedKeylessAccount.serialize}. + * @returns A fully-constructed {@link FederatedKeylessAccount}. + */ + static fromBytes(bytes: HexInput): FederatedKeylessAccount { + return FederatedKeylessAccount.deserialize(new Deserializer(Hex.hexInputToUint8Array(bytes))); + } + + /** + * High-level factory that creates a {@link FederatedKeylessAccount} by + * extracting OIDC claims directly from the JWT. + * + * This is the recommended way to construct a {@link FederatedKeylessAccount}. + * + * @param args.address - Optional explicit on-chain address. + * @param args.proof - The zero-knowledge proof, or a promise resolving to one. + * @param args.jwt - The raw JWT string from the OIDC provider. + * @param args.ephemeralKeyPair - The short-lived key pair committed to in the JWT nonce. + * @param args.pepper - A 31-byte random value used to blind the user identifier. + * @param args.jwkAddress - The on-chain address (or address input) where the JWKs are stored. + * @param args.uidKey - The JWT claim to use as the user identifier. Defaults to `"sub"`. + * @param args.proofFetchCallback - Required when `proof` is a promise; called on completion. + * @param args.verificationKey - The Groth16 verification key (hashed and stored). + * @param args.verificationKeyHash - Pre-computed 32-byte hash of the verification key. + * @returns A new {@link FederatedKeylessAccount}. + * + * @throws Error if both `verificationKey` and `verificationKeyHash` are provided. + * + * @example + * ```typescript + * const account = FederatedKeylessAccount.create({ + * jwt, + * ephemeralKeyPair, + * pepper, + * proof: zkProof, + * jwkAddress: "0xabc...", + * }); + * ``` + */ + static create(args: { + address?: AccountAddress; + proof: ZeroKnowledgeSig | Promise; + jwt: string; + ephemeralKeyPair: EphemeralKeyPair; + pepper: HexInput; + jwkAddress: AccountAddressInput; + uidKey?: string; + proofFetchCallback?: ProofFetchCallback; + verificationKey?: Groth16VerificationKey; + verificationKeyHash?: Uint8Array; + }): FederatedKeylessAccount { + const { + address, + proof, + jwt, + ephemeralKeyPair, + pepper, + jwkAddress, + uidKey = "sub", + proofFetchCallback, + verificationKey, + verificationKeyHash, + } = args; + + if (verificationKeyHash && verificationKey) { + throw new Error("Cannot provide both verificationKey and verificationKeyHash"); + } + + const { iss, aud, uidVal } = getIssAudAndUidVal({ jwt, uidKey }); + return new FederatedKeylessAccount({ + address, + proof, + ephemeralKeyPair, + iss, + uidKey, + uidVal, + aud, + pepper, + jwkAddress: AccountAddress.from(jwkAddress), + jwt, + proofFetchCallback, + verificationKeyHash: verificationKeyHash ?? (verificationKey ? verificationKey.hash() : undefined), + }); + } +} diff --git a/v10/src/account/index.ts b/v10/src/account/index.ts new file mode 100644 index 000000000..62cb18fb5 --- /dev/null +++ b/v10/src/account/index.ts @@ -0,0 +1,41 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * @module account + * + * Public account types and factory utilities for the Aptos TypeScript SDK. + * + * This module exports: + * - The core {@link Account} interface and related type-guards + * ({@link isSingleKeySigner}, {@link isKeylessSigner}) + * - Concrete account implementations: + * - {@link Ed25519Account} - legacy Ed25519 signing scheme + * - {@link SingleKeyAccount} - unified SingleKey scheme (Ed25519 or Secp256k1) + * - {@link MultiKeyAccount} - M-of-N multi-key signing + * - {@link MultiEd25519Account} - legacy M-of-N Ed25519 signing + * - {@link KeylessAccount} - OIDC-based keyless authentication + * - {@link FederatedKeylessAccount} - keyless with a custom JWK provider + * - {@link AbstractedAccount} - account abstraction via on-chain Move functions + * - {@link DerivableAbstractedAccount} - derivable account abstraction + * - Supporting types: + * - {@link EphemeralKeyPair} - short-lived key pair for keyless flows + * - {@link AbstractKeylessAccount} - shared base class for keyless accounts + * - Factory functions: + * - {@link generateAccount} + * - {@link accountFromPrivateKey} + * - {@link accountFromDerivationPath} + * - {@link authKey} + */ + +export * from "./abstract-keyless-account.js"; +export * from "./abstracted-account.js"; +export * from "./ed25519-account.js"; +export * from "./ephemeral-key-pair.js"; +export * from "./factory.js"; +export * from "./federated-keyless-account.js"; +export * from "./keyless-account.js"; +export * from "./multi-ed25519-account.js"; +export * from "./multi-key-account.js"; +export * from "./single-key-account.js"; +export * from "./types.js"; diff --git a/v10/src/account/keyless-account.ts b/v10/src/account/keyless-account.ts new file mode 100644 index 000000000..0379a2519 --- /dev/null +++ b/v10/src/account/keyless-account.ts @@ -0,0 +1,193 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { Deserializer } from "../bcs/deserializer.js"; +import type { Serializer } from "../bcs/serializer.js"; +import type { AccountAddress } from "../core/account-address.js"; +import { type Groth16VerificationKey, KeylessPublicKey, type ZeroKnowledgeSig } from "../crypto/keyless.js"; +import type { HexInput } from "../hex/index.js"; +import { Hex } from "../hex/index.js"; +import { AbstractKeylessAccount, getIssAudAndUidVal, type ProofFetchCallback } from "./abstract-keyless-account.js"; +import type { EphemeralKeyPair } from "./ephemeral-key-pair.js"; + +/** + * A standard Aptos keyless account backed by an OIDC JWT from a trusted provider. + * + * Keyless accounts allow users to authenticate using an OAuth/OIDC identity + * (e.g. Google, Apple) instead of a traditional private key. The account + * address is derived from the OIDC provider's issuer (`iss`), the application + * identifier (`aud`), and a blinded user identifier, so the user's identity is + * never exposed on-chain. + * + * Use the static {@link KeylessAccount.create} factory for the most ergonomic + * construction; the constructor is available for advanced use cases. + * + * @example + * ```typescript + * const ekp = EphemeralKeyPair.generate(); + * const account = KeylessAccount.create({ + * jwt, + * ephemeralKeyPair: ekp, + * pepper, + * proof: zkProof, + * }); + * await account.checkKeylessAccountValidity(); + * const sig = account.sign(message); + * ``` + */ +export class KeylessAccount extends AbstractKeylessAccount { + /** The {@link KeylessPublicKey} derived from the OIDC parameters. */ + readonly publicKey: KeylessPublicKey; + + /** + * Creates a {@link KeylessAccount} from explicit OIDC claim values. + * + * Prefer {@link KeylessAccount.create} for a higher-level API that extracts + * claims directly from the JWT. + * + * @param args.address - Optional explicit on-chain address; derived from the public key when omitted. + * @param args.ephemeralKeyPair - The short-lived key pair whose public key is committed to in the JWT nonce. + * @param args.iss - The OIDC issuer claim (`iss`) from the JWT. + * @param args.uidKey - The JWT payload claim used as the user identifier. + * @param args.uidVal - The value of `uidKey` in the JWT payload. + * @param args.aud - The audience claim (`aud`) from the JWT. + * @param args.pepper - A 31-byte random value used to blind the user identifier. + * @param args.proof - The zero-knowledge proof, or a promise that will resolve to one. + * @param args.proofFetchCallback - Required when `proof` is a promise; called on completion. + * @param args.jwt - The raw JWT string. + * @param args.verificationKeyHash - Optional 32-byte hash of the Groth16 verification key. + */ + constructor(args: { + address?: AccountAddress; + ephemeralKeyPair: EphemeralKeyPair; + iss: string; + uidKey: string; + uidVal: string; + aud: string; + pepper: HexInput; + proof: ZeroKnowledgeSig | Promise; + proofFetchCallback?: ProofFetchCallback; + jwt: string; + verificationKeyHash?: HexInput; + }) { + const publicKey = KeylessPublicKey.create(args); + super({ publicKey, ...args }); + this.publicKey = publicKey; + } + + /** + * Serializes this account into BCS bytes. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + super.serialize(serializer); + } + + /** + * Deserializes a {@link KeylessAccount} from a BCS byte stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A fully-constructed {@link KeylessAccount}. + */ + static deserialize(deserializer: Deserializer): KeylessAccount { + const { address, proof, ephemeralKeyPair, jwt, uidKey, pepper, verificationKeyHash } = + AbstractKeylessAccount.partialDeserialize(deserializer); + const { iss, aud, uidVal } = getIssAudAndUidVal({ jwt, uidKey }); + return new KeylessAccount({ + address, + proof, + ephemeralKeyPair, + iss, + uidKey, + uidVal, + aud, + pepper, + jwt, + verificationKeyHash, + }); + } + + /** + * Deserializes a {@link KeylessAccount} from a hex-encoded byte string or + * `Uint8Array`. + * + * @param bytes - BCS bytes previously produced by {@link KeylessAccount.serialize}. + * @returns A fully-constructed {@link KeylessAccount}. + */ + static fromBytes(bytes: HexInput): KeylessAccount { + return KeylessAccount.deserialize(new Deserializer(Hex.hexInputToUint8Array(bytes))); + } + + /** + * High-level factory that creates a {@link KeylessAccount} by extracting OIDC + * claims directly from the JWT. + * + * This is the recommended way to construct a {@link KeylessAccount}. + * + * @param args.address - Optional explicit on-chain address. + * @param args.proof - The zero-knowledge proof, or a promise resolving to one. + * @param args.jwt - The raw JWT string from the OIDC provider. + * @param args.ephemeralKeyPair - The short-lived key pair committed to in the JWT nonce. + * @param args.pepper - A 31-byte random value used to blind the user identifier. + * @param args.uidKey - The JWT claim to use as the user identifier. Defaults to `"sub"`. + * @param args.proofFetchCallback - Required when `proof` is a promise; called on completion. + * @param args.verificationKey - The Groth16 verification key (hashed and stored). + * @param args.verificationKeyHash - Pre-computed 32-byte hash of the verification key. + * @returns A new {@link KeylessAccount}. + * + * @throws Error if both `verificationKey` and `verificationKeyHash` are provided. + * + * @example + * ```typescript + * const account = KeylessAccount.create({ + * jwt, + * ephemeralKeyPair, + * pepper, + * proof: zkProof, + * }); + * ``` + */ + static create(args: { + address?: AccountAddress; + proof: ZeroKnowledgeSig | Promise; + jwt: string; + ephemeralKeyPair: EphemeralKeyPair; + pepper: HexInput; + uidKey?: string; + proofFetchCallback?: ProofFetchCallback; + verificationKey?: Groth16VerificationKey; + verificationKeyHash?: Uint8Array; + }): KeylessAccount { + const { + address, + proof, + jwt, + ephemeralKeyPair, + pepper, + uidKey = "sub", + proofFetchCallback, + verificationKey, + verificationKeyHash, + } = args; + + if (verificationKeyHash && verificationKey) { + throw new Error("Cannot provide both verificationKey and verificationKeyHash"); + } + + const { iss, aud, uidVal } = getIssAudAndUidVal({ jwt, uidKey }); + return new KeylessAccount({ + address, + proof, + ephemeralKeyPair, + iss, + uidKey, + uidVal, + aud, + pepper, + jwt, + proofFetchCallback, + verificationKeyHash: verificationKeyHash ?? (verificationKey ? verificationKey.hash() : undefined), + }); + } +} diff --git a/v10/src/account/multi-ed25519-account.ts b/v10/src/account/multi-ed25519-account.ts new file mode 100644 index 000000000..b54055127 --- /dev/null +++ b/v10/src/account/multi-ed25519-account.ts @@ -0,0 +1,177 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { AccountAddress, type AccountAddressInput } from "../core/account-address.js"; +import { AuthenticationKey } from "../core/authentication-key.js"; +import type { Ed25519PrivateKey, Ed25519Signature } from "../crypto/ed25519.js"; +import { type MultiEd25519PublicKey, MultiEd25519Signature } from "../crypto/multi-ed25519.js"; +import { SigningScheme } from "../crypto/types.js"; +import type { HexInput } from "../hex/index.js"; +import { AccountAuthenticatorMultiEd25519 } from "../transactions/authenticator.js"; +import { generateSigningMessageForTransaction } from "../transactions/signing-message.js"; +import type { AnyRawTransaction } from "../transactions/types.js"; +import type { Account } from "./types.js"; + +/** + * Constructor arguments for {@link MultiEd25519Account}. + */ +export interface MultiEd25519SignerConstructorArgs { + /** The {@link MultiEd25519PublicKey} containing all public keys and the threshold. */ + publicKey: MultiEd25519PublicKey; + /** + * The active Ed25519 private keys. The count must equal + * `publicKey.threshold`. + */ + signers: Ed25519PrivateKey[]; + /** + * Optional explicit on-chain address. When omitted the address is derived + * from the MultiEd25519 authentication key. + */ + address?: AccountAddressInput; +} + +/** + * A legacy multi-signature account that requires M-of-N Ed25519 signatures. + * + * This class implements the original Aptos multi-Ed25519 authentication scheme. + * For new accounts, prefer {@link MultiKeyAccount} with individual + * {@link SingleKeyAccount} signers, which supports heterogeneous key types. + * + * The number of private keys provided must exactly equal the + * `publicKey.threshold`. + * + * @example + * ```typescript + * const keys = [Ed25519PrivateKey.generate(), Ed25519PrivateKey.generate()]; + * const multiKey = new MultiEd25519PublicKey({ + * publicKeys: keys.map((k) => k.publicKey()), + * threshold: 2, + * }); + * const account = new MultiEd25519Account({ publicKey: multiKey, signers: keys }); + * ``` + */ +export class MultiEd25519Account implements Account { + /** The {@link MultiEd25519PublicKey} holding all public keys and the threshold. */ + readonly publicKey: MultiEd25519PublicKey; + /** The on-chain address of this account. */ + readonly accountAddress: AccountAddress; + /** Always `SigningScheme.MultiEd25519` for this account type. */ + readonly signingScheme = SigningScheme.MultiEd25519; + /** + * The active private keys, sorted in ascending index order as required by + * on-chain verification. + */ + readonly signers: Ed25519PrivateKey[]; + /** + * The indices of each signer within the public key array, sorted in + * ascending order. + */ + readonly signerIndices: number[]; + /** + * The pre-computed bitmap indicating which key positions have provided a + * signature. + */ + readonly signaturesBitmap: Uint8Array; + + /** + * Creates a {@link MultiEd25519Account}. + * + * @param args - {@link MultiEd25519SignerConstructorArgs} + * + * @throws Error if the number of signers does not match `publicKey.threshold`. + */ + constructor(args: MultiEd25519SignerConstructorArgs) { + const { signers, publicKey, address } = args; + this.publicKey = publicKey; + this.accountAddress = address + ? AccountAddress.from(address) + : AuthenticationKey.fromSchemeAndBytes({ + scheme: SigningScheme.MultiEd25519, + input: this.publicKey.bcsToBytes(), + }).derivedAddress(); + + if (publicKey.threshold > signers.length) { + throw new Error( + `Not enough signers provided to satisfy the required signatures. Need ${publicKey.threshold} signers, but only ${signers.length} provided`, + ); + } else if (publicKey.threshold < signers.length) { + throw new Error( + `More signers provided than required. Need ${publicKey.threshold} signers, but ${signers.length} provided`, + ); + } + + const bitPositions: number[] = []; + for (const signer of signers) { + bitPositions.push(this.publicKey.getIndex(signer.publicKey())); + } + + const signersAndBitPosition: [Ed25519PrivateKey, number][] = signers.map((signer, index) => [ + signer, + bitPositions[index], + ]); + signersAndBitPosition.sort((a, b) => a[1] - b[1]); + + this.signers = signersAndBitPosition.map((value) => value[0]); + this.signerIndices = signersAndBitPosition.map((value) => value[1]); + this.signaturesBitmap = this.publicKey.createBitmap({ bits: this.signerIndices }); + } + + /** + * Verifies that the given multi-Ed25519 signature is valid for the given message. + * + * @param args - An object with the `message` (hex input) and the + * `signature` ({@link MultiEd25519Signature}) to verify. + * @returns `true` if the signature is valid, `false` otherwise. + */ + verifySignature(args: { message: HexInput; signature: MultiEd25519Signature }): boolean { + return this.publicKey.verifySignature(args); + } + + /** + * Signs a message with all active keys and returns an + * {@link AccountAuthenticatorMultiEd25519}. + * + * @param message - The message bytes to sign, in any supported hex input format. + * @returns An {@link AccountAuthenticatorMultiEd25519} ready for use in a transaction. + */ + signWithAuthenticator(message: HexInput): AccountAuthenticatorMultiEd25519 { + return new AccountAuthenticatorMultiEd25519(this.publicKey, this.sign(message)); + } + + /** + * Signs a raw transaction with all active keys and returns an + * {@link AccountAuthenticatorMultiEd25519}. + * + * @param transaction - The raw transaction to sign. + * @returns An {@link AccountAuthenticatorMultiEd25519} containing all signatures. + */ + signTransactionWithAuthenticator(transaction: AnyRawTransaction): AccountAuthenticatorMultiEd25519 { + return new AccountAuthenticatorMultiEd25519(this.publicKey, this.signTransaction(transaction)); + } + + /** + * Signs a message with all active keys and returns the raw + * {@link MultiEd25519Signature}. + * + * @param message - The message bytes to sign, in any supported hex input format. + * @returns A {@link MultiEd25519Signature} containing all individual signatures and the bitmap. + */ + sign(message: HexInput): MultiEd25519Signature { + const signatures = []; + for (const signer of this.signers) { + signatures.push(signer.sign(message) as Ed25519Signature); + } + return new MultiEd25519Signature({ signatures, bitmap: this.signaturesBitmap }); + } + + /** + * Signs a raw transaction with all active keys and returns the raw + * {@link MultiEd25519Signature}. + * + * @param transaction - The raw transaction to sign. + * @returns A {@link MultiEd25519Signature} containing all individual signatures and the bitmap. + */ + signTransaction(transaction: AnyRawTransaction): MultiEd25519Signature { + return this.sign(generateSigningMessageForTransaction(transaction)); + } +} diff --git a/v10/src/account/multi-key-account.ts b/v10/src/account/multi-key-account.ts new file mode 100644 index 000000000..c8ea500a9 --- /dev/null +++ b/v10/src/account/multi-key-account.ts @@ -0,0 +1,253 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { AccountAddress, type AccountAddressInput } from "../core/account-address.js"; +import { AuthenticationKey } from "../core/authentication-key.js"; +import { MultiKey, MultiKeySignature } from "../crypto/multi-key.js"; +import type { PublicKey } from "../crypto/public-key.js"; +import { SigningScheme } from "../crypto/types.js"; +import type { HexInput } from "../hex/index.js"; +import { AccountAuthenticatorMultiKey } from "../transactions/authenticator.js"; +import type { AnyRawTransaction } from "../transactions/types.js"; +import type { AbstractKeylessAccount } from "./abstract-keyless-account.js"; +import { Ed25519Account } from "./ed25519-account.js"; +import { SingleKeyAccount } from "./single-key-account.js"; +import type { Account, KeylessSigner, SingleKeySigner, SingleKeySignerOrLegacyEd25519Account } from "./types.js"; + +/** + * A multi-key account that requires M-of-N signatures to authorise transactions. + * + * Each signer is either a {@link SingleKeyAccount} or a legacy {@link Ed25519Account} + * (which is silently promoted to a {@link SingleKeyAccount} internally). The number + * of signers provided at construction time must equal the `signaturesRequired` + * threshold declared in the accompanying {@link MultiKey}. + * + * Implements {@link KeylessSigner} so that keyless sub-signers are supported. + * + * @example + * ```typescript + * const alice = SingleKeyAccount.generate(); + * const bob = SingleKeyAccount.generate(); + * + * const account = MultiKeyAccount.fromPublicKeysAndSigners({ + * publicKeys: [alice.publicKey.publicKey, bob.publicKey.publicKey], + * signaturesRequired: 2, + * signers: [alice, bob], + * }); + * ``` + */ +export class MultiKeyAccount implements Account, KeylessSigner { + /** The {@link MultiKey} holding the set of public keys and the threshold. */ + readonly publicKey: MultiKey; + /** The on-chain address of this account. */ + readonly accountAddress: AccountAddress; + /** Always `SigningScheme.MultiKey` for this account type. */ + readonly signingScheme: SigningScheme = SigningScheme.MultiKey; + /** + * The active signers, sorted in ascending bit-position order as required by + * on-chain verification. + */ + readonly signers: Account[]; + /** + * The indices of each signer within the {@link MultiKey}'s public key array, + * sorted in ascending order. + */ + readonly signerIndicies: number[]; + /** + * The pre-computed bitmap indicating which key positions in the {@link MultiKey} + * have provided a signature. + */ + readonly signaturesBitmap: Uint8Array; + + /** + * Creates a {@link MultiKeyAccount} from an existing {@link MultiKey} and the + * corresponding active signers. + * + * The number of `signers` must exactly equal `multiKey.signaturesRequired`. + * Legacy {@link Ed25519Account} signers are automatically promoted to + * {@link SingleKeyAccount}. + * + * @param args.multiKey - The {@link MultiKey} containing all public keys and the threshold. + * @param args.signers - The subset of signers (must equal the threshold in count). + * @param args.address - Optional explicit on-chain address; derived from the MultiKey when omitted. + * + * @throws Error if the number of signers does not match `multiKey.signaturesRequired`. + */ + constructor(args: { + multiKey: MultiKey; + signers: SingleKeySignerOrLegacyEd25519Account[]; + address?: AccountAddressInput; + }) { + const { multiKey, address } = args; + + const signers = args.signers.map((signer) => + signer instanceof Ed25519Account ? SingleKeyAccount.fromEd25519Account(signer) : signer, + ); + + if (multiKey.signaturesRequired > signers.length) { + throw new Error( + `Not enough signers provided to satisfy the required signatures. Need ${multiKey.signaturesRequired} signers, but only ${signers.length} provided`, + ); + } else if (multiKey.signaturesRequired < signers.length) { + throw new Error( + `More signers provided than required. Need ${multiKey.signaturesRequired} signers, but ${signers.length} provided`, + ); + } + + this.publicKey = multiKey; + this.accountAddress = address + ? AccountAddress.from(address) + : AuthenticationKey.fromSchemeAndBytes({ + scheme: SigningScheme.MultiKey, + input: this.publicKey.bcsToBytes(), + }).derivedAddress(); + + // For each signer, find its corresponding position in the MultiKey's public keys array + const bitPositions: number[] = []; + for (const signer of signers) { + bitPositions.push(this.publicKey.getIndex((signer as SingleKeySigner).getAnyPublicKey())); + } + + // Sort signers by bit position (on-chain verification requires ascending order) + const signersAndBitPosition: [Account, number][] = signers.map((signer, index) => [ + signer as Account, + bitPositions[index], + ]); + signersAndBitPosition.sort((a, b) => a[1] - b[1]); + + this.signers = signersAndBitPosition.map((value) => value[0]); + this.signerIndicies = signersAndBitPosition.map((value) => value[1]); + this.signaturesBitmap = this.publicKey.createBitmap({ bits: bitPositions }); + } + + /** + * Convenience factory that constructs a {@link MultiKey} from raw public keys + * and a threshold, then creates a {@link MultiKeyAccount} from it. + * + * @param args.publicKeys - The full set of public keys for this multi-key account. + * @param args.signaturesRequired - The minimum number of signatures needed. + * @param args.signers - The active signers (count must equal `signaturesRequired`). + * @param args.address - Optional explicit on-chain address. + * @returns A new {@link MultiKeyAccount}. + * + * @example + * ```typescript + * const account = MultiKeyAccount.fromPublicKeysAndSigners({ + * publicKeys: [alice.publicKey.publicKey, bob.publicKey.publicKey], + * signaturesRequired: 2, + * signers: [alice, bob], + * }); + * ``` + */ + static fromPublicKeysAndSigners(args: { + address?: AccountAddressInput; + publicKeys: PublicKey[]; + signaturesRequired: number; + signers: SingleKeySignerOrLegacyEd25519Account[]; + }): MultiKeyAccount { + const { address, publicKeys, signaturesRequired, signers } = args; + const multiKey = new MultiKey({ publicKeys, signaturesRequired }); + return new MultiKeyAccount({ multiKey, signers, address }); + } + + /** + * Type-guard that checks whether an {@link Account} is a {@link MultiKeyAccount}. + * + * @param account - The account to check. + * @returns `true` if `account` is an instance of {@link MultiKeyAccount}. + */ + static isMultiKeySigner(account: Account): account is MultiKeyAccount { + return account instanceof MultiKeyAccount; + } + + /** + * Waits for any keyless sub-signers to finish fetching their zero-knowledge proofs. + * + * @returns A promise that resolves once all pending proof fetches have completed. + */ + async waitForProofFetch(): Promise { + const keylessSigners = this.signers.filter( + (signer) => "waitForProofFetch" in signer && typeof signer.waitForProofFetch === "function", + ); + await Promise.all(keylessSigners.map(async (signer) => (signer as AbstractKeylessAccount).waitForProofFetch())); + } + + /** + * Validates all keyless sub-signers by calling their `checkKeylessAccountValidity` + * method. + * + * Throws a {@link KeylessError} if any sub-signer is expired or missing a proof. + * + * @param args - Additional arguments forwarded to each keyless signer's validity check. + * @returns A promise that resolves when all keyless signers are valid. + */ + async checkKeylessAccountValidity(...args: unknown[]): Promise { + const keylessSigners = this.signers.filter( + (signer) => "checkKeylessAccountValidity" in signer && typeof signer.checkKeylessAccountValidity === "function", + ); + await Promise.all(keylessSigners.map((signer) => (signer as KeylessSigner).checkKeylessAccountValidity(...args))); + } + + /** + * Signs a message with all active signers and returns an + * {@link AccountAuthenticatorMultiKey}. + * + * @param message - The message bytes to sign, in any supported hex input format. + * @returns An {@link AccountAuthenticatorMultiKey} containing all signatures and the bitmap. + */ + signWithAuthenticator(message: HexInput): AccountAuthenticatorMultiKey { + return new AccountAuthenticatorMultiKey(this.publicKey, this.sign(message)); + } + + /** + * Signs a raw transaction with all active signers and returns an + * {@link AccountAuthenticatorMultiKey}. + * + * @param transaction - The raw transaction to sign. + * @returns An {@link AccountAuthenticatorMultiKey} containing all signatures and the bitmap. + */ + signTransactionWithAuthenticator(transaction: AnyRawTransaction): AccountAuthenticatorMultiKey { + return new AccountAuthenticatorMultiKey(this.publicKey, this.signTransaction(transaction)); + } + + /** + * Signs a message with all active signers and returns the raw + * {@link MultiKeySignature}. + * + * @param data - The message bytes to sign, in any supported hex input format. + * @returns A {@link MultiKeySignature} containing all individual signatures and the bitmap. + */ + sign(data: HexInput): MultiKeySignature { + const signatures = []; + for (const signer of this.signers) { + signatures.push(signer.sign(data)); + } + return new MultiKeySignature({ signatures, bitmap: this.signaturesBitmap }); + } + + /** + * Signs a raw transaction with all active signers and returns the raw + * {@link MultiKeySignature}. + * + * @param transaction - The raw transaction to sign. + * @returns A {@link MultiKeySignature} containing all individual signatures and the bitmap. + */ + signTransaction(transaction: AnyRawTransaction): MultiKeySignature { + const signatures = []; + for (const signer of this.signers) { + signatures.push(signer.signTransaction(transaction)); + } + return new MultiKeySignature({ signatures, bitmap: this.signaturesBitmap }); + } + + /** + * Verifies that the given multi-key signature is valid for the given message. + * + * @param args - An object containing the `message` (hex input) and the + * `signature` ({@link MultiKeySignature}) to verify. + * @returns `true` if the signature is valid, `false` otherwise. + */ + verifySignature(args: { message: HexInput; signature: MultiKeySignature }): boolean { + return this.publicKey.verifySignature(args); + } +} diff --git a/v10/src/account/single-key-account.ts b/v10/src/account/single-key-account.ts new file mode 100644 index 000000000..44af6d41e --- /dev/null +++ b/v10/src/account/single-key-account.ts @@ -0,0 +1,237 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { AccountAddress, type AccountAddressInput } from "../core/account-address.js"; +import { AuthenticationKey } from "../core/authentication-key.js"; +import { Ed25519PrivateKey } from "../crypto/ed25519.js"; +import type { PrivateKey } from "../crypto/private-key.js"; +import { Secp256k1PrivateKey } from "../crypto/secp256k1.js"; +import { AnyPublicKey, AnySignature, type PrivateKeyInput } from "../crypto/single-key.js"; +import { SigningScheme, SigningSchemeInput } from "../crypto/types.js"; +import type { HexInput } from "../hex/index.js"; +import { AccountAuthenticatorSingleKey } from "../transactions/authenticator.js"; +import { generateSigningMessageForTransaction } from "../transactions/signing-message.js"; +import type { AnyRawTransaction } from "../transactions/types.js"; +import type { Ed25519Account } from "./ed25519-account.js"; +import type { Account, SingleKeySigner } from "./types.js"; + +/** + * Constructor arguments for {@link SingleKeyAccount}. + */ +export interface SingleKeySignerConstructorArgs { + /** Any supported private key (Ed25519 or Secp256k1). */ + privateKey: PrivateKeyInput; + /** + * Optional explicit on-chain address. When omitted the address is derived + * from the SingleKey authentication key. + */ + address?: AccountAddressInput; +} + +/** + * Arguments for {@link SingleKeyAccount.generate}. + */ +export interface SingleKeySignerGenerateArgs { + /** + * The signing scheme to use when generating the key pair. + * Defaults to `SigningSchemeInput.Ed25519`. + */ + scheme?: SigningSchemeInput; +} + +/** + * Arguments for {@link SingleKeyAccount.fromDerivationPath}. + * + * Combines {@link SingleKeySignerGenerateArgs} with BIP-39/BIP-44 derivation + * path parameters. + */ +export type SingleKeySignerFromDerivationPathArgs = SingleKeySignerGenerateArgs & { + /** BIP-44 derivation path string (e.g. `"m/44'/637'/0'/0'/0'"`). */ + path: string; + /** Space-separated BIP-39 mnemonic phrase. */ + mnemonic: string; +}; + +/** + * An account that uses the unified `SingleKey` authentication scheme. + * + * Unlike the legacy {@link Ed25519Account}, this account wraps its public key + * in an {@link AnyPublicKey} envelope, enabling support for multiple key types + * (Ed25519, Secp256k1) under a single on-chain scheme. + * + * @example + * ```typescript + * // Generate a new SingleKey account backed by Ed25519 + * const account = SingleKeyAccount.generate(); + * + * // Generate a SingleKey account backed by Secp256k1 + * const secp = SingleKeyAccount.generate({ scheme: SigningSchemeInput.Secp256k1Ecdsa }); + * + * // Wrap an existing private key + * const key = new Ed25519PrivateKey("0xabc..."); + * const account2 = new SingleKeyAccount({ privateKey: key }); + * ``` + */ +export class SingleKeyAccount implements Account, SingleKeySigner { + /** The underlying private key (Ed25519 or Secp256k1). */ + readonly privateKey: PrivateKey; + /** The {@link AnyPublicKey} wrapper around this account's public key. */ + readonly publicKey: AnyPublicKey; + /** The on-chain address of this account. */ + readonly accountAddress: AccountAddress; + /** Always `SigningScheme.SingleKey` for this account type. */ + readonly signingScheme = SigningScheme.SingleKey; + + /** + * Creates a {@link SingleKeyAccount} from an existing private key and an + * optional explicit address. + * + * @param args - {@link SingleKeySignerConstructorArgs} + */ + constructor(args: SingleKeySignerConstructorArgs) { + const { privateKey, address } = args; + this.privateKey = privateKey; + this.publicKey = new AnyPublicKey(privateKey.publicKey()); + this.accountAddress = address + ? AccountAddress.from(address) + : AuthenticationKey.fromSchemeAndBytes({ + scheme: SigningScheme.SingleKey, + input: this.publicKey.bcsToBytes(), + }).derivedAddress(); + } + + /** + * Returns the {@link AnyPublicKey} associated with this account. + * + * @returns The {@link AnyPublicKey} wrapping the underlying public key. + */ + getAnyPublicKey(): AnyPublicKey { + return this.publicKey; + } + + /** + * Generates a new {@link SingleKeyAccount} with a randomly generated private key. + * + * @param args - Optional {@link SingleKeySignerGenerateArgs} to specify the key scheme. + * @returns A new {@link SingleKeyAccount} instance. + * + * @example + * ```typescript + * const account = SingleKeyAccount.generate(); + * const secp = SingleKeyAccount.generate({ scheme: SigningSchemeInput.Secp256k1Ecdsa }); + * ``` + */ + static generate(args: SingleKeySignerGenerateArgs = {}): SingleKeyAccount { + const { scheme = SigningSchemeInput.Ed25519 } = args; + let privateKey: PrivateKeyInput; + switch (scheme) { + case SigningSchemeInput.Ed25519: + privateKey = Ed25519PrivateKey.generate(); + break; + case SigningSchemeInput.Secp256k1Ecdsa: + privateKey = Secp256k1PrivateKey.generate(); + break; + default: + throw new Error(`Unsupported signature scheme ${scheme}`); + } + return new SingleKeyAccount({ privateKey }); + } + + /** + * Derives a {@link SingleKeyAccount} from a BIP-44 derivation path and a + * BIP-39 mnemonic phrase. + * + * @param args - {@link SingleKeySignerFromDerivationPathArgs} + * @returns A deterministic {@link SingleKeyAccount} for the given path and mnemonic. + * + * @example + * ```typescript + * const account = SingleKeyAccount.fromDerivationPath({ + * path: "m/44'/637'/0'/0'/0'", + * mnemonic: "word1 word2 ... word12", + * }); + * ``` + */ + static fromDerivationPath(args: SingleKeySignerFromDerivationPathArgs): SingleKeyAccount { + const { scheme = SigningSchemeInput.Ed25519, path, mnemonic } = args; + let privateKey: PrivateKeyInput; + switch (scheme) { + case SigningSchemeInput.Ed25519: + privateKey = Ed25519PrivateKey.fromDerivationPath(path, mnemonic); + break; + case SigningSchemeInput.Secp256k1Ecdsa: + privateKey = Secp256k1PrivateKey.fromDerivationPath(path, mnemonic); + break; + default: + throw new Error(`Unsupported signature scheme ${scheme}`); + } + return new SingleKeyAccount({ privateKey }); + } + + /** + * Wraps a legacy {@link Ed25519Account} in a {@link SingleKeyAccount} using the + * same underlying private key and address. + * + * Useful when migrating from the legacy Ed25519 scheme to the SingleKey scheme + * without changing the account's on-chain address. + * + * @param account - The legacy {@link Ed25519Account} to convert. + * @returns A new {@link SingleKeyAccount} with the same key and address. + */ + static fromEd25519Account(account: Ed25519Account): SingleKeyAccount { + return new SingleKeyAccount({ privateKey: account.privateKey, address: account.accountAddress }); + } + + /** + * Verifies that the given signature is valid for the given message under this + * account's public key. + * + * @param args - An object containing the `message` (hex input) and the + * `signature` ({@link AnySignature}) to verify. + * @returns `true` if the signature is valid, `false` otherwise. + */ + verifySignature(args: { message: HexInput; signature: AnySignature }): boolean { + return this.publicKey.verifySignature(args); + } + + /** + * Signs a message and returns an {@link AccountAuthenticatorSingleKey} wrapping + * the public key and the signature. + * + * @param message - The message bytes to sign, in any supported hex input format. + * @returns An {@link AccountAuthenticatorSingleKey} ready for use in a transaction. + */ + signWithAuthenticator(message: HexInput): AccountAuthenticatorSingleKey { + return new AccountAuthenticatorSingleKey(this.publicKey, this.sign(message)); + } + + /** + * Signs a raw transaction and returns an {@link AccountAuthenticatorSingleKey}. + * + * @param transaction - The raw transaction to sign. + * @returns An {@link AccountAuthenticatorSingleKey} containing the signature. + */ + signTransactionWithAuthenticator(transaction: AnyRawTransaction): AccountAuthenticatorSingleKey { + return new AccountAuthenticatorSingleKey(this.publicKey, this.signTransaction(transaction)); + } + + /** + * Signs a message and returns the raw {@link AnySignature}. + * + * @param message - The message bytes to sign, in any supported hex input format. + * @returns The {@link AnySignature} over the message. + */ + sign(message: HexInput): AnySignature { + return new AnySignature(this.privateKey.sign(message)); + } + + /** + * Signs a raw transaction and returns the raw {@link AnySignature}. + * + * @param transaction - The raw transaction to sign. + * @returns The {@link AnySignature} over the transaction signing message. + */ + signTransaction(transaction: AnyRawTransaction): AnySignature { + return this.sign(generateSigningMessageForTransaction(transaction)); + } +} diff --git a/v10/src/account/types.ts b/v10/src/account/types.ts new file mode 100644 index 000000000..2142aa608 --- /dev/null +++ b/v10/src/account/types.ts @@ -0,0 +1,269 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { AccountAddress, AccountAddressInput } from "../core/account-address.js"; +import type { Ed25519PrivateKey } from "../crypto/ed25519.js"; +import type { AccountPublicKey } from "../crypto/public-key.js"; +import type { Signature } from "../crypto/signature.js"; +import type { AnyPublicKey, PrivateKeyInput } from "../crypto/single-key.js"; +import type { SigningScheme, SigningSchemeInput } from "../crypto/types.js"; +import type { HexInput } from "../hex/index.js"; +import type { AccountAuthenticator } from "../transactions/authenticator.js"; +import type { AnyRawTransaction } from "../transactions/types.js"; + +// ── Account interface ── + +/** + * Core interface that every Aptos account implementation must satisfy. + * + * An account holds a public key and an on-chain address, and is capable of + * signing arbitrary messages as well as raw transactions in both bare-signature + * and authenticator-wrapped forms. + * + * @example + * ```typescript + * const account: Account = Ed25519Account.generate(); + * const sig = account.sign(new TextEncoder().encode("hello")); + * ``` + */ +export interface Account { + /** The public key associated with this account. */ + readonly publicKey: AccountPublicKey; + /** The on-chain address of this account. */ + readonly accountAddress: AccountAddress; + /** The signing scheme used by this account (e.g. Ed25519, SingleKey, MultiKey). */ + readonly signingScheme: SigningScheme; + + /** + * Signs a raw message and wraps the result in an {@link AccountAuthenticator}. + * + * @param message - The message bytes to sign, in any supported hex input format. + * @returns An {@link AccountAuthenticator} containing the public key and signature. + */ + signWithAuthenticator(message: HexInput): AccountAuthenticator; + + /** + * Signs a raw transaction and wraps the result in an {@link AccountAuthenticator}. + * + * @param transaction - The raw transaction to sign. + * @returns An {@link AccountAuthenticator} containing the public key and signature. + */ + signTransactionWithAuthenticator(transaction: AnyRawTransaction): AccountAuthenticator; + + /** + * Signs a raw message and returns the bare {@link Signature}. + * + * @param message - The message bytes to sign, in any supported hex input format. + * @returns The raw {@link Signature} over the message. + */ + sign(message: HexInput): Signature; + + /** + * Signs a raw transaction and returns the bare {@link Signature}. + * + * @param transaction - The raw transaction to sign. + * @returns The raw {@link Signature} over the transaction signing message. + */ + signTransaction(transaction: AnyRawTransaction): Signature; +} + +// ── SingleKeySigner ── + +/** + * Extension of {@link Account} for accounts whose on-chain representation uses + * the `SingleKey` authentication scheme. + * + * Exposes `getAnyPublicKey()` to retrieve the wrapped {@link AnyPublicKey}. + */ +export interface SingleKeySigner extends Account { + /** + * Returns the {@link AnyPublicKey} wrapper around this account's public key. + * + * @returns The {@link AnyPublicKey} associated with this signer. + */ + getAnyPublicKey(): AnyPublicKey; +} + +/** + * Type-guard that checks whether an unknown value implements the + * {@link SingleKeySigner} interface. + * + * @param obj - The value to test. + * @returns `true` if `obj` has a callable `getAnyPublicKey` method. + */ +export function isSingleKeySigner(obj: unknown): obj is SingleKeySigner { + return ( + typeof obj === "object" && + obj !== null && + "getAnyPublicKey" in obj && + typeof (obj as Record).getAnyPublicKey === "function" + ); +} + +// ── KeylessSigner ── + +/** + * Extension of {@link Account} for keyless accounts (both standard and federated). + * + * Adds `checkKeylessAccountValidity()` which validates that the ephemeral key + * has not expired and that a zero-knowledge proof is available before signing. + */ +export interface KeylessSigner extends Account { + /** + * Validates the keyless account state prior to signing. + * + * Throws a {@link KeylessError} if the ephemeral key pair has expired or if + * the proof has not yet been fetched. + * + * @param args - Optional additional arguments accepted by concrete implementations. + * @returns A promise that resolves when the account is valid, or rejects with a + * {@link KeylessError}. + */ + checkKeylessAccountValidity(...args: unknown[]): Promise; +} + +/** + * Type-guard that checks whether an unknown value implements the + * {@link KeylessSigner} interface. + * + * @param obj - The value to test. + * @returns `true` if `obj` has a callable `checkKeylessAccountValidity` method. + */ +export function isKeylessSigner(obj: unknown): obj is KeylessSigner { + return ( + typeof obj === "object" && + obj !== null && + "checkKeylessAccountValidity" in obj && + typeof (obj as Record).checkKeylessAccountValidity === "function" + ); +} + +// ── Argument types for Account factory methods ── + +/** + * Arguments for creating a legacy Ed25519 account from a private key. + * + * The resulting account uses the legacy Ed25519 signing scheme and derives its + * address from the Ed25519 authentication key. + */ +export interface CreateEd25519AccountFromPrivateKeyArgs { + /** The Ed25519 private key to use. */ + privateKey: Ed25519PrivateKey; + /** Optional explicit account address; defaults to the derived authentication key address. */ + address?: AccountAddressInput; + /** When `true` (the default), creates a legacy Ed25519 account. */ + legacy?: true; +} + +/** + * Arguments for creating a SingleKey-scheme Ed25519 account from a private key. + * + * Setting `legacy: false` opts into the unified `SingleKey` authentication scheme + * even though the underlying key type is Ed25519. + */ +export interface CreateEd25519SingleKeyAccountFromPrivateKeyArgs { + /** The Ed25519 private key to use. */ + privateKey: Ed25519PrivateKey; + /** Optional explicit account address; defaults to the derived authentication key address. */ + address?: AccountAddressInput; + /** Must be `false` to select the SingleKey scheme for an Ed25519 key. */ + legacy: false; +} + +/** + * Arguments for creating a SingleKey account from any supported private key type. + * + * Use this when `privateKey` is not an {@link Ed25519PrivateKey} or when you + * explicitly want the `SingleKey` authentication scheme. + */ +export interface CreateSingleKeyAccountFromPrivateKeyArgs { + /** Any supported private key (Ed25519 or Secp256k1). */ + privateKey: PrivateKeyInput; + /** Optional explicit account address; defaults to the derived authentication key address. */ + address?: AccountAddressInput; + /** Must be `false` (or omitted) to use the SingleKey scheme. */ + legacy?: false; +} + +/** + * General-purpose arguments accepted by the {@link accountFromPrivateKey} factory. + * + * Prefer the more specific overload argument types when the exact output type + * matters, and use this interface only when the distinction is dynamic. + */ +export interface CreateAccountFromPrivateKeyArgs { + /** Any supported private key (Ed25519 or Secp256k1). */ + privateKey: PrivateKeyInput; + /** Optional explicit account address; defaults to the derived authentication key address. */ + address?: AccountAddressInput; + /** Whether to use the legacy Ed25519 scheme (`true`) or the SingleKey scheme (`false`). */ + legacy?: boolean; +} + +/** + * Arguments for generating a new legacy Ed25519 account. + */ +export interface GenerateEd25519AccountArgs { + /** Must be `SigningSchemeInput.Ed25519` or omitted. */ + scheme?: SigningSchemeInput.Ed25519; + /** When `true` (the default), generates a legacy Ed25519 account. */ + legacy?: true; +} + +/** + * Arguments for generating a new SingleKey-scheme Ed25519 account. + * + * Setting `legacy: false` opts into the `SingleKey` authentication scheme. + */ +export interface GenerateEd25519SingleKeyAccountArgs { + /** Must be `SigningSchemeInput.Ed25519` or omitted. */ + scheme?: SigningSchemeInput.Ed25519; + /** Must be `false` to select the SingleKey scheme. */ + legacy: false; +} + +/** + * Arguments for generating a new SingleKey account with a non-Ed25519 scheme. + * + * Use this when you want to generate an account backed by e.g. Secp256k1. + */ +export interface GenerateSingleKeyAccountArgs { + /** A signing scheme that is not Ed25519 (e.g. `SigningSchemeInput.Secp256k1Ecdsa`). */ + scheme: Exclude; + /** Must be `false` (or omitted) to use the SingleKey scheme. */ + legacy?: false; +} + +/** + * General-purpose arguments accepted by the {@link generateAccount} factory. + * + * Prefer the more specific overload argument types when the exact output type + * matters, and use this interface only when the distinction is dynamic. + */ +export interface GenerateAccountArgs { + /** The desired signing scheme. Defaults to `SigningSchemeInput.Ed25519`. */ + scheme?: SigningSchemeInput; + /** Whether to use the legacy Ed25519 scheme (`true`) or the SingleKey scheme (`false`). */ + legacy?: boolean; +} + +/** + * Arguments for deriving a private key from a BIP-39 mnemonic phrase and a + * BIP-44 derivation path. + */ +export interface PrivateKeyFromDerivationPathArgs { + /** BIP-44 derivation path string (e.g. `"m/44'/637'/0'/0'/0'"`). */ + path: string; + /** Space-separated BIP-39 mnemonic phrase. */ + mnemonic: string; +} + +// ── Union types ── + +/** + * Union of {@link SingleKeySigner} and the legacy {@link import("./ed25519-account.js").Ed25519Account}. + * + * Used in places such as {@link MultiKeyAccount} where both account types can + * serve as individual signers within a multi-key setup. + */ +export type SingleKeySignerOrLegacyEd25519Account = SingleKeySigner | import("./ed25519-account.js").Ed25519Account; diff --git a/v10/src/api/account.ts b/v10/src/api/account.ts new file mode 100644 index 000000000..2a31052a1 --- /dev/null +++ b/v10/src/api/account.ts @@ -0,0 +1,170 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { AnyNumber } from "../bcs/types.js"; +import { get, paginateWithCursor, paginateWithObfuscatedCursor } from "../client/get.js"; +import type { AccountAddressInput } from "../core/account-address.js"; +import { AccountAddress } from "../core/account-address.js"; +import { AptosApiType } from "../core/constants.js"; +import type { AptosConfig } from "./config.js"; +import type { + AccountData, + CommittedTransactionResponse, + MoveModuleBytecode, + MoveResource, + MoveStructId, +} from "./types.js"; + +/** + * Retrieves core account information including the sequence number and authentication key. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param accountAddress - The address of the account to query. + * @returns The account data containing sequence number and authentication key. + */ +export async function getAccountInfo(config: AptosConfig, accountAddress: AccountAddressInput): Promise { + const url = config.getRequestUrl(AptosApiType.FULLNODE); + const response = await get({ + url, + apiType: AptosApiType.FULLNODE, + path: `accounts/${AccountAddress.from(accountAddress)}`, + originMethod: "getAccountInfo", + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); + return response.data; +} + +/** + * Retrieves all Move modules published under the specified account. Results are paginated automatically. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param accountAddress - The address of the account whose modules to retrieve. + * @param options - Optional parameters. + * @param options.limit - Maximum number of modules per page. Defaults to 1000. + * @param options.ledgerVersion - The ledger version to query at. Defaults to the latest version. + * @returns An array of all Move modules published under the account. + */ +export async function getAccountModules( + config: AptosConfig, + accountAddress: AccountAddressInput, + options?: { limit?: number; ledgerVersion?: AnyNumber }, +): Promise { + const url = config.getRequestUrl(AptosApiType.FULLNODE); + return paginateWithObfuscatedCursor({ + url, + apiType: AptosApiType.FULLNODE, + path: `accounts/${AccountAddress.from(accountAddress)}/modules`, + originMethod: "getAccountModules", + params: { limit: options?.limit ?? 1000, ledger_version: options?.ledgerVersion }, + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); +} + +/** + * Retrieves a single Move module by name from the specified account. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param accountAddress - The address of the account that published the module. + * @param moduleName - The name of the module to retrieve. + * @param options - Optional parameters. + * @param options.ledgerVersion - The ledger version to query at. Defaults to the latest version. + * @returns The Move module bytecode and ABI. + */ +export async function getAccountModule( + config: AptosConfig, + accountAddress: AccountAddressInput, + moduleName: string, + options?: { ledgerVersion?: AnyNumber }, +): Promise { + const url = config.getRequestUrl(AptosApiType.FULLNODE); + const response = await get({ + url, + apiType: AptosApiType.FULLNODE, + path: `accounts/${AccountAddress.from(accountAddress)}/module/${moduleName}`, + originMethod: "getAccountModule", + params: { ledger_version: options?.ledgerVersion }, + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); + return response.data; +} + +/** + * Retrieves a specific Move resource by type from the specified account. + * @typeParam T - The expected shape of the resource data. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param accountAddress - The address of the account holding the resource. + * @param resourceType - The fully qualified Move struct type of the resource (e.g. `"0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>"`). + * @param options - Optional parameters. + * @param options.ledgerVersion - The ledger version to query at. Defaults to the latest version. + * @returns The deserialized resource data. + */ +export async function getAccountResource( + config: AptosConfig, + accountAddress: AccountAddressInput, + resourceType: MoveStructId, + options?: { ledgerVersion?: AnyNumber }, +): Promise { + const url = config.getRequestUrl(AptosApiType.FULLNODE); + const response = await get>({ + url, + apiType: AptosApiType.FULLNODE, + path: `accounts/${AccountAddress.from(accountAddress)}/resource/${resourceType}`, + originMethod: "getAccountResource", + params: { ledger_version: options?.ledgerVersion }, + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); + return response.data.data; +} + +/** + * Retrieves all Move resources stored under the specified account. Results are paginated automatically. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param accountAddress - The address of the account whose resources to retrieve. + * @param options - Optional parameters. + * @param options.limit - Maximum number of resources per page. Defaults to 1000. + * @param options.ledgerVersion - The ledger version to query at. Defaults to the latest version. + * @returns An array of all Move resources stored under the account. + */ +export async function getAccountResources( + config: AptosConfig, + accountAddress: AccountAddressInput, + options?: { limit?: number; ledgerVersion?: AnyNumber }, +): Promise { + const url = config.getRequestUrl(AptosApiType.FULLNODE); + return paginateWithObfuscatedCursor({ + url, + apiType: AptosApiType.FULLNODE, + path: `accounts/${AccountAddress.from(accountAddress)}/resources`, + originMethod: "getAccountResources", + params: { limit: options?.limit ?? 1000, ledger_version: options?.ledgerVersion }, + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); +} + +/** + * Retrieves transactions sent by the specified account. Results are paginated automatically. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param accountAddress - The address of the account whose transactions to retrieve. + * @param options - Optional parameters. + * @param options.offset - The sequence number to start listing transactions from. + * @param options.limit - Maximum number of transactions to return per page. + * @returns An array of committed transactions sent by the account. + */ +export async function getAccountTransactions( + config: AptosConfig, + accountAddress: AccountAddressInput, + options?: { offset?: AnyNumber; limit?: number }, +): Promise { + const url = config.getRequestUrl(AptosApiType.FULLNODE); + return paginateWithCursor({ + url, + apiType: AptosApiType.FULLNODE, + path: `accounts/${AccountAddress.from(accountAddress)}/transactions`, + originMethod: "getAccountTransactions", + params: { start: options?.offset, limit: options?.limit }, + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); +} diff --git a/v10/src/api/coin.ts b/v10/src/api/coin.ts new file mode 100644 index 000000000..d32b61914 --- /dev/null +++ b/v10/src/api/coin.ts @@ -0,0 +1,38 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { U64 } from "../bcs/move-primitives.js"; +import type { AnyNumber } from "../bcs/types.js"; +import type { AccountAddressInput } from "../core/account-address.js"; +import { AccountAddress } from "../core/account-address.js"; +import type { SimpleTransaction } from "../transactions/simple-transaction.js"; +import type { AptosConfig } from "./config.js"; +import { type BuildSimpleTransactionOptions, buildSimpleTransaction } from "./transaction.js"; + +/** + * Builds a transaction to transfer APT coins from one account to another using `0x1::aptos_account::transfer_coins`. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param sender - The address of the account sending the coins. + * @param recipient - The address of the account receiving the coins. + * @param amount - The amount of coins to transfer in Octas (1 APT = 10^8 Octas). + * @param options - Optional transaction building parameters (gas, expiration, sequence number). + * @returns A {@link SimpleTransaction} ready to be signed and submitted. + */ +export async function transferCoinTransaction( + config: AptosConfig, + sender: AccountAddressInput, + recipient: AccountAddressInput, + amount: AnyNumber, + options?: BuildSimpleTransactionOptions, +): Promise { + return buildSimpleTransaction( + config, + sender, + { + function: `0x1::aptos_account::transfer_coins`, + typeArguments: [], + functionArguments: [AccountAddress.from(recipient), new U64(amount)], + }, + options, + ); +} diff --git a/v10/src/api/config.ts b/v10/src/api/config.ts new file mode 100644 index 000000000..7f8b7aa47 --- /dev/null +++ b/v10/src/api/config.ts @@ -0,0 +1,224 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { Client, ClientConfig, FaucetConfig, FullNodeConfig, IndexerConfig } from "../client/types.js"; +import { AptosApiType, DEFAULT_MAX_GAS_AMOUNT, DEFAULT_TXN_EXP_SEC_FROM_NOW } from "../core/constants.js"; +import { + Network, + NetworkToFaucetAPI, + NetworkToIndexerAPI, + NetworkToNodeAPI, + NetworkToPepperAPI, + NetworkToProverAPI, +} from "../core/network.js"; + +/** + * Configuration options for creating an {@link AptosConfig} instance. + * All fields are optional; sensible defaults are used when omitted. + */ +export interface AptosSettings { + /** The Aptos network to connect to. Defaults to `Network.DEVNET`. */ + network?: Network; + /** Custom fullnode REST API URL. Requires `network` to be specified. */ + fullnode?: string; + /** Custom faucet API URL. Requires `network` to be specified. */ + faucet?: string; + /** Custom pepper service URL for keyless accounts. Requires `network` to be specified. */ + pepper?: string; + /** Custom prover service URL for keyless accounts. Requires `network` to be specified. */ + prover?: string; + /** Custom indexer API URL. Requires `network` to be specified. */ + indexer?: string; + /** Custom HTTP client to replace the default `@aptos-labs/aptos-client` transport. */ + client?: Client; + /** Global client configuration applied to all API requests (headers, API key, etc.). */ + clientConfig?: ClientConfig; + /** Client configuration overrides specific to fullnode requests. */ + fullnodeConfig?: FullNodeConfig; + /** Client configuration overrides specific to indexer requests. */ + indexerConfig?: IndexerConfig; + /** Client configuration overrides specific to faucet requests. */ + faucetConfig?: FaucetConfig; + /** Default maximum gas amount for transactions. Defaults to {@link DEFAULT_MAX_GAS_AMOUNT}. */ + defaultMaxGasAmount?: number; + /** Default transaction expiration in seconds from now. Defaults to {@link DEFAULT_TXN_EXP_SEC_FROM_NOW}. */ + defaultTxnExpSecFromNow?: number; +} + +/** + * Holds the resolved configuration for interacting with the Aptos blockchain. + * Resolves endpoint URLs based on the chosen network and optional custom overrides. + */ +export class AptosConfig { + /** The Aptos network this config targets. */ + readonly network: Network; + /** Custom fullnode REST API URL, if provided. */ + readonly fullnode?: string; + /** Custom faucet API URL, if provided. */ + readonly faucet?: string; + /** Custom pepper service URL, if provided. */ + readonly pepper?: string; + /** Custom prover service URL, if provided. */ + readonly prover?: string; + /** Custom indexer API URL, if provided. */ + readonly indexer?: string; + /** Custom HTTP client to replace the default transport. */ + readonly client?: Client; + /** Global client configuration applied to all API requests. */ + readonly clientConfig?: ClientConfig; + /** Client configuration overrides for fullnode requests. */ + readonly fullnodeConfig?: FullNodeConfig; + /** Client configuration overrides for indexer requests. */ + readonly indexerConfig?: IndexerConfig; + /** Client configuration overrides for faucet requests. */ + readonly faucetConfig?: FaucetConfig; + /** Default maximum gas amount for transactions. */ + readonly defaultMaxGasAmount: number; + /** Default transaction expiration in seconds from now. */ + readonly defaultTxnExpSecFromNow: number; + + /** + * Creates a new AptosConfig instance. + * @param settings - Optional configuration settings. If custom endpoint URLs are provided, + * the `network` field must also be specified. + */ + constructor(settings?: AptosSettings) { + if (settings?.fullnode || settings?.indexer || settings?.faucet || settings?.pepper || settings?.prover) { + if (!settings?.network) { + throw new Error("Custom endpoints require a network to be specified"); + } + } + + this.network = settings?.network ?? Network.DEVNET; + this.fullnode = settings?.fullnode; + this.faucet = settings?.faucet; + this.pepper = settings?.pepper; + this.prover = settings?.prover; + this.indexer = settings?.indexer; + this.client = settings?.client; + this.clientConfig = settings?.clientConfig; + this.fullnodeConfig = settings?.fullnodeConfig; + this.indexerConfig = settings?.indexerConfig; + this.faucetConfig = settings?.faucetConfig; + this.defaultMaxGasAmount = settings?.defaultMaxGasAmount ?? DEFAULT_MAX_GAS_AMOUNT; + this.defaultTxnExpSecFromNow = settings?.defaultTxnExpSecFromNow ?? DEFAULT_TXN_EXP_SEC_FROM_NOW; + } + + /** + * Resolves the base URL for a given API type, using custom overrides or network defaults. + * @param apiType - The type of API endpoint to resolve (fullnode, faucet, indexer, etc.). + * @returns The resolved base URL string. + * @throws Error if a custom URL is required but not provided. + */ + getRequestUrl(apiType: AptosApiType): string { + switch (apiType) { + case AptosApiType.FULLNODE: + if (this.fullnode !== undefined) return this.fullnode; + if (this.network === Network.CUSTOM) throw new Error("Please provide a custom full node url"); + return ( + NetworkToNodeAPI[this.network] ?? + (() => { + throw new Error(`No fullnode URL configured for network ${this.network}`); + })() + ); + case AptosApiType.FAUCET: + if (this.faucet !== undefined) return this.faucet; + if (this.network === Network.TESTNET) { + throw new Error( + "There is no way to programmatically mint testnet APT, you must use the minting site at https://aptos.dev/network/faucet", + ); + } + if (this.network === Network.MAINNET) throw new Error("There is no mainnet faucet"); + if (this.network === Network.CUSTOM) throw new Error("Please provide a custom faucet url"); + return ( + NetworkToFaucetAPI[this.network] ?? + (() => { + throw new Error(`No faucet URL configured for network ${this.network}`); + })() + ); + case AptosApiType.INDEXER: + if (this.indexer !== undefined) return this.indexer; + if (this.network === Network.CUSTOM) throw new Error("Please provide a custom indexer url"); + return ( + NetworkToIndexerAPI[this.network] ?? + (() => { + throw new Error(`No indexer URL configured for network ${this.network}`); + })() + ); + case AptosApiType.PEPPER: + if (this.pepper !== undefined) return this.pepper; + if (this.network === Network.CUSTOM) throw new Error("Please provide a custom pepper service url"); + return ( + NetworkToPepperAPI[this.network] ?? + (() => { + throw new Error(`No pepper service URL configured for network ${this.network}`); + })() + ); + case AptosApiType.PROVER: + if (this.prover !== undefined) return this.prover; + if (this.network === Network.CUSTOM) throw new Error("Please provide a custom prover service url"); + return ( + NetworkToProverAPI[this.network] ?? + (() => { + throw new Error(`No prover service URL configured for network ${this.network}`); + })() + ); + default: + throw new Error(`apiType ${apiType as string} is not supported`); + } + } + + /** + * Returns the merged client configuration for fullnode requests, combining global, fullnode-specific, and per-call overrides. + * @param overrides - Optional per-call client configuration overrides. + * @returns The merged client configuration. + */ + getMergedFullnodeConfig(overrides?: ClientConfig): ClientConfig { + return { + ...this.clientConfig, + ...this.fullnodeConfig, + ...overrides, + HEADERS: { ...this.clientConfig?.HEADERS, ...this.fullnodeConfig?.HEADERS, ...overrides?.HEADERS }, + }; + } + + /** + * Returns the merged client configuration for indexer requests, combining global, indexer-specific, and per-call overrides. + * @param overrides - Optional per-call client configuration overrides. + * @returns The merged client configuration. + */ + getMergedIndexerConfig(overrides?: ClientConfig): ClientConfig { + return { + ...this.clientConfig, + ...this.indexerConfig, + ...overrides, + HEADERS: { ...this.clientConfig?.HEADERS, ...this.indexerConfig?.HEADERS, ...overrides?.HEADERS }, + }; + } + + /** + * Returns the merged client configuration for faucet requests, combining global and faucet-specific overrides. + * Note: the `API_KEY` field is excluded for faucet requests. + * @param overrides - Optional per-call client configuration overrides. + * @returns The merged client configuration. + */ + getMergedFaucetConfig(overrides?: ClientConfig): ClientConfig { + // Faucet does not support API_KEY + const { API_KEY: _, ...clientConfig } = this.clientConfig ?? {}; + return { + ...clientConfig, + ...this.faucetConfig, + ...overrides, + HEADERS: { ...clientConfig?.HEADERS, ...this.faucetConfig?.HEADERS, ...overrides?.HEADERS }, + }; + } +} + +/** + * Factory function that creates a new {@link AptosConfig} instance. + * @param settings - Optional configuration settings. + * @returns A new AptosConfig instance. + */ +export function createConfig(settings?: AptosSettings): AptosConfig { + return new AptosConfig(settings); +} diff --git a/v10/src/api/faucet.ts b/v10/src/api/faucet.ts new file mode 100644 index 000000000..ce46c5f81 --- /dev/null +++ b/v10/src/api/faucet.ts @@ -0,0 +1,60 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { post } from "../client/post.js"; +import type { AccountAddressInput } from "../core/account-address.js"; +import { AccountAddress } from "../core/account-address.js"; +import { AptosApiType } from "../core/constants.js"; +import type { AptosConfig } from "./config.js"; +import { waitForTransaction } from "./transaction.js"; +import type { UserTransactionResponse } from "./types.js"; + +/** + * Funds an account with APT from the faucet. Only available on devnet and localnet networks. + * Submits a faucet request and waits for the funding transaction to be committed. + * + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param accountAddress - The address of the account to fund. + * @param amount - The amount of APT to fund in Octas (1 APT = 10^8 Octas). + * @param options - Optional parameters. + * @param options.timeoutSecs - Maximum time to wait for the funding transaction to commit. Defaults to 20. + * @param options.checkSuccess - If `true` (the default), throws if the funding transaction fails. + * @returns The committed user transaction response for the funding transaction. + * @throws Error if the network does not support a faucet (e.g. mainnet or testnet). + * + * @example + * ```typescript + * const config = new AptosConfig({ network: Network.DEVNET }); + * const txn = await fundAccount(config, "0x1", 100_000_000); // Fund 1 APT + * ``` + */ +export async function fundAccount( + config: AptosConfig, + accountAddress: AccountAddressInput, + amount: number, + options?: { timeoutSecs?: number; checkSuccess?: boolean }, +): Promise { + const url = config.getRequestUrl(AptosApiType.FAUCET); + const address = AccountAddress.from(accountAddress).toString(); + + const response = await post<{ txn_hashes: string[] }>({ + url, + apiType: AptosApiType.FAUCET, + path: "fund", + originMethod: "fundAccount", + body: { address, amount }, + overrides: config.getMergedFaucetConfig(), + client: config.client, + }); + + const txnHash = response.data.txn_hashes[0]; + if (!txnHash) { + throw new Error("Faucet response contained no transaction hashes"); + } + const result = await waitForTransaction(config, txnHash, { + timeoutSecs: options?.timeoutSecs, + checkSuccess: options?.checkSuccess, + }); + + return result as UserTransactionResponse; +} diff --git a/v10/src/api/general.ts b/v10/src/api/general.ts new file mode 100644 index 000000000..4ae14728b --- /dev/null +++ b/v10/src/api/general.ts @@ -0,0 +1,149 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { AnyNumber } from "../bcs/types.js"; +import { get } from "../client/get.js"; +import { post } from "../client/post.js"; +import { MimeType } from "../client/types.js"; +import { AptosApiType } from "../core/constants.js"; +import type { AptosConfig } from "./config.js"; +import type { Block, GasEstimation, LedgerInfo, ViewFunctionPayload } from "./types.js"; + +/** + * Retrieves the current ledger information from the fullnode, including chain ID, epoch, and latest version. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @returns The current ledger information. + */ +export async function getLedgerInfo(config: AptosConfig): Promise { + const url = config.getRequestUrl(AptosApiType.FULLNODE); + const response = await get({ + url, + apiType: AptosApiType.FULLNODE, + path: "", + originMethod: "getLedgerInfo", + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); + return response.data; +} + +/** + * Retrieves the chain ID of the connected Aptos network. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @returns The chain ID as a number (e.g. 1 for mainnet, 2 for testnet). + */ +export async function getChainId(config: AptosConfig): Promise { + const info = await getLedgerInfo(config); + return info.chain_id; +} + +/** + * Retrieves a block by its ledger version number. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param ledgerVersion - The ledger version contained within the target block. + * @param options - Optional parameters. + * @param options.withTransactions - If `true`, includes the transactions within the block. + * @returns The block containing the specified ledger version. + */ +export async function getBlockByVersion( + config: AptosConfig, + ledgerVersion: AnyNumber, + options?: { withTransactions?: boolean }, +): Promise { + const url = config.getRequestUrl(AptosApiType.FULLNODE); + const response = await get({ + url, + apiType: AptosApiType.FULLNODE, + path: `blocks/by_version/${ledgerVersion}`, + originMethod: "getBlockByVersion", + params: { with_transactions: options?.withTransactions }, + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); + return response.data; +} + +/** + * Retrieves a block by its height. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param blockHeight - The height of the block to retrieve. + * @param options - Optional parameters. + * @param options.withTransactions - If `true`, includes the transactions within the block. + * @returns The block at the specified height. + */ +export async function getBlockByHeight( + config: AptosConfig, + blockHeight: AnyNumber, + options?: { withTransactions?: boolean }, +): Promise { + const url = config.getRequestUrl(AptosApiType.FULLNODE); + const response = await get({ + url, + apiType: AptosApiType.FULLNODE, + path: `blocks/by_height/${blockHeight}`, + originMethod: "getBlockByHeight", + params: { with_transactions: options?.withTransactions }, + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); + return response.data; +} + +/** + * Executes a Move view function on-chain and returns its result without submitting a transaction. + * View functions are read-only and do not modify blockchain state. + * + * @typeParam T - The expected return type tuple of the view function. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param payload - The view function payload containing the function name, type arguments, and arguments. + * @param options - Optional parameters. + * @param options.ledgerVersion - The ledger version to execute the view function against. Defaults to the latest version. + * @returns The return values of the view function. + * + * @example + * ```typescript + * const [balance] = await view<[string]>(config, { + * function: "0x1::coin::balance", + * type_arguments: ["0x1::aptos_coin::AptosCoin"], + * arguments: ["0x1"], + * }); + * ``` + */ +export async function view( + config: AptosConfig, + payload: ViewFunctionPayload, + options?: { ledgerVersion?: AnyNumber }, +): Promise { + const url = config.getRequestUrl(AptosApiType.FULLNODE); + const response = await post({ + url, + apiType: AptosApiType.FULLNODE, + path: "view", + originMethod: "view", + body: payload, + params: { ledger_version: options?.ledgerVersion }, + contentType: MimeType.JSON, + acceptType: MimeType.JSON, + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); + return response.data; +} + +/** + * Retrieves the current gas price estimation from the fullnode. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @returns The gas price estimation including deprioritized, normal, and prioritized estimates. + */ +export async function getGasPriceEstimation(config: AptosConfig): Promise { + const url = config.getRequestUrl(AptosApiType.FULLNODE); + const response = await get({ + url, + apiType: AptosApiType.FULLNODE, + path: "estimate_gas_price", + originMethod: "getGasPriceEstimation", + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); + return response.data; +} diff --git a/v10/src/api/index.ts b/v10/src/api/index.ts new file mode 100644 index 000000000..6fe3d47bb --- /dev/null +++ b/v10/src/api/index.ts @@ -0,0 +1,261 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { Account as AccountSigner } from "../account/types.js"; +import type { AnyRawTransaction } from "../transactions/types.js"; +import * as accountFns from "./account.js"; +import * as coinFns from "./coin.js"; +import { AptosConfig, type AptosSettings } from "./config.js"; +import * as faucetFns from "./faucet.js"; +import * as generalFns from "./general.js"; +import * as tableFns from "./table.js"; +import * as transactionFns from "./transaction.js"; + +// ── Namespace API classes (composition, not mixins) ── + +/** Provides access to general blockchain queries such as ledger info, blocks, view functions, and gas estimation. */ +class GeneralAPI { + constructor(private config: AptosConfig) {} + + /** Retrieves the current ledger information from the fullnode. */ + getLedgerInfo() { + return generalFns.getLedgerInfo(this.config); + } + + /** Retrieves the chain ID of the connected Aptos network. */ + getChainId() { + return generalFns.getChainId(this.config); + } + + /** Retrieves a block by the ledger version it contains. */ + getBlockByVersion(...args: DropFirst>) { + return generalFns.getBlockByVersion(this.config, ...args); + } + + /** Retrieves a block by its height. */ + getBlockByHeight(...args: DropFirst>) { + return generalFns.getBlockByHeight(this.config, ...args); + } + + /** Executes a Move view function on-chain without submitting a transaction. */ + view(...args: DropFirst>) { + return generalFns.view(this.config, ...args); + } + + /** Retrieves the current gas price estimation from the fullnode. */ + getGasPriceEstimation() { + return generalFns.getGasPriceEstimation(this.config); + } +} + +/** Provides access to account-related queries such as account info, modules, resources, and transactions. */ +class AccountAPI { + constructor(private config: AptosConfig) {} + + /** Retrieves core account information (sequence number and authentication key). */ + getInfo(...args: DropFirst>) { + return accountFns.getAccountInfo(this.config, ...args); + } + + /** Retrieves all Move modules published under the specified account. */ + getModules(...args: DropFirst>) { + return accountFns.getAccountModules(this.config, ...args); + } + + /** Retrieves a single Move module by name from the specified account. */ + getModule(...args: DropFirst>) { + return accountFns.getAccountModule(this.config, ...args); + } + + /** Retrieves a specific Move resource by type from the specified account. */ + getResource(...args: DropFirst>>) { + return accountFns.getAccountResource(this.config, ...args); + } + + /** Retrieves all Move resources stored under the specified account. */ + getResources(...args: DropFirst>) { + return accountFns.getAccountResources(this.config, ...args); + } + + /** Retrieves transactions sent by the specified account. */ + getTransactions(...args: DropFirst>) { + return accountFns.getAccountTransactions(this.config, ...args); + } +} + +/** Provides access to transaction building, signing, submission, waiting, and querying. */ +class TransactionAPI { + constructor(private config: AptosConfig) {} + + /** Builds a simple entry function transaction. */ + buildSimple(...args: DropFirst>) { + return transactionFns.buildSimpleTransaction(this.config, ...args); + } + + /** Signs a transaction using the provided account's private key. */ + sign(signer: AccountSigner, transaction: AnyRawTransaction) { + return transactionFns.signTransaction(signer, transaction); + } + + /** Submits a signed transaction to the fullnode. */ + submit(...args: DropFirst>) { + return transactionFns.submitTransaction(this.config, ...args); + } + + /** Signs and submits a transaction in a single step. */ + signAndSubmit(signer: AccountSigner, transaction: AnyRawTransaction) { + return transactionFns.signAndSubmitTransaction(this.config, signer, transaction); + } + + /** Waits for a transaction to be committed on-chain. */ + waitForTransaction(...args: DropFirst>) { + return transactionFns.waitForTransaction(this.config, ...args); + } + + /** Retrieves a transaction by its hash. */ + getByHash(...args: DropFirst>) { + return transactionFns.getTransactionByHash(this.config, ...args); + } + + /** Retrieves a transaction by its ledger version number. */ + getByVersion(...args: DropFirst>) { + return transactionFns.getTransactionByVersion(this.config, ...args); + } + + /** Retrieves a list of transactions from the ledger. */ + getAll(...args: DropFirst>) { + return transactionFns.getTransactions(this.config, ...args); + } + + /** Generates the BCS-serialized signing message for a raw transaction. */ + getSigningMessage(transaction: AnyRawTransaction) { + return transactionFns.getSigningMessage(transaction); + } +} + +/** Provides access to coin transfer operations. */ +class CoinAPI { + constructor(private config: AptosConfig) {} + + /** Builds a coin transfer transaction. */ + transferTransaction(...args: DropFirst>) { + return coinFns.transferCoinTransaction(this.config, ...args); + } +} + +/** Provides access to the faucet for funding accounts on devnet/localnet. */ +class FaucetAPI { + constructor(private config: AptosConfig) {} + + /** Funds an account with APT from the faucet and waits for the transaction to commit. */ + fund(...args: DropFirst>) { + return faucetFns.fundAccount(this.config, ...args); + } +} + +/** Provides access to on-chain Move table queries. */ +class TableAPI { + constructor(private config: AptosConfig) {} + + /** Retrieves a single item from a Move table by its key. */ + getItem(...args: DropFirst>>) { + return tableFns.getTableItem(this.config, ...args); + } +} + +// ── Aptos Facade ── + +/** + * The primary entry point for interacting with the Aptos blockchain. + * Aggregates domain-specific APIs (account, transaction, coin, etc.) into a single facade. + * + * @example + * ```typescript + * const aptos = new Aptos({ network: Network.TESTNET }); + * + * // Query ledger info + * const ledgerInfo = await aptos.general.getLedgerInfo(); + * + * // Build, sign, and submit a transaction + * const txn = await aptos.transaction.buildSimple(sender.accountAddress, { + * function: "0x1::aptos_account::transfer", + * functionArguments: [recipient, new U64(1_000_000)], + * }); + * const pending = await aptos.transaction.signAndSubmit(sender, txn); + * const committed = await aptos.transaction.waitForTransaction(pending.hash); + * ``` + */ +export class Aptos { + /** The resolved configuration for this instance. */ + readonly config: AptosConfig; + /** General blockchain queries (ledger info, blocks, view functions, gas estimation). */ + readonly general: GeneralAPI; + /** Account-related queries (info, modules, resources, transactions). */ + readonly account: AccountAPI; + /** Transaction building, signing, submission, waiting, and querying. */ + readonly transaction: TransactionAPI; + /** Coin transfer operations. */ + readonly coin: CoinAPI; + /** Faucet operations for funding accounts on devnet/localnet. */ + readonly faucet: FaucetAPI; + /** On-chain Move table queries. */ + readonly table: TableAPI; + + /** + * Creates a new Aptos client instance. + * @param settings - Optional configuration settings, or an existing {@link AptosConfig} instance. + */ + constructor(settings?: AptosSettings | AptosConfig) { + this.config = settings instanceof AptosConfig ? settings : new AptosConfig(settings); + this.general = new GeneralAPI(this.config); + this.account = new AccountAPI(this.config); + this.transaction = new TransactionAPI(this.config); + this.coin = new CoinAPI(this.config); + this.faucet = new FaucetAPI(this.config); + this.table = new TableAPI(this.config); + } +} + +// ── Helper type ── + +/** + * Utility type that removes the first element from a tuple type. + * Used internally to strip the `config` parameter from standalone functions when wrapping them in namespace API classes. + */ +// biome-ignore lint/suspicious/noExplicitAny: TypeScript conditional tuple inference requires `any[]` +type DropFirst = T extends [unknown, ...infer Rest] ? Rest : never; + +export { + getAccountInfo, + getAccountModule, + getAccountModules, + getAccountResource, + getAccountResources, + getAccountTransactions, +} from "./account.js"; +export { transferCoinTransaction } from "./coin.js"; +// ── Re-exports ── +export { AptosConfig, type AptosSettings, createConfig } from "./config.js"; +export { fundAccount } from "./faucet.js"; +export { + getBlockByHeight, + getBlockByVersion, + getChainId, + getGasPriceEstimation, + getLedgerInfo, + view, +} from "./general.js"; +export { getTableItem } from "./table.js"; +export type { BuildSimpleTransactionOptions } from "./transaction.js"; +export { + buildSimpleTransaction, + getSigningMessage, + getTransactionByHash, + getTransactionByVersion, + getTransactions, + signAndSubmitTransaction, + signTransaction, + submitTransaction, + waitForTransaction, +} from "./transaction.js"; +export * from "./types.js"; diff --git a/v10/src/api/table.ts b/v10/src/api/table.ts new file mode 100644 index 000000000..3b31af4c7 --- /dev/null +++ b/v10/src/api/table.ts @@ -0,0 +1,38 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { AnyNumber } from "../bcs/types.js"; +import { post } from "../client/post.js"; +import { AptosApiType } from "../core/constants.js"; +import type { AptosConfig } from "./config.js"; +import type { TableItemRequest } from "./types.js"; + +/** + * Retrieves a single item from a Move table by its key. + * @typeParam T - The expected type of the table value. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param handle - The table handle (a hex-encoded address identifying the table). + * @param data - The table item request specifying the key type, value type, and key to look up. + * @param options - Optional parameters. + * @param options.ledgerVersion - The ledger version to query at. Defaults to the latest version. + * @returns The deserialized table value. + */ +export async function getTableItem( + config: AptosConfig, + handle: string, + data: TableItemRequest, + options?: { ledgerVersion?: AnyNumber }, +): Promise { + const url = config.getRequestUrl(AptosApiType.FULLNODE); + const response = await post({ + url, + apiType: AptosApiType.FULLNODE, + path: `tables/${handle}/item`, + originMethod: "getTableItem", + body: data, + params: { ledger_version: options?.ledgerVersion }, + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); + return response.data; +} diff --git a/v10/src/api/transaction.ts b/v10/src/api/transaction.ts new file mode 100644 index 000000000..87f556b29 --- /dev/null +++ b/v10/src/api/transaction.ts @@ -0,0 +1,374 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { Account } from "../account/types.js"; +import { Serializer } from "../bcs/serializer.js"; +import type { AnyNumber, EntryFunctionArgument } from "../bcs/types.js"; +import { get } from "../client/get.js"; +import { post } from "../client/post.js"; +import { MimeType } from "../client/types.js"; +import type { AccountAddressInput } from "../core/account-address.js"; +import { AccountAddress } from "../core/account-address.js"; +import { AptosApiType } from "../core/constants.js"; +import { AptosApiError } from "../core/errors.js"; +import type { TypeTag } from "../core/type-tag.js"; +import type { HexInput } from "../hex/index.js"; +import { Hex } from "../hex/index.js"; +import { type AccountAuthenticator, TransactionAuthenticatorSingleSender } from "../transactions/authenticator.js"; +import { ChainId } from "../transactions/chain-id.js"; +import { RawTransaction } from "../transactions/raw-transaction.js"; +import { SignedTransaction } from "../transactions/signed-transaction.js"; +import { generateSigningMessageForTransaction } from "../transactions/signing-message.js"; +import { SimpleTransaction } from "../transactions/simple-transaction.js"; +import { EntryFunction, TransactionPayloadEntryFunction } from "../transactions/transaction-payload.js"; +import type { AnyRawTransaction } from "../transactions/types.js"; +import type { AptosConfig } from "./config.js"; +import { getGasPriceEstimation, getLedgerInfo } from "./general.js"; +import type { + CommittedTransactionResponse, + MoveStructId, + PendingTransactionResponse, + TransactionResponse, +} from "./types.js"; +import { isPendingTransactionResponse } from "./types.js"; + +// ── Transaction building ── + +/** Options for customizing how a simple transaction is built. */ +export interface BuildSimpleTransactionOptions { + /** The maximum amount of gas units the sender is willing to pay. Defaults to the config's `defaultMaxGasAmount`. */ + maxGasAmount?: AnyNumber; + /** The gas unit price in Octas. Defaults to the network's estimated gas price. */ + gasUnitPrice?: AnyNumber; + /** The transaction expiration timestamp in seconds since the Unix epoch. Defaults to `now + config.defaultTxnExpSecFromNow`. */ + expireTimestamp?: AnyNumber; + /** The sender's account sequence number. If omitted, it is fetched from the network automatically. */ + sequenceNumber?: AnyNumber; +} + +/** + * Builds a simple entry function transaction. Automatically fetches the sender's sequence number, + * chain ID, and gas estimation from the network if not provided in `options`. + * + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param sender - The address of the transaction sender. + * @param payload - The entry function to call. + * @param payload.function - The fully qualified function name (e.g. `"0x1::aptos_account::transfer"`). + * @param payload.typeArguments - Optional type arguments for generic functions. + * @param payload.functionArguments - Optional arguments to pass to the function. + * @param options - Optional transaction parameters (gas, expiration, sequence number). + * @returns A {@link SimpleTransaction} ready to be signed and submitted. + * + * @example + * ```typescript + * const txn = await buildSimpleTransaction(config, senderAddress, { + * function: "0x1::aptos_account::transfer", + * functionArguments: [recipientAddress, new U64(1_000_000)], + * }); + * ``` + */ +export async function buildSimpleTransaction( + config: AptosConfig, + sender: AccountAddressInput, + payload: { + function: MoveStructId; + typeArguments?: TypeTag[]; + functionArguments?: EntryFunctionArgument[]; + }, + options?: BuildSimpleTransactionOptions, +): Promise { + const senderAddress = AccountAddress.from(sender); + + // Fetch account info and gas estimation in parallel if not provided + const [ledgerInfo, gasEstimation, accountData] = await Promise.all([ + getLedgerInfo(config), + options?.gasUnitPrice !== undefined ? null : getGasPriceEstimation(config), + options?.sequenceNumber !== undefined ? null : getAccountSequenceNumber(config, senderAddress), + ]); + + const parts = payload.function.split("::"); + if (parts.length !== 3) { + throw new Error(`Invalid function identifier: "${payload.function}". Expected format: "address::module::function"`); + } + const [moduleAddress, moduleName, functionName] = parts; + const moduleId = `${moduleAddress}::${moduleName}` as `${string}::${string}`; + + const entryFunction = EntryFunction.build( + moduleId, + functionName, + payload.typeArguments ?? [], + payload.functionArguments ?? [], + ); + + const now = Math.floor(Date.now() / 1000); + const rawTxn = new RawTransaction( + senderAddress, + BigInt(options?.sequenceNumber ?? accountData ?? 0n), + new TransactionPayloadEntryFunction(entryFunction), + BigInt(options?.maxGasAmount ?? config.defaultMaxGasAmount), + BigInt(options?.gasUnitPrice ?? gasEstimation?.gas_estimate ?? 100), + BigInt(options?.expireTimestamp ?? now + config.defaultTxnExpSecFromNow), + new ChainId(ledgerInfo?.chain_id ?? 4), + ); + + return new SimpleTransaction(rawTxn); +} + +async function getAccountSequenceNumber(config: AptosConfig, address: AccountAddress): Promise { + const url = config.getRequestUrl(AptosApiType.FULLNODE); + const response = await get<{ sequence_number: string }>({ + url, + apiType: AptosApiType.FULLNODE, + path: `accounts/${address}`, + originMethod: "getAccountSequenceNumber", + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); + return BigInt(response.data.sequence_number); +} + +// ── Signing ── + +/** + * Signs a transaction using the provided account's private key. + * @param signer - The account that will sign the transaction. + * @param transaction - The raw transaction to sign. + * @returns An authenticator containing the signature. + */ +export function signTransaction(signer: Account, transaction: AnyRawTransaction): AccountAuthenticator { + return signer.signTransactionWithAuthenticator(transaction); +} + +// ── Submission ── + +/** + * Submits a signed transaction to the Aptos fullnode for inclusion in the mempool. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param transaction - The raw transaction that was signed. + * @param senderAuthenticator - The authenticator produced by signing the transaction. + * @returns The pending transaction response containing the transaction hash. + */ +export async function submitTransaction( + config: AptosConfig, + transaction: AnyRawTransaction, + senderAuthenticator: AccountAuthenticator, +): Promise { + const signedTxn = new SignedTransaction( + transaction.rawTransaction, + new TransactionAuthenticatorSingleSender(senderAuthenticator), + ); + + const serializer = new Serializer(); + signedTxn.serialize(serializer); + + const url = config.getRequestUrl(AptosApiType.FULLNODE); + const response = await post({ + url, + apiType: AptosApiType.FULLNODE, + path: "transactions", + originMethod: "submitTransaction", + body: serializer.toUint8Array(), + contentType: MimeType.BCS_SIGNED_TRANSACTION, + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); + + return response.data; +} + +/** + * Signs and submits a transaction in a single step. This is a convenience wrapper + * that calls {@link signTransaction} followed by {@link submitTransaction}. + * + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param signer - The account that will sign and submit the transaction. + * @param transaction - The raw transaction to sign and submit. + * @returns The pending transaction response containing the transaction hash. + * + * @example + * ```typescript + * const txn = await buildSimpleTransaction(config, sender.accountAddress, { + * function: "0x1::aptos_account::transfer", + * functionArguments: [recipient, new U64(1_000_000)], + * }); + * const pending = await signAndSubmitTransaction(config, sender, txn); + * const committed = await waitForTransaction(config, pending.hash); + * ``` + */ +export async function signAndSubmitTransaction( + config: AptosConfig, + signer: Account, + transaction: AnyRawTransaction, +): Promise { + const authenticator = signTransaction(signer, transaction); + return submitTransaction(config, transaction, authenticator); +} + +// ── Waiting ── + +/** + * Waits for a transaction to be committed on-chain. First attempts a long-poll endpoint, + * then falls back to polling with exponential backoff. + * + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param transactionHash - The hash of the pending transaction to wait for. + * @param options - Optional parameters. + * @param options.timeoutSecs - Maximum time to wait in seconds. Defaults to 20. + * @param options.checkSuccess - If `true` (the default), throws an error if the transaction fails on-chain. + * @returns The committed transaction response. + * @throws Error if the transaction times out or fails (when `checkSuccess` is `true`). + * + * @example + * ```typescript + * const committed = await waitForTransaction(config, pendingTxn.hash, { + * timeoutSecs: 30, + * checkSuccess: true, + * }); + * console.log("Transaction version:", committed.version); + * ``` + */ +export async function waitForTransaction( + config: AptosConfig, + transactionHash: HexInput, + options?: { timeoutSecs?: number; checkSuccess?: boolean }, +): Promise { + const timeoutSecs = options?.timeoutSecs ?? 20; + const checkSuccess = options?.checkSuccess ?? true; + const hashStr = typeof transactionHash === "string" ? transactionHash : Hex.fromHexInput(transactionHash).toString(); + + const startTime = Date.now(); + let lastError: Error | undefined; + + // Try the long-poll endpoint first + const url = config.getRequestUrl(AptosApiType.FULLNODE); + try { + const response = await get({ + url, + apiType: AptosApiType.FULLNODE, + path: `transactions/wait_by_hash/${hashStr}`, + originMethod: "waitForTransaction", + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); + if (!isPendingTransactionResponse(response.data)) { + if (checkSuccess && !("success" in response.data && response.data.success)) { + throw new Error(`Transaction ${hashStr} failed: ${(response.data as Record).vm_status}`); + } + return response.data as CommittedTransactionResponse; + } + } catch (e) { + // Only suppress 404 (not found yet) and 429 (rate limit); re-throw everything else + if (e instanceof AptosApiError && (e.status === 404 || e.status === 429)) { + lastError = e; + } else { + throw e; + } + } + + // Fall back to polling + let delayMs = 200; + while (Date.now() - startTime < timeoutSecs * 1000) { + await new Promise((resolve) => setTimeout(resolve, delayMs)); + delayMs = Math.min(delayMs * 1.5, 2000); + + try { + const txn = await getTransactionByHash(config, hashStr); + if (!isPendingTransactionResponse(txn)) { + if (checkSuccess && !("success" in txn && txn.success)) { + throw new Error(`Transaction ${hashStr} failed: ${(txn as Record).vm_status}`); + } + return txn as CommittedTransactionResponse; + } + } catch (e) { + // Only suppress 404-like "not found yet" errors; re-throw everything else + if (e instanceof AptosApiError && e.status === 404) { + // Transaction not committed yet, keep polling + } else { + throw e; + } + } + } + + throw lastError ?? new Error(`Transaction ${hashStr} timed out after ${timeoutSecs}s`); +} + +// ── Transaction queries ── + +/** + * Retrieves a transaction by its hash. The returned transaction may be pending or committed. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param transactionHash - The hash of the transaction to look up. + * @returns The transaction response (pending or committed). + */ +export async function getTransactionByHash( + config: AptosConfig, + transactionHash: HexInput, +): Promise { + const hashStr = typeof transactionHash === "string" ? transactionHash : Hex.fromHexInput(transactionHash).toString(); + const url = config.getRequestUrl(AptosApiType.FULLNODE); + const response = await get({ + url, + apiType: AptosApiType.FULLNODE, + path: `transactions/by_hash/${hashStr}`, + originMethod: "getTransactionByHash", + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); + return response.data; +} + +/** + * Retrieves a committed transaction by its ledger version number. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param ledgerVersion - The ledger version of the transaction to retrieve. + * @returns The transaction response at the specified version. + */ +export async function getTransactionByVersion( + config: AptosConfig, + ledgerVersion: AnyNumber, +): Promise { + const url = config.getRequestUrl(AptosApiType.FULLNODE); + const response = await get({ + url, + apiType: AptosApiType.FULLNODE, + path: `transactions/by_version/${ledgerVersion}`, + originMethod: "getTransactionByVersion", + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); + return response.data; +} + +/** + * Retrieves a list of transactions from the ledger, ordered by version. + * @param config - The Aptos configuration specifying which network and endpoints to use. + * @param options - Optional parameters. + * @param options.offset - The ledger version to start listing from. + * @param options.limit - Maximum number of transactions to return. + * @returns An array of transaction responses. + */ +export async function getTransactions( + config: AptosConfig, + options?: { offset?: AnyNumber; limit?: number }, +): Promise { + const url = config.getRequestUrl(AptosApiType.FULLNODE); + const response = await get({ + url, + apiType: AptosApiType.FULLNODE, + path: "transactions", + originMethod: "getTransactions", + params: { start: options?.offset, limit: options?.limit }, + overrides: config.getMergedFullnodeConfig(), + client: config.client, + }); + return response.data; +} + +/** + * Generates the BCS-serialized signing message (bytes to sign) for a raw transaction. + * @param transaction - The raw transaction to generate the signing message for. + * @returns The signing message as a `Uint8Array`. + */ +export function getSigningMessage(transaction: AnyRawTransaction): Uint8Array { + return generateSigningMessageForTransaction(transaction); +} diff --git a/v10/src/api/types.ts b/v10/src/api/types.ts new file mode 100644 index 000000000..2591a4d3f --- /dev/null +++ b/v10/src/api/types.ts @@ -0,0 +1,371 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +// REST API response types from the Aptos fullnode + +// Re-export MoveStructId from transactions to avoid duplication +import type { MoveStructId } from "../transactions/types.js"; + +export type { MoveStructId } from "../transactions/types.js"; + +/** The role a node plays in the Aptos network. */ +export enum RoleType { + /** A validator node that participates in consensus. */ + VALIDATOR = "validator", + /** A full node that serves API requests but does not participate in consensus. */ + FULL_NODE = "full_node", +} + +/** Information about the current state of the blockchain ledger, returned by the fullnode API. */ +export type LedgerInfo = { + /** The chain ID identifying this Aptos network (e.g. 1 for mainnet, 2 for testnet). */ + chain_id: number; + /** The current epoch number as a string. */ + epoch: string; + /** The latest ledger version (transaction height) as a string. */ + ledger_version: string; + /** The oldest available ledger version as a string. */ + oldest_ledger_version: string; + /** The timestamp of the latest committed transaction in microseconds as a string. */ + ledger_timestamp: string; + /** The role of this node (validator or full_node). */ + node_role: RoleType; + /** The oldest available block height as a string. */ + oldest_block_height: string; + /** The latest block height as a string. */ + block_height: string; + /** The git hash of the node binary, if available. */ + git_hash?: string; +}; + +/** Core on-chain data for an Aptos account. */ +export type AccountData = { + /** The next sequence number for transactions from this account. */ + sequence_number: string; + /** The authentication key associated with this account. */ + authentication_key: string; +}; + +/** + * A Move resource stored under an account, consisting of its struct type and data payload. + * @typeParam T - The shape of the resource data. Defaults to `Record`. + */ +export type MoveResource> = { + /** The fully qualified Move struct type (e.g. `0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>`). */ + type: MoveStructId; + /** The deserialized resource data. */ + data: T; +}; + +/** A published Move module, containing its bytecode and optional ABI. */ +export type MoveModuleBytecode = { + /** The hex-encoded bytecode of the module. */ + bytecode: string; + /** The ABI (application binary interface) of the module, if available. */ + abi?: MoveModule; +}; + +/** The ABI of a Move module, describing its address, name, functions, and structs. */ +export type MoveModule = { + /** The address where the module is published. */ + address: string; + /** The name of the module. */ + name: string; + /** List of friend modules that have access to this module's private functions. */ + friends: string[]; + /** The functions exposed by this module. */ + exposed_functions: MoveFunction[]; + /** The structs defined in this module. */ + structs: MoveStruct[]; +}; + +/** Description of a function defined in a Move module. */ +export type MoveFunction = { + /** The function name. */ + name: string; + /** The visibility level (e.g. "public", "private", "friend"). */ + visibility: string; + /** Whether this function is an entry function that can be called directly in transactions. */ + is_entry: boolean; + /** Whether this function is a view function that can be called without submitting a transaction. */ + is_view: boolean; + /** Generic type parameter constraints for this function. */ + generic_type_params: { constraints: string[] }[]; + /** The parameter types as Move type strings. */ + params: string[]; + /** The return types as Move type strings. */ + return: string[]; +}; + +/** Description of a struct defined in a Move module. */ +export type MoveStruct = { + /** The struct name. */ + name: string; + /** Whether this struct is a native (built-in) type. */ + is_native: boolean; + /** The abilities this struct possesses (e.g. "copy", "drop", "store", "key"). */ + abilities: string[]; + /** Generic type parameter constraints for this struct. */ + generic_type_params: { constraints: string[] }[]; + /** The fields of the struct with their names and types. */ + fields: { name: string; type: string }[]; +}; + +/** Gas price estimation returned by the fullnode, providing low/median/high estimates. */ +export type GasEstimation = { + /** The estimated gas unit price for deprioritized (low-priority) transactions. */ + deprioritized_gas_estimate?: number; + /** The estimated gas unit price for normal-priority transactions. */ + gas_estimate: number; + /** The estimated gas unit price for prioritized (high-priority) transactions. */ + prioritized_gas_estimate?: number; +}; + +/** A block on the Aptos blockchain, containing metadata and optionally its transactions. */ +export type Block = { + /** The height of this block as a string. */ + block_height: string; + /** The hash of this block. */ + block_hash: string; + /** The timestamp when this block was committed in microseconds as a string. */ + block_timestamp: string; + /** The first transaction version included in this block. */ + first_version: string; + /** The last transaction version included in this block. */ + last_version: string; + /** The transactions in this block, if requested. */ + transactions?: TransactionResponse[]; +}; + +/** The request body for querying a table item by key. */ +export type TableItemRequest = { + /** The Move type of the table key (e.g. `"address"`, `"u64"`). */ + key_type: string; + /** The Move type of the table value. */ + value_type: string; + /** The key to look up in the table. */ + key: unknown; +}; + +// ── Transaction response types ── + +/** Discriminant values for the different transaction response types returned by the API. */ +export enum TransactionResponseType { + /** A transaction that has been submitted but not yet committed. */ + Pending = "pending_transaction", + /** A user-submitted transaction that has been committed. */ + User = "user_transaction", + /** The genesis transaction that initialized the blockchain. */ + Genesis = "genesis_transaction", + /** A block metadata transaction inserted at the start of each block. */ + BlockMetadata = "block_metadata_transaction", + /** A state checkpoint transaction. */ + StateCheckpoint = "state_checkpoint_transaction", + /** A validator-specific transaction. */ + Validator = "validator_transaction", + /** A block epilogue transaction inserted at the end of each block. */ + BlockEpilogue = "block_epilogue_transaction", +} + +/** Union of all possible transaction response types (pending or committed). */ +export type TransactionResponse = PendingTransactionResponse | CommittedTransactionResponse; + +/** Union of all committed (finalized) transaction response types. */ +export type CommittedTransactionResponse = + | UserTransactionResponse + | GenesisTransactionResponse + | BlockMetadataTransactionResponse + | StateCheckpointTransactionResponse + | ValidatorTransactionResponse + | BlockEpilogueTransactionResponse; + +/** A transaction that has been submitted to the mempool but not yet committed to the ledger. */ +export type PendingTransactionResponse = { + type: TransactionResponseType.Pending; + /** The transaction hash. */ + hash: string; + /** The sender's account address. */ + sender: string; + /** The sender's sequence number for this transaction. */ + sequence_number: string; + /** The maximum gas amount the sender is willing to pay. */ + max_gas_amount: string; + /** The gas unit price in Octas. */ + gas_unit_price: string; + /** The transaction expiration timestamp in seconds since the Unix epoch. */ + expiration_timestamp_secs: string; + /** The transaction payload. */ + payload: unknown; + /** The transaction signature, if present. */ + signature?: unknown; +}; + +/** A user-submitted transaction that has been committed to the ledger. */ +export type UserTransactionResponse = { + type: TransactionResponseType.User; + /** The ledger version at which this transaction was committed. */ + version: string; + /** The transaction hash. */ + hash: string; + /** Hash of the state changes produced by this transaction. */ + state_change_hash: string; + /** Root hash of the event accumulator. */ + event_root_hash: string; + /** Hash of the state checkpoint, if applicable. */ + state_checkpoint_hash: string | null; + /** The actual gas consumed by this transaction. */ + gas_used: string; + /** Whether the transaction executed successfully. */ + success: boolean; + /** The VM status or error message. */ + vm_status: string; + /** Root hash of the transaction accumulator. */ + accumulator_root_hash: string; + /** The state changes (write set) produced by this transaction. */ + changes: unknown[]; + /** The sender's account address. */ + sender: string; + /** The sender's sequence number for this transaction. */ + sequence_number: string; + /** The maximum gas amount the sender was willing to pay. */ + max_gas_amount: string; + /** The gas unit price in Octas. */ + gas_unit_price: string; + /** The transaction expiration timestamp in seconds since the Unix epoch. */ + expiration_timestamp_secs: string; + /** The transaction payload. */ + payload: unknown; + /** The transaction signature, if present. */ + signature?: unknown; + /** The events emitted by this transaction. */ + events: unknown[]; + /** The timestamp when this transaction was committed in microseconds. */ + timestamp: string; +}; + +/** The genesis transaction that initialized the blockchain state. */ +export type GenesisTransactionResponse = { + type: TransactionResponseType.Genesis; + version: string; + hash: string; + state_change_hash: string; + event_root_hash: string; + state_checkpoint_hash?: string; + gas_used: string; + success: boolean; + vm_status: string; + accumulator_root_hash: string; + changes: unknown[]; + payload: unknown; + events: unknown[]; +}; + +/** A block metadata transaction inserted by validators at the beginning of each block. */ +export type BlockMetadataTransactionResponse = { + type: TransactionResponseType.BlockMetadata; + version: string; + hash: string; + state_change_hash: string; + event_root_hash: string; + state_checkpoint_hash?: string; + gas_used: string; + success: boolean; + vm_status: string; + accumulator_root_hash: string; + changes: unknown[]; + /** The unique ID of this block metadata transaction. */ + id: string; + /** The epoch number. */ + epoch: string; + /** The round number within the epoch. */ + round: string; + events: unknown[]; + /** Bitvec of which validators voted for the previous block. */ + previous_block_votes_bitvec: number[]; + /** The address of the validator that proposed this block. */ + proposer: string; + /** Indices of validators that failed to propose. */ + failed_proposer_indices: number[]; + timestamp: string; +}; + +/** A state checkpoint transaction that records a snapshot of the ledger state. */ +export type StateCheckpointTransactionResponse = { + type: TransactionResponseType.StateCheckpoint; + version: string; + hash: string; + state_change_hash: string; + event_root_hash: string; + state_checkpoint_hash?: string; + gas_used: string; + success: boolean; + vm_status: string; + accumulator_root_hash: string; + changes: unknown[]; + timestamp: string; +}; + +/** A validator-specific transaction (e.g. stake operations). */ +export type ValidatorTransactionResponse = { + type: TransactionResponseType.Validator; + version: string; + hash: string; + state_change_hash: string; + event_root_hash: string; + state_checkpoint_hash?: string; + gas_used: string; + success: boolean; + vm_status: string; + accumulator_root_hash: string; + changes: unknown[]; + events: unknown[]; + timestamp: string; +}; + +/** A block epilogue transaction inserted at the end of each block. */ +export type BlockEpilogueTransactionResponse = { + type: TransactionResponseType.BlockEpilogue; + version: string; + hash: string; + state_change_hash: string; + event_root_hash: string; + state_checkpoint_hash?: string; + gas_used: string; + success: boolean; + vm_status: string; + accumulator_root_hash: string; + changes: unknown[]; + timestamp: string; +}; + +// ── View function types ── + +/** The payload for calling a Move view function on-chain without submitting a transaction. */ +export type ViewFunctionPayload = { + /** The fully qualified function name (e.g. `"0x1::coin::balance"`). */ + function: MoveStructId; + /** The type arguments for the function call, as Move type strings. */ + type_arguments: string[]; + /** The arguments to pass to the function. */ + arguments: unknown[]; +}; + +// ── Type guards ── + +/** + * Checks whether a transaction response represents a pending (not yet committed) transaction. + * @param response - The transaction response to check. + * @returns `true` if the response is a {@link PendingTransactionResponse}. + */ +export function isPendingTransactionResponse(response: TransactionResponse): response is PendingTransactionResponse { + return response.type === TransactionResponseType.Pending; +} + +/** + * Checks whether a transaction response represents a committed user transaction. + * @param response - The transaction response to check. + * @returns `true` if the response is a {@link UserTransactionResponse}. + */ +export function isUserTransactionResponse(response: TransactionResponse): response is UserTransactionResponse { + return response.type === TransactionResponseType.User; +} diff --git a/v10/src/bcs/consts.ts b/v10/src/bcs/consts.ts new file mode 100644 index 000000000..55d3f87c5 --- /dev/null +++ b/v10/src/bcs/consts.ts @@ -0,0 +1,52 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { + Int8, + Int16, + Int32, + Int64, + Int128, + Int256, + Uint8, + Uint16, + Uint32, + Uint64, + Uint128, + Uint256, +} from "./types.js"; + +// Unsigned integer upper bounds (2^N - 1) +export const MAX_U8_NUMBER: Uint8 = 255; +export const MAX_U16_NUMBER: Uint16 = 65535; +export const MAX_U32_NUMBER: Uint32 = 4294967295; +export const MAX_U64_BIG_INT: Uint64 = 18446744073709551615n; +export const MAX_U128_BIG_INT: Uint128 = 340282366920938463463374607431768211455n; +export const MAX_U256_BIG_INT: Uint256 = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + +// Signed integer bounds +export const MIN_I8_NUMBER: Int8 = -128; +export const MAX_I8_NUMBER: Int8 = 127; +export const MIN_I16_NUMBER: Int16 = -32768; +export const MAX_I16_NUMBER: Int16 = 32767; +export const MIN_I32_NUMBER: Int32 = -2147483648; +export const MAX_I32_NUMBER: Int32 = 2147483647; +export const MIN_I64_BIG_INT: Int64 = -9223372036854775808n; +export const MAX_I64_BIG_INT: Int64 = 9223372036854775807n; +export const MIN_I128_BIG_INT: Int128 = -170141183460469231731687303715884105728n; +export const MAX_I128_BIG_INT: Int128 = 170141183460469231731687303715884105727n; +export const MIN_I256_BIG_INT: Int256 = -57896044618658097711785492504343953926634992332820282019728792003956564819968n; +export const MAX_I256_BIG_INT: Int256 = 57896044618658097711785492504343953926634992332820282019728792003956564819967n; + +// Cached BigInt constants for hot serialization/deserialization paths +export const BIGINT_0 = 0n; +export const BIGINT_1 = 1n; +export const BIGINT_32 = 32n; +export const BIGINT_63 = 63n; +export const BIGINT_64 = 64n; +export const BIGINT_127 = 127n; +export const BIGINT_128 = 128n; +export const BIGINT_255 = 255n; +export const BIGINT_256 = 256n; +export const BIGINT_MAX_U32 = BigInt(MAX_U32_NUMBER); diff --git a/v10/src/bcs/deserializer.ts b/v10/src/bcs/deserializer.ts new file mode 100644 index 000000000..8d1fb1e48 --- /dev/null +++ b/v10/src/bcs/deserializer.ts @@ -0,0 +1,429 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { Hex } from "../hex/hex.js"; +import { BIGINT_1, BIGINT_32, BIGINT_63, BIGINT_64, BIGINT_127, BIGINT_128, BIGINT_255, BIGINT_256 } from "./consts.js"; +import type { HexInput, Uint8, Uint16, Uint32, Uint64, Uint128, Uint256 } from "./types.js"; + +const TEXT_DECODER = new TextDecoder(); + +/** Max 10MB to prevent memory exhaustion from malformed BCS data. */ +const MAX_DESERIALIZE_BYTES_LENGTH = 10 * 1024 * 1024; + +/** + * Interface for types that expose a static `deserialize` factory method. + * + * Implement this interface on the class (not an instance) to allow + * {@link Deserializer.deserialize} and {@link Deserializer.deserializeVector} to + * reconstruct values from a byte stream. + * + * @typeParam T - The type that `deserialize` produces. + * + * @example + * ```typescript + * class MyType implements Deserializable { + * static deserialize(deserializer: Deserializer): MyType { + * const value = deserializer.deserializeU32(); + * return new MyType(value); + * } + * } + * ``` + */ +export interface Deserializable { + /** + * Reads bytes from `deserializer` and constructs an instance of `T`. + * @param deserializer - The deserializer to read from. + * @returns A newly constructed `T`. + */ + deserialize(deserializer: Deserializer): T; +} + +/** + * BCS (Binary Canonical Serialization) deserializer for decoding Move and Aptos data types. + * + * Reads typed values sequentially from a fixed byte buffer. The buffer is copied + * on construction so outside mutations do not affect in-progress deserialization. + * + * @example + * ```typescript + * const bytes = new Uint8Array([0xff, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f]); + * const deserializer = new Deserializer(bytes); + * const num = deserializer.deserializeU8(); // 255 + * const str = deserializer.deserializeStr(); // "hello" + * deserializer.assertFinished(); + * ``` + */ +export class Deserializer { + private buffer: ArrayBuffer; + private dataView: DataView; + private offset: number; + + /** + * Creates a new Deserializer that reads from a copy of the given bytes. + * @param data - The bytes to deserialize. + */ + constructor(data: Uint8Array) { + // Copy to prevent outside mutation + this.buffer = new ArrayBuffer(data.length); + new Uint8Array(this.buffer).set(data, 0); + this.dataView = new DataView(this.buffer); + this.offset = 0; + } + + /** + * Creates a Deserializer from a hex-encoded string or `Uint8Array`. + * @param hex - A hex string (with or without `0x` prefix) or raw bytes. + * @returns A new `Deserializer` wrapping the decoded bytes. + */ + static fromHex(hex: HexInput): Deserializer { + return new Deserializer(Hex.hexInputToUint8Array(hex)); + } + + private read(length: number): Uint8Array { + if (this.offset + length > this.buffer.byteLength) { + throw new Error("Reached to the end of buffer"); + } + const bytes = new Uint8Array(this.buffer, this.offset, length); + this.offset += length; + return bytes; + } + + /** + * Returns the number of bytes remaining in the buffer that have not yet been read. + * @returns The remaining byte count. + */ + remaining(): number { + return this.buffer.byteLength - this.offset; + } + + /** + * Asserts that all bytes in the buffer have been consumed. + * Call this after deserialization is complete to detect trailing bytes that + * indicate a malformed or truncated payload. + * @throws {Error} If there are unread bytes remaining. + */ + assertFinished(): void { + if (this.remaining() !== 0) { + throw new Error("Buffer has remaining bytes"); + } + } + + // ── String / Bytes ── + + /** + * Deserializes a UTF-8 string that was serialized with a ULEB128 length prefix. + * @returns The decoded string. + */ + deserializeStr(): string { + return TEXT_DECODER.decode(this.deserializeBytes()); + } + + /** + * Deserializes a length-prefixed byte array. + * Reads a ULEB128 length, then reads that many bytes. + * @returns A copy of the deserialized bytes. + * @throws {Error} If the encoded length exceeds 10 MB. + */ + deserializeBytes(): Uint8Array { + const len = this.deserializeUleb128AsU32(); + if (len > MAX_DESERIALIZE_BYTES_LENGTH) { + throw new Error( + `Deserialization error: byte array length ${len} exceeds maximum allowed ${MAX_DESERIALIZE_BYTES_LENGTH}`, + ); + } + return this.read(len).slice(); + } + + /** + * Deserializes exactly `len` bytes (no length prefix). + * The caller is responsible for knowing the expected byte count. + * @param len - The number of bytes to read. + * @returns A copy of the deserialized bytes. + */ + deserializeFixedBytes(len: number): Uint8Array { + return this.read(len).slice(); + } + + // ── Boolean ── + + /** + * Deserializes a single byte as a boolean (`1` → `true`, `0` → `false`). + * @returns The boolean value. + * @throws {Error} If the byte is not `0` or `1`. + */ + deserializeBool(): boolean { + const bool = this.read(1)[0]; + if (bool !== 1 && bool !== 0) { + throw new Error("Invalid boolean value"); + } + return bool === 1; + } + + // ── Unsigned integers ── + + /** + * Deserializes an unsigned 8-bit integer (u8). + * @returns A number in the range [0, 255]. + */ + deserializeU8(): Uint8 { + return this.read(1)[0]; + } + + /** + * Deserializes an unsigned 16-bit integer (u16) in little-endian byte order. + * @returns A number in the range [0, 65535]. + */ + deserializeU16(): Uint16 { + if (this.offset + 2 > this.buffer.byteLength) { + throw new Error("Reached to the end of buffer"); + } + const value = this.dataView.getUint16(this.offset, true); + this.offset += 2; + return value; + } + + /** + * Deserializes an unsigned 32-bit integer (u32) in little-endian byte order. + * @returns A number in the range [0, 4294967295]. + */ + deserializeU32(): Uint32 { + if (this.offset + 4 > this.buffer.byteLength) { + throw new Error("Reached to the end of buffer"); + } + const value = this.dataView.getUint32(this.offset, true); + this.offset += 4; + return value; + } + + /** + * Deserializes an unsigned 64-bit integer (u64) in little-endian byte order. + * @returns A `bigint` in the range [0, 2^64 - 1]. + */ + deserializeU64(): Uint64 { + const low = this.deserializeU32(); + const high = this.deserializeU32(); + return (BigInt(high) << BIGINT_32) | BigInt(low); + } + + /** + * Deserializes an unsigned 128-bit integer (u128) in little-endian byte order. + * @returns A `bigint` in the range [0, 2^128 - 1]. + */ + deserializeU128(): Uint128 { + const low = this.deserializeU64(); + const high = this.deserializeU64(); + return (high << BIGINT_64) | low; + } + + /** + * Deserializes an unsigned 256-bit integer (u256) in little-endian byte order. + * @returns A `bigint` in the range [0, 2^256 - 1]. + */ + deserializeU256(): Uint256 { + const low = this.deserializeU128(); + const high = this.deserializeU128(); + return (high << BIGINT_128) | low; + } + + // ── Signed integers ── + + /** + * Deserializes a signed 8-bit integer (i8). + * @returns A number in the range [-128, 127]. + */ + deserializeI8(): number { + if (this.offset + 1 > this.buffer.byteLength) { + throw new Error("Reached to the end of buffer"); + } + const value = this.dataView.getInt8(this.offset); + this.offset += 1; + return value; + } + + /** + * Deserializes a signed 16-bit integer (i16) in little-endian byte order. + * @returns A number in the range [-32768, 32767]. + */ + deserializeI16(): number { + if (this.offset + 2 > this.buffer.byteLength) { + throw new Error("Reached to the end of buffer"); + } + const value = this.dataView.getInt16(this.offset, true); + this.offset += 2; + return value; + } + + /** + * Deserializes a signed 32-bit integer (i32) in little-endian byte order. + * @returns A number in the range [-2147483648, 2147483647]. + */ + deserializeI32(): number { + if (this.offset + 4 > this.buffer.byteLength) { + throw new Error("Reached to the end of buffer"); + } + const value = this.dataView.getInt32(this.offset, true); + this.offset += 4; + return value; + } + + /** + * Deserializes a signed 64-bit integer (i64) in little-endian byte order using two's complement. + * @returns A `bigint` in the range [-2^63, 2^63 - 1]. + */ + deserializeI64(): bigint { + const low = this.deserializeU32(); + const high = this.deserializeU32(); + const unsigned = (BigInt(high) << BIGINT_32) | BigInt(low); + const signBit = BIGINT_1 << BIGINT_63; + return unsigned >= signBit ? unsigned - (BIGINT_1 << BIGINT_64) : unsigned; + } + + /** + * Deserializes a signed 128-bit integer (i128) in little-endian byte order using two's complement. + * @returns A `bigint` in the range [-2^127, 2^127 - 1]. + */ + deserializeI128(): bigint { + const low = this.deserializeU64(); + const high = this.deserializeU64(); + const unsigned = (high << BIGINT_64) | low; + const signBit = BIGINT_1 << BIGINT_127; + return unsigned >= signBit ? unsigned - (BIGINT_1 << BIGINT_128) : unsigned; + } + + /** + * Deserializes a signed 256-bit integer (i256) in little-endian byte order using two's complement. + * @returns A `bigint` in the range [-2^255, 2^255 - 1]. + */ + deserializeI256(): bigint { + const low = this.deserializeU128(); + const high = this.deserializeU128(); + const unsigned = (high << BIGINT_128) | low; + const signBit = BIGINT_1 << BIGINT_255; + return unsigned >= signBit ? unsigned - (BIGINT_1 << BIGINT_256) : unsigned; + } + + // ── ULEB128 ── + + /** + * Deserializes a ULEB128-encoded value as a `u32`. + * ULEB128 is used in BCS to encode vector lengths and enum variant tags. + * @returns A number in the range [0, 4294967295]. + * @throws {Error} If the encoded value overflows a u32. + */ + deserializeUleb128AsU32(): Uint32 { + let value = 0; + let shift = 0; + + for (let i = 0; i < 5; i++) { + const byte = this.deserializeU8(); + + // On the 5th byte (shift=28), only bits 0-3 can contribute to u32 + if (i === 4 && (byte & 0x70) !== 0) { + throw new Error("Overflow while parsing uleb128-encoded uint32 value"); + } + + value = (value | ((byte & 0x7f) << shift)) >>> 0; + + if ((byte & 0x80) === 0) { + return value; + } + shift += 7; + } + + throw new Error("Malformed ULEB128: continuation bit set on terminal byte"); + } + + // ── Composable deserialization ── + + /** + * Deserializes a value of type `T` using the static `deserialize` method of `cls`. + * This is the primary way to read complex types from a byte stream. + * + * @typeParam T - The type to deserialize. + * @param cls - An object (typically a class constructor) with a static `deserialize` method. + * @returns The deserialized value. + * + * @example + * ```typescript + * const myValue = deserializer.deserialize(MyType); + * ``` + */ + deserialize(cls: Deserializable): T { + return cls.deserialize(this); + } + + /** + * Deserializes a BCS vector of values of type `T`. + * Reads a ULEB128 length, then deserializes that many elements using `cls.deserialize`. + * + * @typeParam T - The element type. + * @param cls - An object with a static `deserialize` method for the element type. + * @returns An array of deserialized values. + * + * @example + * ```typescript + * const items = deserializer.deserializeVector(MyType); + * ``` + */ + deserializeVector(cls: Deserializable, maxLen = 65_536): Array { + const length = this.deserializeUleb128AsU32(); + if (length > maxLen) { + throw new Error(`BCS vector length ${length} exceeds maximum allowed ${maxLen}`); + } + const vector: T[] = []; + for (let i = 0; i < length; i += 1) { + vector.push(this.deserialize(cls)); + } + return vector; + } + + // ── Optional ── + + /** + * Deserializes a BCS `Option` value. + * + * Reads a boolean presence flag. If `true`, reads the value using the specified type. + * Returns `undefined` when the option is absent. + * + * Overloads allow specifying the type as `"string"`, `"bytes"`, `"fixedBytes"`, + * or a {@link Deserializable} class. + * + * @param type - `"string"` to read a UTF-8 string; `"bytes"` to read a length-prefixed + * byte array; `"fixedBytes"` to read an exact number of bytes (requires `len`); + * or a `Deserializable` class for structured types. + * @param len - Required when `type` is `"fixedBytes"`: the number of bytes to read. + * @returns The deserialized value, or `undefined` if the option was absent. + * + * @example + * ```typescript + * const name = deserializer.deserializeOption("string"); // string | undefined + * const data = deserializer.deserializeOption("bytes"); // Uint8Array | undefined + * const fixed = deserializer.deserializeOption("fixedBytes", 32); // Uint8Array | undefined + * const myObj = deserializer.deserializeOption(MyType); // MyType | undefined + * ``` + */ + deserializeOption(type: "string"): string | undefined; + deserializeOption(type: "bytes"): Uint8Array | undefined; + deserializeOption(type: "fixedBytes", len: number): Uint8Array | undefined; + deserializeOption(type: Deserializable): T | undefined; + deserializeOption( + type: Deserializable | "string" | "bytes" | "fixedBytes", + len?: number, + ): T | string | Uint8Array | undefined { + const exists = this.deserializeBool(); + if (!exists) return undefined; + + if (type === "string") return this.deserializeStr(); + if (type === "bytes") return this.deserializeBytes(); + if (type === "fixedBytes") { + if (len === undefined) throw new Error("Fixed bytes length not provided"); + return this.deserializeFixedBytes(len); + } + return this.deserialize(type); + } + + /** @deprecated Use `deserializeOption("string")` instead. */ + deserializeOptionStr(): string | undefined { + return this.deserializeOption("string"); + } +} diff --git a/v10/src/bcs/index.ts b/v10/src/bcs/index.ts new file mode 100644 index 000000000..bac09d1c8 --- /dev/null +++ b/v10/src/bcs/index.ts @@ -0,0 +1,71 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * @module bcs + * + * Binary Canonical Serialization (BCS) module. + * + * Provides serialization and deserialization of Move primitive types and structs + * for use in Aptos transactions and on-chain data encoding. + * + * Key exports: + * - {@link Serializer} / {@link Deserializer} — low-level byte I/O + * - {@link Serializable} / {@link Deserializable} — base types for custom structs + * - Move primitives: {@link Bool}, {@link U8}, {@link U16}, {@link U32}, {@link U64}, + * {@link U128}, {@link U256}, {@link I8}, {@link I16}, {@link I32}, {@link I64}, + * {@link I128}, {@link I256} + * - Move structs: {@link MoveVector}, {@link MoveOption}, {@link MoveString}, + * {@link FixedBytes}, {@link EntryFunctionBytes}, {@link Serialized} + * - Numeric bounds constants (e.g. {@link MAX_U64_BIG_INT}, {@link MIN_I8_NUMBER}) + */ +export { + MAX_I8_NUMBER, + MAX_I16_NUMBER, + MAX_I32_NUMBER, + MAX_I64_BIG_INT, + MAX_I128_BIG_INT, + MAX_I256_BIG_INT, + MAX_U8_NUMBER, + MAX_U16_NUMBER, + MAX_U32_NUMBER, + MAX_U64_BIG_INT, + MAX_U128_BIG_INT, + MAX_U256_BIG_INT, + MIN_I8_NUMBER, + MIN_I16_NUMBER, + MIN_I32_NUMBER, + MIN_I64_BIG_INT, + MIN_I128_BIG_INT, + MIN_I256_BIG_INT, +} from "./consts.js"; +export { type Deserializable, Deserializer } from "./deserializer.js"; +export { Bool, I8, I16, I32, I64, I128, I256, U8, U16, U32, U64, U128, U256 } from "./move-primitives.js"; +export { EntryFunctionBytes, FixedBytes, MoveOption, MoveString, MoveVector, Serialized } from "./move-structs.js"; +export { + ensureBoolean, + outOfRangeErrorMessage, + Serializable, + Serializer, + validateNumberInRange, +} from "./serializer.js"; +export type { + AnyNumber, + EntryFunctionArgument, + HexInput, + Int8, + Int16, + Int32, + Int64, + Int128, + Int256, + ScriptFunctionArgument, + TransactionArgument, + Uint8, + Uint16, + Uint32, + Uint64, + Uint128, + Uint256, +} from "./types.js"; +export { ScriptTransactionArgumentVariants } from "./types.js"; diff --git a/v10/src/bcs/move-primitives.ts b/v10/src/bcs/move-primitives.ts new file mode 100644 index 000000000..2f0c4b14b --- /dev/null +++ b/v10/src/bcs/move-primitives.ts @@ -0,0 +1,646 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { + BIGINT_0, + MAX_I8_NUMBER, + MAX_I16_NUMBER, + MAX_I32_NUMBER, + MAX_I64_BIG_INT, + MAX_I128_BIG_INT, + MAX_I256_BIG_INT, + MAX_U8_NUMBER, + MAX_U16_NUMBER, + MAX_U32_NUMBER, + MAX_U64_BIG_INT, + MAX_U128_BIG_INT, + MAX_U256_BIG_INT, + MIN_I8_NUMBER, + MIN_I16_NUMBER, + MIN_I32_NUMBER, + MIN_I64_BIG_INT, + MIN_I128_BIG_INT, + MIN_I256_BIG_INT, +} from "./consts.js"; +import type { Deserializer } from "./deserializer.js"; +import { ensureBoolean, Serializable, type Serializer, validateNumberInRange } from "./serializer.js"; +import type { AnyNumber, Int8, Int16, Int32, TransactionArgument, Uint8, Uint16, Uint32 } from "./types.js"; +import { ScriptTransactionArgumentVariants } from "./types.js"; + +// ── Bool ── + +/** + * A Move `bool` value. Serializes as a single byte (`1` for true, `0` for false). + * Implements {@link TransactionArgument} so it can be passed to both entry functions + * and script functions. + * + * @example + * ```typescript + * const flag = new Bool(true); + * const bytes = flag.bcsToBytes(); // Uint8Array([1]) + * ``` + */ +export class Bool extends Serializable implements TransactionArgument { + /** The underlying boolean value. */ + public readonly value: boolean; + + /** + * @param value - The boolean value. Must be a JavaScript `boolean`. + * @throws {Error} If `value` is not a boolean. + */ + constructor(value: boolean) { + super(); + ensureBoolean(value); + this.value = value; + } + + serialize(serializer: Serializer): void { + serializer.serializeBool(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.Bool); + serializer.serialize(this); + } + + /** + * Deserializes a `Bool` from the given deserializer. + * @param deserializer - The deserializer to read from. + * @returns A new `Bool` instance. + */ + static deserialize(deserializer: Deserializer): Bool { + return new Bool(deserializer.deserializeBool()); + } +} + +// ── Unsigned integers ── + +/** + * A Move `u8` value (unsigned 8-bit integer, range [0, 255]). + * Implements {@link TransactionArgument} for use in entry and script functions. + * + * @example + * ```typescript + * const byte = new U8(42); + * ``` + */ +export class U8 extends Serializable implements TransactionArgument { + /** The underlying u8 value. */ + public readonly value: Uint8; + + /** + * @param value - A number in the range [0, 255]. + * @throws {Error} If `value` is out of range. + */ + constructor(value: Uint8) { + super(); + validateNumberInRange(value, 0, MAX_U8_NUMBER); + this.value = value; + } + + serialize(serializer: Serializer): void { + serializer.serializeU8(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.U8); + serializer.serialize(this); + } + + /** + * Deserializes a `U8` from the given deserializer. + * @param deserializer - The deserializer to read from. + * @returns A new `U8` instance. + */ + static deserialize(deserializer: Deserializer): U8 { + return new U8(deserializer.deserializeU8()); + } +} + +/** + * A Move `u16` value (unsigned 16-bit integer, range [0, 65535]). + * Implements {@link TransactionArgument} for use in entry and script functions. + * + * @example + * ```typescript + * const val = new U16(1000); + * ``` + */ +export class U16 extends Serializable implements TransactionArgument { + /** The underlying u16 value. */ + public readonly value: Uint16; + + /** + * @param value - A number in the range [0, 65535]. + * @throws {Error} If `value` is out of range. + */ + constructor(value: Uint16) { + super(); + validateNumberInRange(value, 0, MAX_U16_NUMBER); + this.value = value; + } + + serialize(serializer: Serializer): void { + serializer.serializeU16(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.U16); + serializer.serialize(this); + } + + /** + * Deserializes a `U16` from the given deserializer. + * @param deserializer - The deserializer to read from. + * @returns A new `U16` instance. + */ + static deserialize(deserializer: Deserializer): U16 { + return new U16(deserializer.deserializeU16()); + } +} + +/** + * A Move `u32` value (unsigned 32-bit integer, range [0, 4294967295]). + * Implements {@link TransactionArgument} for use in entry and script functions. + * + * @example + * ```typescript + * const val = new U32(100000); + * ``` + */ +export class U32 extends Serializable implements TransactionArgument { + /** The underlying u32 value. */ + public readonly value: Uint32; + + /** + * @param value - A number in the range [0, 4294967295]. + * @throws {Error} If `value` is out of range. + */ + constructor(value: Uint32) { + super(); + validateNumberInRange(value, 0, MAX_U32_NUMBER); + this.value = value; + } + + serialize(serializer: Serializer): void { + serializer.serializeU32(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.U32); + serializer.serialize(this); + } + + /** + * Deserializes a `U32` from the given deserializer. + * @param deserializer - The deserializer to read from. + * @returns A new `U32` instance. + */ + static deserialize(deserializer: Deserializer): U32 { + return new U32(deserializer.deserializeU32()); + } +} + +/** + * A Move `u64` value (unsigned 64-bit integer, range [0, 2^64 - 1]). + * The internal value is stored as a `bigint`. + * Accepts both `number` and `bigint` as constructor input. + * Implements {@link TransactionArgument} for use in entry and script functions. + * + * @example + * ```typescript + * const val = new U64(9999999999n); + * ``` + */ +export class U64 extends Serializable implements TransactionArgument { + /** The underlying u64 value, stored as `bigint`. */ + public readonly value: bigint; + + /** + * @param value - A value in the range [0, 2^64 - 1]. Accepts `number` or `bigint`. + * @throws {Error} If `value` is out of range. + */ + constructor(value: AnyNumber) { + super(); + validateNumberInRange(value, BIGINT_0, MAX_U64_BIG_INT); + this.value = BigInt(value); + } + + serialize(serializer: Serializer): void { + serializer.serializeU64(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.U64); + serializer.serialize(this); + } + + /** + * Deserializes a `U64` from the given deserializer. + * @param deserializer - The deserializer to read from. + * @returns A new `U64` instance. + */ + static deserialize(deserializer: Deserializer): U64 { + return new U64(deserializer.deserializeU64()); + } +} + +/** + * A Move `u128` value (unsigned 128-bit integer, range [0, 2^128 - 1]). + * The internal value is stored as a `bigint`. + * Accepts both `number` and `bigint` as constructor input. + * Implements {@link TransactionArgument} for use in entry and script functions. + * + * @example + * ```typescript + * const val = new U128(340282366920938463463374607431768211455n); + * ``` + */ +export class U128 extends Serializable implements TransactionArgument { + /** The underlying u128 value, stored as `bigint`. */ + public readonly value: bigint; + + /** + * @param value - A value in the range [0, 2^128 - 1]. Accepts `number` or `bigint`. + * @throws {Error} If `value` is out of range. + */ + constructor(value: AnyNumber) { + super(); + validateNumberInRange(value, BIGINT_0, MAX_U128_BIG_INT); + this.value = BigInt(value); + } + + serialize(serializer: Serializer): void { + serializer.serializeU128(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.U128); + serializer.serialize(this); + } + + /** + * Deserializes a `U128` from the given deserializer. + * @param deserializer - The deserializer to read from. + * @returns A new `U128` instance. + */ + static deserialize(deserializer: Deserializer): U128 { + return new U128(deserializer.deserializeU128()); + } +} + +/** + * A Move `u256` value (unsigned 256-bit integer, range [0, 2^256 - 1]). + * The internal value is stored as a `bigint`. + * Accepts both `number` and `bigint` as constructor input. + * Implements {@link TransactionArgument} for use in entry and script functions. + * + * @example + * ```typescript + * const val = new U256(1n); + * ``` + */ +export class U256 extends Serializable implements TransactionArgument { + /** The underlying u256 value, stored as `bigint`. */ + public readonly value: bigint; + + /** + * @param value - A value in the range [0, 2^256 - 1]. Accepts `number` or `bigint`. + * @throws {Error} If `value` is out of range. + */ + constructor(value: AnyNumber) { + super(); + validateNumberInRange(value, BIGINT_0, MAX_U256_BIG_INT); + this.value = BigInt(value); + } + + serialize(serializer: Serializer): void { + serializer.serializeU256(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.U256); + serializer.serialize(this); + } + + /** + * Deserializes a `U256` from the given deserializer. + * @param deserializer - The deserializer to read from. + * @returns A new `U256` instance. + */ + static deserialize(deserializer: Deserializer): U256 { + return new U256(deserializer.deserializeU256()); + } +} + +// ── Signed integers ── + +/** + * A Move `i8` value (signed 8-bit integer, range [-128, 127]). + * Implements {@link TransactionArgument} for use in entry and script functions. + * + * @example + * ```typescript + * const val = new I8(-1); + * ``` + */ +export class I8 extends Serializable implements TransactionArgument { + /** The underlying i8 value. */ + public readonly value: Int8; + + /** + * @param value - A number in the range [-128, 127]. + * @throws {Error} If `value` is out of range. + */ + constructor(value: Int8) { + super(); + validateNumberInRange(value, MIN_I8_NUMBER, MAX_I8_NUMBER); + this.value = value; + } + + serialize(serializer: Serializer): void { + serializer.serializeI8(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.I8); + serializer.serialize(this); + } + + /** + * Deserializes an `I8` from the given deserializer. + * @param deserializer - The deserializer to read from. + * @returns A new `I8` instance. + */ + static deserialize(deserializer: Deserializer): I8 { + return new I8(deserializer.deserializeI8()); + } +} + +/** + * A Move `i16` value (signed 16-bit integer, range [-32768, 32767]). + * Implements {@link TransactionArgument} for use in entry and script functions. + * + * @example + * ```typescript + * const val = new I16(-1000); + * ``` + */ +export class I16 extends Serializable implements TransactionArgument { + /** The underlying i16 value. */ + public readonly value: Int16; + + /** + * @param value - A number in the range [-32768, 32767]. + * @throws {Error} If `value` is out of range. + */ + constructor(value: Int16) { + super(); + validateNumberInRange(value, MIN_I16_NUMBER, MAX_I16_NUMBER); + this.value = value; + } + + serialize(serializer: Serializer): void { + serializer.serializeI16(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.I16); + serializer.serialize(this); + } + + /** + * Deserializes an `I16` from the given deserializer. + * @param deserializer - The deserializer to read from. + * @returns A new `I16` instance. + */ + static deserialize(deserializer: Deserializer): I16 { + return new I16(deserializer.deserializeI16()); + } +} + +/** + * A Move `i32` value (signed 32-bit integer, range [-2147483648, 2147483647]). + * Implements {@link TransactionArgument} for use in entry and script functions. + * + * @example + * ```typescript + * const val = new I32(-50000); + * ``` + */ +export class I32 extends Serializable implements TransactionArgument { + /** The underlying i32 value. */ + public readonly value: Int32; + + /** + * @param value - A number in the range [-2147483648, 2147483647]. + * @throws {Error} If `value` is out of range. + */ + constructor(value: Int32) { + super(); + validateNumberInRange(value, MIN_I32_NUMBER, MAX_I32_NUMBER); + this.value = value; + } + + serialize(serializer: Serializer): void { + serializer.serializeI32(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.I32); + serializer.serialize(this); + } + + /** + * Deserializes an `I32` from the given deserializer. + * @param deserializer - The deserializer to read from. + * @returns A new `I32` instance. + */ + static deserialize(deserializer: Deserializer): I32 { + return new I32(deserializer.deserializeI32()); + } +} + +/** + * A Move `i64` value (signed 64-bit integer, range [-2^63, 2^63 - 1]). + * The internal value is stored as a `bigint`. + * Accepts both `number` and `bigint` as constructor input. + * Implements {@link TransactionArgument} for use in entry and script functions. + * + * @example + * ```typescript + * const val = new I64(-9223372036854775808n); + * ``` + */ +export class I64 extends Serializable implements TransactionArgument { + /** The underlying i64 value, stored as `bigint`. */ + public readonly value: bigint; + + /** + * @param value - A value in the range [-2^63, 2^63 - 1]. Accepts `number` or `bigint`. + * @throws {Error} If `value` is out of range. + */ + constructor(value: AnyNumber) { + super(); + validateNumberInRange(value, MIN_I64_BIG_INT, MAX_I64_BIG_INT); + this.value = BigInt(value); + } + + serialize(serializer: Serializer): void { + serializer.serializeI64(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.I64); + serializer.serialize(this); + } + + /** + * Deserializes an `I64` from the given deserializer. + * @param deserializer - The deserializer to read from. + * @returns A new `I64` instance. + */ + static deserialize(deserializer: Deserializer): I64 { + return new I64(deserializer.deserializeI64()); + } +} + +/** + * A Move `i128` value (signed 128-bit integer, range [-2^127, 2^127 - 1]). + * The internal value is stored as a `bigint`. + * Accepts both `number` and `bigint` as constructor input. + * Implements {@link TransactionArgument} for use in entry and script functions. + * + * @example + * ```typescript + * const val = new I128(-1n); + * ``` + */ +export class I128 extends Serializable implements TransactionArgument { + /** The underlying i128 value, stored as `bigint`. */ + public readonly value: bigint; + + /** + * @param value - A value in the range [-2^127, 2^127 - 1]. Accepts `number` or `bigint`. + * @throws {Error} If `value` is out of range. + */ + constructor(value: AnyNumber) { + super(); + validateNumberInRange(value, MIN_I128_BIG_INT, MAX_I128_BIG_INT); + this.value = BigInt(value); + } + + serialize(serializer: Serializer): void { + serializer.serializeI128(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.I128); + serializer.serialize(this); + } + + /** + * Deserializes an `I128` from the given deserializer. + * @param deserializer - The deserializer to read from. + * @returns A new `I128` instance. + */ + static deserialize(deserializer: Deserializer): I128 { + return new I128(deserializer.deserializeI128()); + } +} + +/** + * A Move `i256` value (signed 256-bit integer, range [-2^255, 2^255 - 1]). + * The internal value is stored as a `bigint`. + * Accepts both `number` and `bigint` as constructor input. + * Implements {@link TransactionArgument} for use in entry and script functions. + * + * @example + * ```typescript + * const val = new I256(-1n); + * ``` + */ +export class I256 extends Serializable implements TransactionArgument { + /** The underlying i256 value, stored as `bigint`. */ + public readonly value: bigint; + + /** + * @param value - A value in the range [-2^255, 2^255 - 1]. Accepts `number` or `bigint`. + * @throws {Error} If `value` is out of range. + */ + constructor(value: AnyNumber) { + super(); + validateNumberInRange(value, MIN_I256_BIG_INT, MAX_I256_BIG_INT); + this.value = BigInt(value); + } + + serialize(serializer: Serializer): void { + serializer.serializeI256(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.I256); + serializer.serialize(this); + } + + /** + * Deserializes an `I256` from the given deserializer. + * @param deserializer - The deserializer to read from. + * @returns A new `I256` instance. + */ + static deserialize(deserializer: Deserializer): I256 { + return new I256(deserializer.deserializeI256()); + } +} diff --git a/v10/src/bcs/move-structs.ts b/v10/src/bcs/move-structs.ts new file mode 100644 index 000000000..deb04adc8 --- /dev/null +++ b/v10/src/bcs/move-structs.ts @@ -0,0 +1,694 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { Hex } from "../hex/hex.js"; +import type { Deserializable } from "./deserializer.js"; +import { Deserializer } from "./deserializer.js"; +import { Bool, I8, I16, I32, I64, I128, I256, U8, U16, U32, U64, U128, U256 } from "./move-primitives.js"; +import { Serializable, type Serializer } from "./serializer.js"; +import type { AnyNumber, EntryFunctionArgument, HexInput, TransactionArgument } from "./types.js"; +import { ScriptTransactionArgumentVariants } from "./types.js"; + +const TEXT_ENCODER = new TextEncoder(); + +// ── FixedBytes ── + +/** + * A fixed-size byte array that serializes without a length prefix. + * Implements {@link TransactionArgument} for use in both entry and script functions. + * Accepts hex string input or raw `Uint8Array`. + * + * @example + * ```typescript + * const bytes = new FixedBytes("0xdeadbeef"); + * serializer.serialize(bytes); // writes 4 bytes, no length prefix + * ``` + */ +export class FixedBytes extends Serializable implements TransactionArgument { + /** The raw bytes stored as a `Uint8Array`. */ + public value: Uint8Array; + + /** + * @param value - A hex string (with or without `0x` prefix) or raw bytes. + */ + constructor(value: HexInput) { + super(); + this.value = Hex.fromHexInput(value).toUint8Array(); + } + + serialize(serializer: Serializer): void { + serializer.serializeFixedBytes(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serialize(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + serializer.serialize(this); + } + + /** + * Deserializes a `FixedBytes` of exactly `length` bytes from the given deserializer. + * @param deserializer - The deserializer to read from. + * @param length - The exact number of bytes to read. + * @returns A new `FixedBytes` instance. + */ + static deserialize(deserializer: Deserializer, length: number): FixedBytes { + return new FixedBytes(deserializer.deserializeFixedBytes(length)); + } +} + +// ── EntryFunctionBytes ── + +/** + * Pre-serialized bytes intended for use as an entry function argument. + * When calling `serializeForEntryFunction`, the byte payload is written with a + * ULEB128 length prefix followed by the raw bytes (no additional BCS wrapping). + * + * Use this type when you have already BCS-encoded an argument and want to pass + * it directly to an entry function without double-encoding. + * Implements {@link EntryFunctionArgument}. + */ +export class EntryFunctionBytes extends Serializable implements EntryFunctionArgument { + /** The wrapped fixed-size byte payload. */ + public readonly value: FixedBytes; + + private constructor(value: HexInput) { + super(); + this.value = new FixedBytes(value); + } + + serialize(serializer: Serializer): void { + serializer.serialize(this.value); + } + + /** + * Serializes the byte payload for an entry function argument. + * Writes a ULEB128 length prefix followed by the raw bytes. + * @param serializer - The serializer to write to. + */ + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(this.value.value.length); + serializer.serialize(this); + } + + /** + * Deserializes an `EntryFunctionBytes` of exactly `length` bytes. + * @param deserializer - The deserializer to read from. + * @param length - The exact number of bytes to read. + * @returns A new `EntryFunctionBytes` instance. + */ + static deserialize(deserializer: Deserializer, length: number): EntryFunctionBytes { + const fixedBytes = FixedBytes.deserialize(deserializer, length); + return new EntryFunctionBytes(fixedBytes.value); + } +} + +// ── MoveVector ── + +/** + * A Move `vector` value. Holds an ordered collection of BCS-serializable elements. + * Implements {@link TransactionArgument} so the whole vector can be passed to entry + * or script functions. + * + * Use the static factory methods (e.g. {@link MoveVector.U8}, {@link MoveVector.Bool}) + * to create typed vectors conveniently. + * + * @typeParam T - The element type. Must extend both {@link Serializable} and + * {@link EntryFunctionArgument}. + * + * @example + * ```typescript + * // Create a vector of u8 from a hex string or number array + * const vec = MoveVector.U8("0xdeadbeef"); + * const vec2 = MoveVector.U8([1, 2, 3]); + * + * // Create a vector of strings + * const strs = MoveVector.MoveString(["hello", "world"]); + * ``` + */ +export class MoveVector + extends Serializable + implements TransactionArgument +{ + /** The underlying array of elements. */ + public values: Array; + + /** + * @param values - The array of elements to wrap. + */ + constructor(values: Array, isU8 = false) { + super(); + this.values = values; + this._isU8 = isU8; + } + + /** @internal Tracks whether this vector was created as a U8 vector, for correct script function serialization. */ + readonly _isU8: boolean; + + serialize(serializer: Serializer): void { + serializer.serializeVector(this.values); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + // U8 vectors use a dedicated tag; all other types (including empty non-U8 vectors) use Serialized + if (this._isU8 || (this.values.length > 0 && this.values[0] instanceof U8)) { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.U8Vector); + serializer.serialize(this); + return; + } + const serialized = new Serialized(this.bcsToBytes()); + serialized.serializeForScriptFunction(serializer); + } + + // ── Factory methods ── + + /** + * Creates a `MoveVector` from a `number[]`, `Uint8Array`, or hex string. + * @param values - The source data. + * @returns A new `MoveVector`. + * @throws {Error} If the input type is not recognized. + */ + static U8(values: Array | HexInput): MoveVector { + let numbers: Array; + + if (Array.isArray(values) && values.length === 0) { + numbers = []; + } else if (Array.isArray(values) && typeof values[0] === "number") { + numbers = values; + } else if (typeof values === "string") { + const bytes = Hex.fromHexInput(values).toUint8Array(); + return new MoveVector( + Array.from({ length: bytes.length }, (_, i) => new U8(bytes[i])), + true, + ); + } else if (values instanceof Uint8Array) { + return new MoveVector( + Array.from({ length: values.length }, (_, i) => new U8(values[i])), + true, + ); + } else { + throw new Error("Invalid input type, must be an number[], Uint8Array, or hex string"); + } + + return new MoveVector( + numbers.map((v) => new U8(v)), + true, + ); + } + + /** + * Creates a `MoveVector` from an array of numbers. + * @param values - Array of numbers in the range [0, 65535]. + * @returns A new `MoveVector`. + */ + static U16(values: Array): MoveVector { + return new MoveVector(values.map((v) => new U16(v))); + } + + /** + * Creates a `MoveVector` from an array of numbers. + * @param values - Array of numbers in the range [0, 4294967295]. + * @returns A new `MoveVector`. + */ + static U32(values: Array): MoveVector { + return new MoveVector(values.map((v) => new U32(v))); + } + + /** + * Creates a `MoveVector` from an array of `number | bigint` values. + * @param values - Array of values in the range [0, 2^64 - 1]. + * @returns A new `MoveVector`. + */ + static U64(values: Array): MoveVector { + return new MoveVector(values.map((v) => new U64(v))); + } + + /** + * Creates a `MoveVector` from an array of `number | bigint` values. + * @param values - Array of values in the range [0, 2^128 - 1]. + * @returns A new `MoveVector`. + */ + static U128(values: Array): MoveVector { + return new MoveVector(values.map((v) => new U128(v))); + } + + /** + * Creates a `MoveVector` from an array of `number | bigint` values. + * @param values - Array of values in the range [0, 2^256 - 1]. + * @returns A new `MoveVector`. + */ + static U256(values: Array): MoveVector { + return new MoveVector(values.map((v) => new U256(v))); + } + + /** + * Creates a `MoveVector` from an array of booleans. + * @param values - Array of boolean values. + * @returns A new `MoveVector`. + */ + static Bool(values: Array): MoveVector { + return new MoveVector(values.map((v) => new Bool(v))); + } + + /** + * Creates a `MoveVector` from an array of numbers. + * @param values - Array of numbers in the range [-128, 127]. + * @returns A new `MoveVector`. + */ + static I8(values: Array): MoveVector { + return new MoveVector(values.map((v) => new I8(v))); + } + + /** + * Creates a `MoveVector` from an array of numbers. + * @param values - Array of numbers in the range [-32768, 32767]. + * @returns A new `MoveVector`. + */ + static I16(values: Array): MoveVector { + return new MoveVector(values.map((v) => new I16(v))); + } + + /** + * Creates a `MoveVector` from an array of numbers. + * @param values - Array of numbers in the range [-2147483648, 2147483647]. + * @returns A new `MoveVector`. + */ + static I32(values: Array): MoveVector { + return new MoveVector(values.map((v) => new I32(v))); + } + + /** + * Creates a `MoveVector` from an array of `number | bigint` values. + * @param values - Array of values in the range [-2^63, 2^63 - 1]. + * @returns A new `MoveVector`. + */ + static I64(values: Array): MoveVector { + return new MoveVector(values.map((v) => new I64(v))); + } + + /** + * Creates a `MoveVector` from an array of `number | bigint` values. + * @param values - Array of values in the range [-2^127, 2^127 - 1]. + * @returns A new `MoveVector`. + */ + static I128(values: Array): MoveVector { + return new MoveVector(values.map((v) => new I128(v))); + } + + /** + * Creates a `MoveVector` from an array of `number | bigint` values. + * @param values - Array of values in the range [-2^255, 2^255 - 1]. + * @returns A new `MoveVector`. + */ + static I256(values: Array): MoveVector { + return new MoveVector(values.map((v) => new I256(v))); + } + + /** + * Creates a `MoveVector` from an array of strings. + * @param values - Array of UTF-8 strings. + * @returns A new `MoveVector`. + */ + static MoveString(values: Array): MoveVector { + return new MoveVector(values.map((v) => new MoveString(v))); + } + + /** + * Deserializes a `MoveVector` from the given deserializer. + * Reads a ULEB128 length, then deserializes that many elements using `cls.deserialize`. + * + * @typeParam T - The element type. + * @param deserializer - The deserializer to read from. + * @param cls - A {@link Deserializable} class for the element type. + * @returns A new `MoveVector`. + */ + /** + * Maximum number of elements allowed when deserializing a `MoveVector`. + * Prevents resource exhaustion from malicious or corrupted BCS data. + */ + static readonly MAX_DESERIALIZE_LENGTH = 1_048_576; + + static deserialize( + deserializer: Deserializer, + cls: Deserializable, + ): MoveVector { + const length = deserializer.deserializeUleb128AsU32(); + if (length > MoveVector.MAX_DESERIALIZE_LENGTH) { + throw new Error( + `MoveVector deserialization length ${length} exceeds maximum allowed ${MoveVector.MAX_DESERIALIZE_LENGTH}`, + ); + } + const values: T[] = []; + for (let i = 0; i < length; i += 1) { + values.push(cls.deserialize(deserializer)); + } + return new MoveVector(values, cls === (U8 as unknown)); + } +} + +// ── Serialized ── + +/** + * A pre-serialized BCS blob stored as a length-prefixed byte array. + * Useful for passing already-encoded data through the transaction argument pipeline. + * + * When used as a script function argument, serializes with the + * {@link ScriptTransactionArgumentVariants.Serialized} variant tag. + * + * Implements {@link TransactionArgument}. + * + * @example + * ```typescript + * const blob = new Serialized(someObject.bcsToBytes()); + * ``` + */ +export class Serialized extends Serializable implements TransactionArgument { + /** The raw bytes of the pre-serialized value. */ + public readonly value: Uint8Array; + + /** + * @param value - A hex string or raw bytes representing a pre-serialized BCS payload. + */ + constructor(value: HexInput) { + super(); + this.value = Hex.fromHexInput(value).toUint8Array(); + } + + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + this.serialize(serializer); + } + + serializeForScriptFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.Serialized); + this.serialize(serializer); + } + + /** + * Deserializes a `Serialized` value from the given deserializer. + * Reads a length-prefixed byte array. + * @param deserializer - The deserializer to read from. + * @returns A new `Serialized` instance. + */ + static deserialize(deserializer: Deserializer): Serialized { + return new Serialized(deserializer.deserializeBytes()); + } + + /** + * Decodes the stored bytes as a `MoveVector` using the given element deserializer. + * The bytes are expected to contain a length-prefixed BCS-encoded vector. + * + * @typeParam T - The element type. + * @param cls - A {@link Deserializable} class for the element type. + * @returns A new `MoveVector` decoded from the stored bytes. + */ + toMoveVector(cls: Deserializable): MoveVector { + const deserializer = new Deserializer(this.value); + const vec = deserializer.deserializeVector(cls); + deserializer.assertFinished(); + return new MoveVector(vec); + } +} + +// ── MoveString ── + +/** + * A Move `0x1::string::String` value. Serializes as a UTF-8 string with a + * ULEB128-encoded byte-length prefix. + * Implements {@link TransactionArgument} for use in entry and script functions. + * + * @example + * ```typescript + * const greeting = new MoveString("hello"); + * const bytes = greeting.bcsToBytes(); + * ``` + */ +export class MoveString extends Serializable implements TransactionArgument { + /** The underlying string value. */ + public value: string; + + /** + * @param value - The UTF-8 string to wrap. + */ + constructor(value: string) { + super(); + this.value = value; + } + + serialize(serializer: Serializer): void { + serializer.serializeStr(this.value); + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + serializeForScriptFunction(serializer: Serializer): void { + const fixedStringBytes = TEXT_ENCODER.encode(this.value); + const vectorU8 = MoveVector.U8(fixedStringBytes); + vectorU8.serializeForScriptFunction(serializer); + } + + /** + * Deserializes a `MoveString` from the given deserializer. + * @param deserializer - The deserializer to read from. + * @returns A new `MoveString` instance. + */ + static deserialize(deserializer: Deserializer): MoveString { + return new MoveString(deserializer.deserializeStr()); + } +} + +// ── MoveOption ── + +/** + * A Move `0x1::option::Option` value. Internally represented as a vector + * with zero elements (None) or one element (Some). + * + * Implements {@link EntryFunctionArgument} for use in entry function calls. + * + * Use the static factory methods (e.g. {@link MoveOption.U8}, {@link MoveOption.MoveString}) + * for convenient construction of typed options. + * + * @typeParam T - The inner type. Must extend both {@link Serializable} and + * {@link EntryFunctionArgument}. + * + * @example + * ```typescript + * const some = new MoveOption(new U64(42n)); // Some(42) + * const none = new MoveOption(); // None + * + * // Factory methods + * const optStr = MoveOption.MoveString("hello"); + * const optNone = MoveOption.U8(null); + * ``` + */ +export class MoveOption + extends Serializable + implements EntryFunctionArgument +{ + private vec: MoveVector; + /** + * The contained value, or `undefined` if this option is `None`. + */ + public readonly value?: T; + + /** + * Creates a new `MoveOption`. + * @param value - The inner value, or `undefined` / `null` for `None`. + */ + constructor(value?: T | null) { + super(); + if (typeof value !== "undefined" && value !== null) { + this.vec = new MoveVector([value]); + } else { + this.vec = new MoveVector([]); + } + [this.value] = this.vec.values; + } + + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + /** + * Returns the inner value, throwing if this option is `None`. + * @returns The contained value. + * @throws {Error} If the option does not contain a value. + */ + unwrap(): T { + if (!this.isSome()) { + throw new Error("Called unwrap on a MoveOption with no value"); + } + return this.vec.values[0]; + } + + /** + * Returns `true` if this option contains a value (`Some`), `false` otherwise (`None`). + * @returns Whether the option has a value. + */ + isSome(): boolean { + return this.vec.values.length === 1; + } + + serialize(serializer: Serializer): void { + this.vec.serialize(serializer); + } + + // ── Factory methods ── + + /** + * Creates a `MoveOption`. Pass `null` or `undefined` to create `None`. + * @param value - An optional number in the range [0, 255]. + * @returns A new `MoveOption`. + */ + static U8(value?: number | null): MoveOption { + return new MoveOption(value !== null && value !== undefined ? new U8(value) : undefined); + } + + /** + * Creates a `MoveOption`. Pass `null` or `undefined` to create `None`. + * @param value - An optional number in the range [0, 65535]. + * @returns A new `MoveOption`. + */ + static U16(value?: number | null): MoveOption { + return new MoveOption(value !== null && value !== undefined ? new U16(value) : undefined); + } + + /** + * Creates a `MoveOption`. Pass `null` or `undefined` to create `None`. + * @param value - An optional number in the range [0, 4294967295]. + * @returns A new `MoveOption`. + */ + static U32(value?: number | null): MoveOption { + return new MoveOption(value !== null && value !== undefined ? new U32(value) : undefined); + } + + /** + * Creates a `MoveOption`. Pass `null` or `undefined` to create `None`. + * @param value - An optional value in the range [0, 2^64 - 1]. + * @returns A new `MoveOption`. + */ + static U64(value?: AnyNumber | null): MoveOption { + return new MoveOption(value !== null && value !== undefined ? new U64(value) : undefined); + } + + /** + * Creates a `MoveOption`. Pass `null` or `undefined` to create `None`. + * @param value - An optional value in the range [0, 2^128 - 1]. + * @returns A new `MoveOption`. + */ + static U128(value?: AnyNumber | null): MoveOption { + return new MoveOption(value !== null && value !== undefined ? new U128(value) : undefined); + } + + /** + * Creates a `MoveOption`. Pass `null` or `undefined` to create `None`. + * @param value - An optional value in the range [0, 2^256 - 1]. + * @returns A new `MoveOption`. + */ + static U256(value?: AnyNumber | null): MoveOption { + return new MoveOption(value !== null && value !== undefined ? new U256(value) : undefined); + } + + /** + * Creates a `MoveOption`. Pass `null` or `undefined` to create `None`. + * @param value - An optional boolean. + * @returns A new `MoveOption`. + */ + static Bool(value?: boolean | null): MoveOption { + return new MoveOption(value !== null && value !== undefined ? new Bool(value) : undefined); + } + + /** + * Creates a `MoveOption`. Pass `null` or `undefined` to create `None`. + * @param value - An optional number in the range [-128, 127]. + * @returns A new `MoveOption`. + */ + static I8(value?: number | null): MoveOption { + return new MoveOption(value !== null && value !== undefined ? new I8(value) : undefined); + } + + /** + * Creates a `MoveOption`. Pass `null` or `undefined` to create `None`. + * @param value - An optional number in the range [-32768, 32767]. + * @returns A new `MoveOption`. + */ + static I16(value?: number | null): MoveOption { + return new MoveOption(value !== null && value !== undefined ? new I16(value) : undefined); + } + + /** + * Creates a `MoveOption`. Pass `null` or `undefined` to create `None`. + * @param value - An optional number in the range [-2147483648, 2147483647]. + * @returns A new `MoveOption`. + */ + static I32(value?: number | null): MoveOption { + return new MoveOption(value !== null && value !== undefined ? new I32(value) : undefined); + } + + /** + * Creates a `MoveOption`. Pass `null` or `undefined` to create `None`. + * @param value - An optional value in the range [-2^63, 2^63 - 1]. + * @returns A new `MoveOption`. + */ + static I64(value?: AnyNumber | null): MoveOption { + return new MoveOption(value !== null && value !== undefined ? new I64(value) : undefined); + } + + /** + * Creates a `MoveOption`. Pass `null` or `undefined` to create `None`. + * @param value - An optional value in the range [-2^127, 2^127 - 1]. + * @returns A new `MoveOption`. + */ + static I128(value?: AnyNumber | null): MoveOption { + return new MoveOption(value !== null && value !== undefined ? new I128(value) : undefined); + } + + /** + * Creates a `MoveOption`. Pass `null` or `undefined` to create `None`. + * @param value - An optional value in the range [-2^255, 2^255 - 1]. + * @returns A new `MoveOption`. + */ + static I256(value?: AnyNumber | null): MoveOption { + return new MoveOption(value !== null && value !== undefined ? new I256(value) : undefined); + } + + /** + * Creates a `MoveOption`. Pass `null` or `undefined` to create `None`. + * @param value - An optional UTF-8 string. + * @returns A new `MoveOption`. + */ + static MoveString(value?: string | null): MoveOption { + return new MoveOption(value !== null && value !== undefined ? new MoveString(value) : undefined); + } + + /** + * Deserializes a `MoveOption` from the given deserializer. + * Reads the underlying vector (0 or 1 elements) and wraps in a `MoveOption`. + * + * @typeParam U - The inner element type. + * @param deserializer - The deserializer to read from. + * @param cls - A {@link Deserializable} class for the inner type. + * @returns A new `MoveOption`. + */ + static deserialize( + deserializer: Deserializer, + cls: Deserializable, + ): MoveOption { + const vector = MoveVector.deserialize(deserializer, cls); + if (vector.values.length > 1) { + throw new Error(`MoveOption deserialized with ${vector.values.length} elements; expected 0 or 1`); + } + return new MoveOption(vector.values[0]); + } +} diff --git a/v10/src/bcs/serializer.ts b/v10/src/bcs/serializer.ts new file mode 100644 index 000000000..2804ea4eb --- /dev/null +++ b/v10/src/bcs/serializer.ts @@ -0,0 +1,555 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { Hex } from "../hex/hex.js"; +import { + BIGINT_0, + BIGINT_1, + BIGINT_32, + BIGINT_64, + BIGINT_128, + BIGINT_256, + BIGINT_MAX_U32, + MAX_I8_NUMBER, + MAX_I16_NUMBER, + MAX_I32_NUMBER, + MAX_I64_BIG_INT, + MAX_I128_BIG_INT, + MAX_I256_BIG_INT, + MAX_U8_NUMBER, + MAX_U16_NUMBER, + MAX_U32_NUMBER, + MAX_U64_BIG_INT, + MAX_U128_BIG_INT, + MAX_U256_BIG_INT, + MIN_I8_NUMBER, + MIN_I16_NUMBER, + MIN_I32_NUMBER, + MIN_I64_BIG_INT, + MIN_I128_BIG_INT, + MIN_I256_BIG_INT, +} from "./consts.js"; +import type { AnyNumber, Uint8, Uint16, Uint32 } from "./types.js"; + +const TEXT_ENCODER = new TextEncoder(); +const MIN_BUFFER_GROWTH = 256; + +// ── Serializer pool ── + +const serializerPool: Serializer[] = []; +const MAX_POOL_SIZE = 8; + +function acquireSerializer(): Serializer { + return serializerPool.pop() ?? new Serializer(); +} + +function releaseSerializer(serializer: Serializer): void { + serializer.reset(); + if (serializerPool.length < MAX_POOL_SIZE) { + serializerPool.push(serializer); + } +} + +// ── Serializable base class ── + +/** + * Abstract base class for types that can be BCS-serialized. + * + * Extend this class and implement {@link serialize} to make a type serializable. + * The base class provides convenience methods for converting to bytes and hex. + * + * @example + * ```typescript + * class MyType extends Serializable { + * constructor(public value: number) { super(); } + * + * serialize(serializer: Serializer): void { + * serializer.serializeU32(this.value); + * } + * } + * + * const obj = new MyType(42); + * const bytes = obj.bcsToBytes(); // Uint8Array + * const hex = obj.bcsToHex(); // Hex + * console.log(obj.toString()); // "0x0000002a" + * ``` + */ +export abstract class Serializable { + /** + * Writes this value into the provided serializer using BCS encoding. + * @param serializer - The serializer to write to. + */ + abstract serialize(serializer: Serializer): void; + + /** + * Returns the BCS-encoded bytes for this value. + * @returns A `Uint8Array` containing the serialized bytes. + */ + bcsToBytes(): Uint8Array { + const serializer = acquireSerializer(); + try { + this.serialize(serializer); + return serializer.toUint8Array(); + } finally { + releaseSerializer(serializer); + } + } + + /** + * Returns the BCS-encoded bytes as a {@link Hex} object. + * @returns A `Hex` instance wrapping the serialized bytes. + */ + bcsToHex(): Hex { + return Hex.fromHexInput(this.bcsToBytes()); + } + + /** + * Returns the hex-encoded BCS bytes as a string without the `0x` prefix. + * @returns A lowercase hex string without leading `0x`. + */ + toStringWithoutPrefix(): string { + return this.bcsToHex().toStringWithoutPrefix(); + } + + /** + * Returns the hex-encoded BCS bytes as a `0x`-prefixed string. + * @returns A lowercase hex string with leading `0x`. + */ + toString(): string { + return `0x${this.toStringWithoutPrefix()}`; + } +} + +// ── Serializer ── + +/** + * BCS (Binary Canonical Serialization) serializer for encoding Move and Aptos data types. + * + * Writes values into a growable internal byte buffer in little-endian order. + * Call {@link toUint8Array} to obtain the serialized bytes when done. + * + * @example + * ```typescript + * const serializer = new Serializer(); + * serializer.serializeU8(255); + * serializer.serializeStr("hello"); + * const bytes = serializer.toUint8Array(); + * ``` + */ +export class Serializer { + private buffer: ArrayBuffer; + private offset: number; + private dataView: DataView; + + /** + * Creates a new Serializer with an internal buffer of the specified initial size. + * The buffer grows automatically as data is written. + * @param length - Initial buffer capacity in bytes. Defaults to 64. Must be greater than 0. + */ + constructor(length: number = 64) { + if (length <= 0) { + throw new Error("Length needs to be greater than 0"); + } + this.buffer = new ArrayBuffer(length); + this.dataView = new DataView(this.buffer); + this.offset = 0; + } + + private ensureBufferWillHandleSize(bytes: number) { + const requiredSize = this.offset + bytes; + if (this.buffer.byteLength >= requiredSize) return; + + const growthSize = Math.max(Math.floor(this.buffer.byteLength * 1.5), requiredSize + MIN_BUFFER_GROWTH); + const newBuffer = new ArrayBuffer(growthSize); + new Uint8Array(newBuffer).set(new Uint8Array(this.buffer, 0, this.offset)); + this.buffer = newBuffer; + this.dataView = new DataView(this.buffer); + } + + protected appendToBuffer(values: Uint8Array) { + this.ensureBufferWillHandleSize(values.length); + new Uint8Array(this.buffer, this.offset).set(values); + this.offset += values.length; + } + + private serializeWithFunction( + fn: (byteOffset: number, value: number, littleEndian?: boolean) => void, + bytesLength: number, + value: number, + ) { + this.ensureBufferWillHandleSize(bytesLength); + fn.apply(this.dataView, [this.offset, value, true]); + this.offset += bytesLength; + } + + // ── String / Bytes ── + + /** + * Serializes a UTF-8 string with a ULEB128 length prefix. + * @param value - The string to serialize. + */ + serializeStr(value: string) { + this.serializeBytes(TEXT_ENCODER.encode(value)); + } + + /** + * Serializes a byte array with a ULEB128 length prefix. + * @param value - The bytes to serialize. + */ + serializeBytes(value: Uint8Array) { + this.serializeU32AsUleb128(value.length); + this.appendToBuffer(value); + } + + /** + * Serializes a byte array without a length prefix (fixed-size encoding). + * The deserializer must know the expected length in advance. + * @param value - The bytes to serialize. + */ + serializeFixedBytes(value: Uint8Array) { + this.appendToBuffer(value); + } + + // ── Boolean ── + + /** + * Serializes a boolean as a single byte (`1` for true, `0` for false). + * @param value - The boolean value to serialize. + */ + serializeBool(value: boolean) { + ensureBoolean(value); + this.ensureBufferWillHandleSize(1); + new Uint8Array(this.buffer, this.offset, 1)[0] = value ? 1 : 0; + this.offset += 1; + } + + // ── Unsigned integers ── + + /** + * Serializes an unsigned 8-bit integer (u8). + * @param value - A number in the range [0, 255]. + */ + serializeU8(value: Uint8) { + validateNumberInRange(value, 0, MAX_U8_NUMBER); + this.ensureBufferWillHandleSize(1); + new Uint8Array(this.buffer, this.offset, 1)[0] = value; + this.offset += 1; + } + + /** + * Serializes an unsigned 16-bit integer (u16) in little-endian byte order. + * @param value - A number in the range [0, 65535]. + */ + serializeU16(value: Uint16) { + validateNumberInRange(value, 0, MAX_U16_NUMBER); + this.serializeWithFunction(DataView.prototype.setUint16, 2, value); + } + + /** + * Serializes an unsigned 32-bit integer (u32) in little-endian byte order. + * @param value - A number in the range [0, 4294967295]. + */ + serializeU32(value: Uint32) { + validateNumberInRange(value, 0, MAX_U32_NUMBER); + this.serializeWithFunction(DataView.prototype.setUint32, 4, value); + } + + /** + * Serializes an unsigned 64-bit integer (u64) in little-endian byte order. + * Accepts both `number` and `bigint` input. + * @param value - A value in the range [0, 2^64 - 1]. + */ + serializeU64(value: AnyNumber) { + validateNumberInRange(value, BIGINT_0, MAX_U64_BIG_INT); + const v = BigInt(value); + this.serializeU32(Number(v & BIGINT_MAX_U32)); + this.serializeU32(Number(v >> BIGINT_32)); + } + + /** + * Serializes an unsigned 128-bit integer (u128) in little-endian byte order. + * Accepts both `number` and `bigint` input. + * @param value - A value in the range [0, 2^128 - 1]. + */ + serializeU128(value: AnyNumber) { + validateNumberInRange(value, BIGINT_0, MAX_U128_BIG_INT); + const v = BigInt(value); + this.serializeU64(v & MAX_U64_BIG_INT); + this.serializeU64(v >> BIGINT_64); + } + + /** + * Serializes an unsigned 256-bit integer (u256) in little-endian byte order. + * Accepts both `number` and `bigint` input. + * @param value - A value in the range [0, 2^256 - 1]. + */ + serializeU256(value: AnyNumber) { + validateNumberInRange(value, BIGINT_0, MAX_U256_BIG_INT); + const v = BigInt(value); + this.serializeU128(v & MAX_U128_BIG_INT); + this.serializeU128(v >> BIGINT_128); + } + + // ── Signed integers ── + + /** + * Serializes a signed 8-bit integer (i8). + * @param value - A number in the range [-128, 127]. + */ + serializeI8(value: number) { + validateNumberInRange(value, MIN_I8_NUMBER, MAX_I8_NUMBER); + this.serializeWithFunction(DataView.prototype.setInt8, 1, value); + } + + /** + * Serializes a signed 16-bit integer (i16) in little-endian byte order. + * @param value - A number in the range [-32768, 32767]. + */ + serializeI16(value: number) { + validateNumberInRange(value, MIN_I16_NUMBER, MAX_I16_NUMBER); + this.serializeWithFunction(DataView.prototype.setInt16, 2, value); + } + + /** + * Serializes a signed 32-bit integer (i32) in little-endian byte order. + * @param value - A number in the range [-2147483648, 2147483647]. + */ + serializeI32(value: number) { + validateNumberInRange(value, MIN_I32_NUMBER, MAX_I32_NUMBER); + this.serializeWithFunction(DataView.prototype.setInt32, 4, value); + } + + /** + * Serializes a signed 64-bit integer (i64) in little-endian byte order using two's complement. + * Accepts both `number` and `bigint` input. + * @param value - A value in the range [-2^63, 2^63 - 1]. + */ + serializeI64(value: AnyNumber) { + validateNumberInRange(value, MIN_I64_BIG_INT, MAX_I64_BIG_INT); + const val = BigInt(value); + const unsigned = val < 0 ? (BIGINT_1 << BIGINT_64) + val : val; + const low = unsigned & BIGINT_MAX_U32; + const high = unsigned >> BIGINT_32; + this.serializeU32(Number(low)); + this.serializeU32(Number(high)); + } + + /** + * Serializes a signed 128-bit integer (i128) in little-endian byte order using two's complement. + * Accepts both `number` and `bigint` input. + * @param value - A value in the range [-2^127, 2^127 - 1]. + */ + serializeI128(value: AnyNumber) { + validateNumberInRange(value, MIN_I128_BIG_INT, MAX_I128_BIG_INT); + const val = BigInt(value); + const unsigned = val < 0 ? (BIGINT_1 << BIGINT_128) + val : val; + const low = unsigned & MAX_U64_BIG_INT; + const high = unsigned >> BIGINT_64; + this.serializeU64(low); + this.serializeU64(high); + } + + /** + * Serializes a signed 256-bit integer (i256) in little-endian byte order using two's complement. + * Accepts both `number` and `bigint` input. + * @param value - A value in the range [-2^255, 2^255 - 1]. + */ + serializeI256(value: AnyNumber) { + validateNumberInRange(value, MIN_I256_BIG_INT, MAX_I256_BIG_INT); + const val = BigInt(value); + const unsigned = val < 0 ? (BIGINT_1 << BIGINT_256) + val : val; + const low = unsigned & MAX_U128_BIG_INT; + const high = unsigned >> BIGINT_128; + this.serializeU128(low); + this.serializeU128(high); + } + + // ── ULEB128 ── + + /** + * Serializes a `u32` value using unsigned LEB128 (ULEB128) variable-length encoding. + * ULEB128 is used in BCS to encode vector lengths and enum variants. + * @param val - A value in the range [0, 4294967295]. + */ + serializeU32AsUleb128(val: Uint32) { + validateNumberInRange(val, 0, MAX_U32_NUMBER); + this.ensureBufferWillHandleSize(5); + const view = new Uint8Array(this.buffer, this.offset); + let value = val; + let i = 0; + while (value >>> 7 !== 0) { + view[i++] = (value & 0x7f) | 0x80; + value >>>= 7; + } + view[i++] = value; + this.offset += i; + } + + // ── Output / management ── + + /** + * Returns a copy of the serialized bytes written so far. + * @returns A new `Uint8Array` containing the serialized data. + */ + toUint8Array(): Uint8Array { + return new Uint8Array(this.buffer, 0, this.offset).slice(); + } + + /** + * Returns a non-copying view of the serialized bytes written so far. + * + * **Warning:** The returned view is backed by the serializer's internal buffer. + * It becomes invalid if any further writes cause the buffer to resize. Only use + * this method when you need zero-copy access and will consume the bytes immediately + * (e.g. passing to a hash update). For most use cases, prefer {@link toUint8Array}. + * + * @internal + * @returns A `Uint8Array` view into the internal buffer. + */ + toUint8ArrayView(): Uint8Array { + return new Uint8Array(this.buffer, 0, this.offset); + } + + /** + * Resets the serializer to an empty state, zeroing existing bytes and resetting the write offset. + * The internal buffer capacity is retained. Useful when reusing a serializer from a pool. + */ + reset(): void { + if (this.offset > 0) { + new Uint8Array(this.buffer, 0, this.offset).fill(0); + } + this.offset = 0; + } + + /** + * Returns the current write offset (i.e. the number of bytes written so far). + * @returns The number of bytes written. + */ + getOffset(): number { + return this.offset; + } + + // ── Composable serialization ── + + /** + * Serializes a {@link Serializable} value by delegating to its `serialize` method. + * @param value - The serializable object to write. + */ + serialize(value: T): void { + value.serialize(this); + } + + /** + * Serializes a {@link Serializable} value as a length-prefixed byte blob. + * First serializes `value` to a temporary buffer, then writes the result + * with a ULEB128 length prefix (equivalent to `serializeBytes(value.bcsToBytes())`). + * @param value - The serializable object to wrap in a byte blob. + */ + serializeAsBytes(value: T): void { + const tempSerializer = acquireSerializer(); + try { + value.serialize(tempSerializer); + this.serializeBytes(tempSerializer.toUint8ArrayView()); + } finally { + releaseSerializer(tempSerializer); + } + } + + /** + * Serializes an array of {@link Serializable} values as a BCS vector. + * Writes a ULEB128 length prefix followed by each element serialized in order. + * @param values - The array of serializable objects to write. + */ + serializeVector(values: Array): void { + this.serializeU32AsUleb128(values.length); + for (const item of values) { + item.serialize(this); + } + } + + /** + * Serializes an optional value as a BCS `Option`. + * Writes a boolean presence flag (`true` = some, `false` = none), followed by + * the value itself if present. + * + * @param value - The optional value to serialize. Pass `undefined` to encode `None`. + * @param len - Required when `value` is a `Uint8Array` and should be written without + * a length prefix (fixed bytes). Omit for length-prefixed bytes. + */ + serializeOption(value?: T, len?: number): void { + const hasValue = value !== undefined; + this.serializeBool(hasValue); + if (hasValue) { + if (typeof value === "string") { + this.serializeStr(value); + } else if (value instanceof Uint8Array) { + if (len !== undefined) { + this.serializeFixedBytes(value); + } else { + this.serializeBytes(value); + } + } else { + value.serialize(this); + } + } + } + + /** @deprecated Use `serializeOption` instead. */ + serializeOptionStr(value?: string): void { + if (value === undefined) { + this.serializeU32AsUleb128(0); + } else { + this.serializeU32AsUleb128(1); + this.serializeStr(value); + } + } +} + +// ── Validation helpers ── + +/** + * Asserts that the given value is a boolean. Throws if it is not. + * Used as a type guard: after this call TypeScript knows `value` is `boolean`. + * @param value - The value to check. + * @throws {Error} If `value` is not a boolean. + */ +export function ensureBoolean(value: unknown): asserts value is boolean { + if (typeof value !== "boolean") { + throw new Error(`${value} is not a boolean value`); + } +} + +/** + * Builds a human-readable error message for an out-of-range numeric value. + * @param value - The value that is out of range. + * @param min - The minimum allowed value (inclusive). + * @param max - The maximum allowed value (inclusive). + * @returns A descriptive error string. + */ +export const outOfRangeErrorMessage = (value: AnyNumber, min: AnyNumber, max: AnyNumber) => + `${value} is out of range: [${min}, ${max}]`; + +/** + * Validates that `value` falls within the inclusive range `[minValue, maxValue]`. + * Throws with a descriptive message if the value is out of range. + * Both `number` and `bigint` values are supported via `AnyNumber`. + * + * @param value - The value to validate. + * @param minValue - The minimum allowed value (inclusive). + * @param maxValue - The maximum allowed value (inclusive). + * @throws {Error} If `value` is less than `minValue` or greater than `maxValue`. + */ +export function validateNumberInRange(value: T, minValue: T, maxValue: T) { + if (typeof value === "number" && typeof minValue === "number" && typeof maxValue === "number") { + if (value > maxValue || value < minValue) { + throw new Error(outOfRangeErrorMessage(value, minValue, maxValue)); + } + return; + } + const v = typeof value === "bigint" ? value : BigInt(value); + const hi = typeof maxValue === "bigint" ? maxValue : BigInt(maxValue); + const lo = typeof minValue === "bigint" ? minValue : BigInt(minValue); + if (v > hi || v < lo) { + throw new Error(outOfRangeErrorMessage(value, minValue, maxValue)); + } +} diff --git a/v10/src/bcs/types.ts b/v10/src/bcs/types.ts new file mode 100644 index 000000000..15a54304b --- /dev/null +++ b/v10/src/bcs/types.ts @@ -0,0 +1,111 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { Hex } from "../hex/hex.js"; +import type { Serializer } from "./serializer.js"; + +// ── Numeric type aliases ── + +/** An unsigned 8-bit integer (0 to 255). Represented as a JavaScript `number`. */ +export type Uint8 = number; +/** An unsigned 16-bit integer (0 to 65535). Represented as a JavaScript `number`. */ +export type Uint16 = number; +/** An unsigned 32-bit integer (0 to 4,294,967,295). Represented as a JavaScript `number`. */ +export type Uint32 = number; +/** An unsigned 64-bit integer. Represented as a JavaScript `bigint` due to its size. */ +export type Uint64 = bigint; +/** An unsigned 128-bit integer. Represented as a JavaScript `bigint`. */ +export type Uint128 = bigint; +/** An unsigned 256-bit integer. Represented as a JavaScript `bigint`. */ +export type Uint256 = bigint; + +/** A signed 8-bit integer (-128 to 127). Represented as a JavaScript `number`. */ +export type Int8 = number; +/** A signed 16-bit integer (-32768 to 32767). Represented as a JavaScript `number`. */ +export type Int16 = number; +/** A signed 32-bit integer (-2,147,483,648 to 2,147,483,647). Represented as a JavaScript `number`. */ +export type Int32 = number; +/** A signed 64-bit integer. Represented as a JavaScript `bigint` due to its size. */ +export type Int64 = bigint; +/** A signed 128-bit integer. Represented as a JavaScript `bigint`. */ +export type Int128 = bigint; +/** A signed 256-bit integer. Represented as a JavaScript `bigint`. */ +export type Int256 = bigint; + +/** A value that can be either a JavaScript `number` or `bigint`. Used for numeric BCS serialization methods that accept both. */ +export type AnyNumber = number | bigint; + +/** Hex string or raw bytes. Re-exported from hex module for convenience. */ +export type { HexInput } from "../hex/hex.js"; + +// ── Transaction argument interfaces ── +// These define how BCS values are serialized in different transaction contexts. + +/** + * A value that can be used as an argument in both entry functions and script functions. + * Combines {@link EntryFunctionArgument} and {@link ScriptFunctionArgument}. + */ +export interface TransactionArgument extends EntryFunctionArgument, ScriptFunctionArgument {} + +/** + * A value that can be serialized as an argument to an entry function transaction. + * Entry function arguments are BCS-encoded and length-prefixed. + */ +export interface EntryFunctionArgument { + /** Serializes the value using standard BCS encoding. */ + serialize(serializer: Serializer): void; + /** + * Serializes the value in the format required for entry function arguments, + * which wraps the BCS bytes with a ULEB128-encoded length prefix. + * @param serializer - The serializer to write to. + */ + serializeForEntryFunction(serializer: Serializer): void; + /** Returns the BCS-encoded bytes for this value. */ + bcsToBytes(): Uint8Array; + /** Returns the BCS-encoded bytes as a {@link Hex} object. */ + bcsToHex(): Hex; +} + +/** + * A value that can be serialized as an argument to a script function transaction. + * Script function arguments are BCS-encoded and tagged with a variant enum. + */ +export interface ScriptFunctionArgument { + /** Serializes the value using standard BCS encoding. */ + serialize(serializer: Serializer): void; + /** + * Serializes the value in the format required for script function arguments, + * which prepends a ULEB128-encoded variant tag from {@link ScriptTransactionArgumentVariants}. + * @param serializer - The serializer to write to. + */ + serializeForScriptFunction(serializer: Serializer): void; + /** Returns the BCS-encoded bytes for this value. */ + bcsToBytes(): Uint8Array; + /** Returns the BCS-encoded bytes as a {@link Hex} object. */ + bcsToHex(): Hex; +} + +// ── Variant enums for script transaction arguments ── + +/** + * Variant discriminants used when encoding Move values as script transaction arguments. + * Each value is prepended as a ULEB128 tag to identify the type of the following argument. + */ +export enum ScriptTransactionArgumentVariants { + U8 = 0, + U64 = 1, + U128 = 2, + Address = 3, + U8Vector = 4, + Bool = 5, + U16 = 6, + U32 = 7, + U256 = 8, + Serialized = 9, + I8 = 10, + I16 = 11, + I32 = 12, + I64 = 13, + I128 = 14, + I256 = 15, +} diff --git a/v10/src/client/aptos-request.ts b/v10/src/client/aptos-request.ts new file mode 100644 index 000000000..8fb7eb553 --- /dev/null +++ b/v10/src/client/aptos-request.ts @@ -0,0 +1,175 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { bcsRequest, jsonRequest } from "@aptos-labs/aptos-client"; +import { AptosApiError } from "../core/errors.js"; +import { VERSION } from "../version.js"; +import type { AptosRequest, AptosResponse, Client, ClientConfig, ClientRequest } from "./types.js"; +import { AptosApiType, MimeType } from "./types.js"; + +/** RFC 7230 token characters for header field names. */ +const SAFE_HEADER_KEY = /^[!#$%&'*+\-.^_`|~0-9a-zA-Z]+$/; + +function validateHeaderValue(key: string, value: string): void { + if (/[\r\n\0]/.test(value)) { + throw new Error(`Header value for '${key}' contains illegal characters (CR, LF, or NUL)`); + } +} + +function mergeHeaders( + overrides: ClientConfig | undefined, + contentType: string, + acceptType: string, + originMethod?: string, +): Record { + const headers: Record = { + "content-type": contentType, + accept: acceptType, + "x-aptos-client": `aptos-typescript-sdk/${VERSION}`, + }; + if (originMethod) { + headers["x-aptos-typescript-sdk-origin-method"] = originMethod; + } + if (overrides?.HEADERS) { + for (const [key, value] of Object.entries(overrides.HEADERS)) { + if (!SAFE_HEADER_KEY.test(key)) { + throw new Error(`Invalid header key: '${key}'`); + } + const strValue = String(value); + validateHeaderValue(key, strValue); + headers[key] = strValue; + } + } + if (overrides?.AUTH_TOKEN) { + validateHeaderValue("Authorization", overrides.AUTH_TOKEN); + headers.Authorization = `Bearer ${overrides.AUTH_TOKEN}`; + } else if (overrides?.API_KEY) { + validateHeaderValue("Authorization", overrides.API_KEY); + headers.Authorization = `Bearer ${overrides.API_KEY}`; + } + return headers; +} + +/** + * Convert raw response headers (plain object from `got`, or `Headers` from `fetch`) + * into a standard `Headers` instance for consistent API access. + */ +function toHeaders(raw: unknown): Headers { + if (raw instanceof Headers) return raw; + const h = new Headers(); + if (raw && typeof raw === "object") { + for (const [key, value] of Object.entries(raw as Record)) { + if (Array.isArray(value)) { + for (const v of value) h.append(key, v); + } else if (value != null) { + h.set(key, String(value)); + } + } + } + return h; +} + +/** + * Executes a single HTTP request to an Aptos API endpoint and returns the parsed response. + * + * Uses `@aptos-labs/aptos-client` under the hood, which provides HTTP/2 in Node.js + * (via `got`) and native `fetch` in browsers. + * + * Handles: + * - Building the full URL with query string parameters + * - Setting `content-type`, `accept`, SDK version, and authorization headers + * - Serializing the request body (JSON or BCS bytes) + * - Parsing the response body (JSON via `aptosClient`, binary via `bcsRequest`) + * - Throwing an {@link AptosApiError} for non-2xx responses, 401, or GraphQL errors + * + * This is the low-level request primitive used by {@link get} and {@link post}. + * Most callers should prefer those helpers rather than calling `aptosRequest` directly. + * + * @typeParam Res - The expected type of the parsed response body. + * @param options - The request options (URL, method, path, body, headers, etc.). + * @param apiType - The Aptos API service being called (used for error context). + * @param client - Optional custom HTTP client. When provided, `client.sendRequest()` is used + * instead of the default `jsonRequest`/`bcsRequest` from `@aptos-labs/aptos-client`. + * @returns A promise that resolves to an {@link AptosResponse} containing the parsed data. + * @throws {AptosApiError} If the response is an error (4xx/5xx or a GraphQL error payload). + */ +export async function aptosRequest( + options: AptosRequest, + apiType: AptosApiType, + client?: Client, +): Promise> { + const { url, method, path, body, contentType, acceptType, params, originMethod, overrides } = options; + + const fullUrl = path ? `${url}/${path}` : url; + + // Validate URL scheme to prevent SSRF via non-HTTP protocols + if (!fullUrl.startsWith("https://") && !fullUrl.startsWith("http://")) { + throw new Error(`Invalid URL scheme: ${fullUrl}. Only http:// and https:// are supported.`); + } + + const headers = mergeHeaders(overrides, contentType ?? MimeType.JSON, acceptType ?? MimeType.JSON, originMethod); + + // Filter out undefined param values — got's searchParams would stringify them as "undefined" + const filteredParams = params + ? Object.fromEntries(Object.entries(params).filter(([, v]) => v !== undefined)) + : undefined; + + const isBcs = acceptType != null && acceptType !== MimeType.JSON; + + const clientRequest: ClientRequest = { + url: fullUrl, + method, + body: body !== undefined && method === "POST" ? body : undefined, + params: filteredParams as ClientRequest["params"], + headers, + http2: overrides?.http2, + }; + + const response = client + ? await client.sendRequest(clientRequest) + : isBcs + ? await bcsRequest(clientRequest) + : await jsonRequest(clientRequest); + + // Normalize: bcsRequest returns Buffer (Node) or ArrayBuffer (browser) — always + // produce a plain Uint8Array so callers get a consistent type. + const data: unknown = isBcs ? new Uint8Array(response.data as ArrayBuffer) : response.data; + + const result: AptosResponse = { + status: response.status, + statusText: response.statusText, + data: data as Res, + url: fullUrl, + headers: toHeaders(response.headers), + }; + + const throwError = (): never => { + // biome-ignore lint/suspicious/noExplicitAny: bridging client and error-layer request/response types + throw new AptosApiError({ apiType, aptosRequest: options as any, aptosResponse: result as any }); + }; + + // Error handling + if (result.status === 401) { + throwError(); + } + + if (apiType === AptosApiType.INDEXER) { + const indexerData = data as Record; + if (indexerData.errors) { + throwError(); + } + if (indexerData.data !== undefined) { + result.data = indexerData.data as Res; + } + } + + if (apiType === AptosApiType.PEPPER || apiType === AptosApiType.PROVER) { + if (result.status >= 400) { + throwError(); + } + } else if (result.status < 200 || result.status >= 300) { + throwError(); + } + + return result; +} diff --git a/v10/src/client/get.ts b/v10/src/client/get.ts new file mode 100644 index 000000000..55492f77b --- /dev/null +++ b/v10/src/client/get.ts @@ -0,0 +1,149 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { AnyNumber } from "../bcs/types.js"; +import { aptosRequest } from "./aptos-request.js"; +import type { AptosApiType, AptosResponse, Client, ClientConfig, MimeType } from "./types.js"; + +/** + * Options for a GET request to an Aptos API endpoint. + * Passed to {@link get}, {@link paginateWithCursor}, and {@link paginateWithObfuscatedCursor}. + */ +export interface GetRequestOptions { + /** The base URL of the API endpoint. */ + url: string; + /** The Aptos API service type (used for error context and routing). */ + apiType: AptosApiType; + /** The path segment to append to the base URL. */ + path: string; + /** The SDK method name that originated this request (for debugging headers). */ + originMethod: string; + /** Optional query string parameters. Undefined values are omitted. */ + params?: Record; + /** MIME type for the request `content-type` header. Defaults to JSON. */ + contentType?: MimeType; + /** Expected MIME type for the response (`accept` header). Defaults to JSON. */ + acceptType?: MimeType; + /** Per-request client configuration overrides (auth, headers, etc.). */ + overrides?: ClientConfig; + /** Custom HTTP client to use instead of the default transport. */ + client?: Client; +} + +/** + * Performs a GET request to an Aptos API endpoint. + * + * @typeParam Res - The expected type of the parsed response body. + * @param options - The request options including URL, path, params, and auth overrides. + * @returns A promise that resolves to an {@link AptosResponse} with the parsed response body. + * @throws {AptosApiError} If the response indicates an error. + * + * @example + * ```typescript + * const response = await get({ + * url: "https://fullnode.mainnet.aptoslabs.com/v1", + * apiType: AptosApiType.FULLNODE, + * path: "accounts/0x1", + * originMethod: "getAccountInfo", + * }); + * ``` + */ +export async function get(options: GetRequestOptions): Promise> { + const { url, apiType, path, originMethod, params, contentType, acceptType, overrides, client } = options; + return aptosRequest( + { url, method: "GET", path, originMethod, params, contentType, acceptType, overrides }, + apiType, + client, + ); +} + +/** + * Performs a paginated GET request using the `x-aptos-cursor` response header. + * Fetches all pages automatically by following cursors until no more pages remain, + * then returns the concatenated results. + * + * Use this for endpoints that paginate via an opaque cursor returned in the + * `x-aptos-cursor` header (e.g. full node REST API list endpoints). + * + * @typeParam Res - An array type extending `Array>`. + * @param options - The request options. `params.limit` controls the page size; it is + * passed to the server on every request. `params.start` is managed internally. + * @returns A promise that resolves to the full concatenated result set. + * @throws {AptosApiError} If any individual page request fails. + * + * @example + * ```typescript + * const allTxns = await paginateWithCursor({ + * url: "https://fullnode.mainnet.aptoslabs.com/v1", + * apiType: AptosApiType.FULLNODE, + * path: "transactions", + * originMethod: "getTransactions", + * params: { limit: 25 }, + * }); + * ``` + */ +export async function paginateWithCursor>>( + options: GetRequestOptions, +): Promise { + const MAX_PAGES = 1000; + let out: Res = [] as unknown as Res; + let cursor: string | undefined; + let pages = 0; + const requestParams = (options.params ?? {}) as Record; + do { + if (++pages > MAX_PAGES) { + throw new Error(`Pagination exceeded ${MAX_PAGES} pages — aborting to prevent infinite loop`); + } + const response = await get({ ...options, params: requestParams }); + cursor = response.headers.get("x-aptos-cursor") ?? undefined; + out = out.concat(response.data) as Res; + (requestParams as Record).start = cursor; + } while (cursor !== undefined); + return out; +} + +/** + * Performs a paginated GET request using an obfuscated (opaque) cursor. + * Similar to {@link paginateWithCursor} but tracks the cursor in `params.cursor` + * instead of `params.start`, and respects a total `limit` cap across all pages. + * + * Use this for endpoints where the caller controls a soft total limit and the + * server returns pages smaller than the requested total. + * + * @typeParam Res - An array type extending `Array>`. + * @param options - The request options. `params.limit` sets the maximum total items + * to return (not just per-page). `params.cursor` is managed internally. + * @returns A promise that resolves to the full concatenated result set, up to the + * total limit if one was specified. + * @throws {AptosApiError} If any individual page request fails. + */ +export async function paginateWithObfuscatedCursor>>( + options: GetRequestOptions, +): Promise { + let out: Res = [] as unknown as Res; + let cursor: string | undefined; + const requestParams = (options.params ?? {}) as Record; + const totalLimit = requestParams.limit as number | undefined; + do { + // Only pass start + limit to the server + const serverParams: Record = {}; + if (typeof requestParams.cursor === "string") { + serverParams.start = requestParams.cursor; + } + if (requestParams.limit !== undefined) { + serverParams.limit = requestParams.limit; + } + + const response = await get({ ...options, params: serverParams }); + cursor = response.headers.get("x-aptos-cursor") ?? undefined; + out = out.concat(response.data) as Res; + (requestParams as Record).cursor = cursor; + + if (totalLimit !== undefined) { + const remaining = totalLimit - out.length; + if (remaining <= 0) break; + requestParams.limit = remaining; + } + } while (cursor !== undefined); + return out; +} diff --git a/v10/src/client/index.ts b/v10/src/client/index.ts new file mode 100644 index 000000000..0bd92f491 --- /dev/null +++ b/v10/src/client/index.ts @@ -0,0 +1,42 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * @module client + * + * HTTP client layer for the Aptos SDK. + * + * Provides low-level primitives for making GET and POST requests to Aptos API + * endpoints (full node, indexer, faucet, pepper, prover), including automatic + * pagination helpers and request/response type definitions. + * + * Key exports: + * - {@link aptosRequest} — core fetch wrapper with error handling + * - {@link get} / {@link post} — typed GET/POST helpers + * - {@link paginateWithCursor} / {@link paginateWithObfuscatedCursor} — auto-pagination + * - {@link AptosRequest} / {@link AptosResponse} — request and response types + * - {@link ClientConfig} / {@link FullNodeConfig} / {@link IndexerConfig} / {@link FaucetConfig} — auth/header config + * - {@link MimeType} — MIME type constants for BCS and JSON encoding + * - {@link AptosApiType} — enum of Aptos API service types + */ +export { aptosRequest } from "./aptos-request.js"; +export type { GetRequestOptions } from "./get.js"; +export { get, paginateWithCursor, paginateWithObfuscatedCursor } from "./get.js"; +export type { PostRequestOptions } from "./post.js"; +export { post } from "./post.js"; +export type { + AptosRequest, + AptosResponse, + Client, + ClientConfig, + ClientRequest, + ClientResponse, + FaucetConfig, + FullNodeConfig, + IndexerConfig, + PaginationArgs, +} from "./types.js"; +export { + AptosApiType, + MimeType, +} from "./types.js"; diff --git a/v10/src/client/post.ts b/v10/src/client/post.ts new file mode 100644 index 000000000..aeb57fee3 --- /dev/null +++ b/v10/src/client/post.ts @@ -0,0 +1,62 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { AnyNumber } from "../bcs/types.js"; +import { aptosRequest } from "./aptos-request.js"; +import type { AptosApiType, AptosResponse, Client, ClientConfig, MimeType } from "./types.js"; + +/** + * Options for a POST request to an Aptos API endpoint. + * Passed to the {@link post} helper function. + */ +export interface PostRequestOptions { + /** The base URL of the API endpoint. */ + url: string; + /** The Aptos API service type (used for error context and routing). */ + apiType: AptosApiType; + /** The path segment to append to the base URL. */ + path: string; + /** The SDK method name that originated this request (for debugging headers). */ + originMethod: string; + /** The request body. Serialized as JSON unless a binary `contentType` is provided. */ + body?: unknown; + /** Optional query string parameters. Undefined values are omitted. */ + params?: Record; + /** MIME type for the request `content-type` header. Defaults to JSON. */ + contentType?: MimeType; + /** Expected MIME type for the response (`accept` header). Defaults to JSON. */ + acceptType?: MimeType; + /** Per-request client configuration overrides (auth, headers, etc.). */ + overrides?: ClientConfig; + /** Custom HTTP client to use instead of the default transport. */ + client?: Client; +} + +/** + * Performs a POST request to an Aptos API endpoint. + * + * @typeParam Res - The expected type of the parsed response body. + * @param options - The request options including URL, path, body, and auth overrides. + * @returns A promise that resolves to an {@link AptosResponse} with the parsed response body. + * @throws {AptosApiError} If the response indicates an error. + * + * @example + * ```typescript + * const response = await post({ + * url: "https://fullnode.mainnet.aptoslabs.com/v1", + * apiType: AptosApiType.FULLNODE, + * path: "transactions", + * originMethod: "submitTransaction", + * body: signedTxnBytes, + * contentType: MimeType.BCS_SIGNED_TRANSACTION, + * }); + * ``` + */ +export async function post(options: PostRequestOptions): Promise> { + const { url, apiType, path, originMethod, body, params, contentType, acceptType, overrides, client } = options; + return aptosRequest( + { url, method: "POST", path, originMethod, body, params, contentType, acceptType, overrides }, + apiType, + client, + ); +} diff --git a/v10/src/client/types.ts b/v10/src/client/types.ts new file mode 100644 index 000000000..af14f5378 --- /dev/null +++ b/v10/src/client/types.ts @@ -0,0 +1,185 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { AnyNumber } from "../bcs/types.js"; + +// Re-export AptosApiType from core so client consumers don't need two imports +export { AptosApiType } from "../core/constants.js"; + +// ── MIME types ── + +/** + * MIME type constants used in Aptos HTTP request and response headers. + * Passed as `contentType` and `acceptType` in request options to control + * the encoding format of request bodies and expected response bodies. + */ +export enum MimeType { + /** Standard JSON content type. Used for most REST API calls. */ + JSON = "application/json", + /** BCS-encoded binary content type. Used for raw binary payloads. */ + BCS = "application/x-bcs", + /** BCS-encoded signed transaction. Used when submitting signed transactions. */ + BCS_SIGNED_TRANSACTION = "application/x.aptos.signed_transaction+bcs", + /** BCS-encoded view function payload. Used for BCS view function calls. */ + BCS_VIEW_FUNCTION = "application/x.aptos.view_function+bcs", +} + +// ── Client config ── + +/** + * Configuration options that can be applied to any Aptos HTTP client request. + * Can be passed at the `AptosConfig` level (as defaults) or overridden per-request. + */ +export interface ClientConfig { + /** Additional HTTP headers to include with every request. */ + HEADERS?: Record; + /** + * API key used for authentication. Sent as a `Bearer` token in the + * `Authorization` header. Ignored if {@link AUTH_TOKEN} is also set. + */ + API_KEY?: string; + /** + * Authentication bearer token. Sent in the `Authorization` header. + * Takes precedence over {@link API_KEY} if both are set. + */ + AUTH_TOKEN?: string; + /** + * Whether to use HTTP/2. Defaults to `true` in Node.js (via `got`), + * ignored in browsers (which use native `fetch`). + */ + http2?: boolean; +} + +/** Client configuration specific to full node REST API requests. */ +export interface FullNodeConfig extends ClientConfig {} +/** Client configuration specific to indexer GraphQL API requests. */ +export interface IndexerConfig extends ClientConfig {} +/** Client configuration specific to faucet API requests. */ +export interface FaucetConfig extends ClientConfig {} + +// ── Custom HTTP client ── + +/** + * Shape of a request passed to a custom {@link Client} implementation. + * Contains all the information needed to make an HTTP request. + */ +export interface ClientRequest { + /** The full URL to send the request to (including any path segments). */ + url: string; + /** The HTTP method. */ + method: "GET" | "POST"; + /** The request body (only present for POST requests). */ + body?: unknown; + /** Query string parameters. Values are URL-encoded by the HTTP library. */ + params?: Record; + /** Merged HTTP headers including `content-type`, `accept`, auth, and SDK version. */ + headers: Record; + /** Whether to use HTTP/2 (Node.js only). */ + http2?: boolean; +} + +/** + * Shape of a response returned by a custom {@link Client} implementation. + * @typeParam T - The parsed response body type (`object` for JSON, `Uint8Array`/`ArrayBuffer` for BCS). + */ +export interface ClientResponse { + /** The HTTP status code. */ + status: number; + /** The HTTP status text. */ + statusText: string; + /** The parsed response body. */ + data: T; + /** The response headers. */ + headers?: Record | Headers; +} + +/** + * Interface for a custom HTTP client that can replace the default `@aptos-labs/aptos-client` transport. + * Implement this to add custom auth, proxies, logging, or use an alternative HTTP library. + * + * @example + * ```typescript + * const myClient: Client = { + * async sendRequest(request: ClientRequest): Promise> { + * const response = await fetch(request.url, { + * method: request.method, + * headers: request.headers, + * body: request.body ? JSON.stringify(request.body) : undefined, + * }); + * return { + * status: response.status, + * statusText: response.statusText, + * data: await response.json() as Res, + * headers: response.headers, + * }; + * }, + * }; + * const config = new AptosConfig({ network: Network.DEVNET, client: myClient }); + * ``` + */ +export interface Client { + /** Sends an HTTP request and returns the parsed response. */ + sendRequest(request: ClientRequest): Promise>; +} + +// ── Request / Response ── + +/** + * Parameters for a single HTTP request to an Aptos API endpoint. + * Constructed internally by {@link get} and {@link post}; rarely used directly. + */ +export interface AptosRequest { + /** The base URL of the API endpoint (e.g. `"https://fullnode.mainnet.aptoslabs.com/v1"`). */ + url: string; + /** The HTTP method to use. */ + method: "GET" | "POST"; + /** Optional path segment appended to `url` with a `/` separator. */ + path?: string; + /** Optional request body. Serialized to JSON unless a binary `contentType` is specified. */ + body?: unknown; + /** MIME type for the request body. Defaults to {@link MimeType.JSON}. */ + contentType?: string; + /** Expected MIME type for the response body. Defaults to {@link MimeType.JSON}. */ + acceptType?: string; + /** Query string parameters. Values are URL-encoded and appended to the URL. */ + params?: Record; + /** The SDK method name that originated this request (for debugging via `x-aptos-typescript-sdk-origin-method` header). */ + originMethod?: string; + /** Per-request overrides for {@link ClientConfig} options (headers, auth tokens, etc.). */ + overrides?: ClientConfig; +} + +/** + * The parsed response returned from an Aptos API request. + * @typeParam Res - The expected type of the `data` field. + */ +export interface AptosResponse { + /** The HTTP status code (e.g. 200, 400, 404). */ + status: number; + /** The HTTP status text (e.g. `"OK"`, `"Not Found"`). */ + statusText: string; + /** The deserialized response body. JSON responses are parsed; BCS responses are `Uint8Array`. */ + data: Res; + /** The full URL that was requested (including query string). */ + url: string; + /** The response headers returned by the server. */ + headers: Headers; +} + +// ── Pagination ── + +/** + * Standard pagination parameters accepted by list/query endpoints. + * Pass these in the `options` object of list API calls to page through results. + * + * @example + * ```typescript + * const txns = await aptos.getTransactions({ offset: 100, limit: 25 }); + * ``` + */ +export interface PaginationArgs { + /** The zero-based index of the first item to return. */ + offset?: number; + /** The maximum number of items to return. */ + limit?: number; +} diff --git a/v10/src/compat/aptos.ts b/v10/src/compat/aptos.ts new file mode 100644 index 000000000..1233886e7 --- /dev/null +++ b/v10/src/compat/aptos.ts @@ -0,0 +1,359 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +// v6-compatible Aptos class that provides flat mixin-style methods +// delegating to v10's namespaced API functions. + +import * as accountFns from "../api/account.js"; +import * as coinFns from "../api/coin.js"; +import type { AptosConfig, AptosSettings } from "../api/config.js"; +import * as faucetFns from "../api/faucet.js"; +import * as generalFns from "../api/general.js"; +import { Aptos as V10Aptos } from "../api/index.js"; +import * as tableFns from "../api/table.js"; +import * as transactionFns from "../api/transaction.js"; +import type { + AccountData, + Block, + CommittedTransactionResponse, + GasEstimation, + LedgerInfo, + MoveModuleBytecode, + MoveResource, + PendingTransactionResponse, + TransactionResponse, + UserTransactionResponse, +} from "../api/types.js"; +import type { AccountAuthenticator } from "../transactions/authenticator.js"; +import type { SimpleTransaction } from "../transactions/simple-transaction.js"; + +import type { + BuildSimpleArgs, + FundAccountArgs, + GetAccountInfoArgs, + GetAccountModuleArgs, + GetAccountModulesArgs, + GetAccountResourceArgs, + GetAccountResourcesArgs, + GetAccountTransactionsArgs, + GetBlockByHeightArgs, + GetBlockByVersionArgs, + GetSigningMessageArgs, + GetTableItemArgs, + GetTransactionByHashArgs, + GetTransactionByVersionArgs, + GetTransactionsArgs, + SignAndSubmitArgs, + SignTransactionArgs, + TransferCoinArgs, + ViewArgs, + WaitForTransactionArgs, +} from "./types.js"; + +// ── v6-compatible Build sub-object ── + +class CompatBuild { + constructor(private config: AptosConfig) {} + + /** + * Builds a simple entry function transaction using v6-style argument shape. + * Delegates to {@link transactionFns.buildSimpleTransaction}. + * + * @param args - The v6-style build arguments containing `sender`, `data`, and optional `options`. + * @returns A promise that resolves to a {@link SimpleTransaction}. + */ + async simple(args: BuildSimpleArgs): Promise { + return transactionFns.buildSimpleTransaction( + this.config, + args.sender, + { + function: args.data.function, + typeArguments: args.data.typeArguments, + functionArguments: args.data.functionArguments, + }, + args.options + ? { + maxGasAmount: args.options.maxGasAmount, + gasUnitPrice: args.options.gasUnitPrice, + expireTimestamp: args.options.expireTimestamp, + sequenceNumber: args.options.accountSequenceNumber, + } + : undefined, + ); + } +} + +// ── v6-compatible Aptos class ── + +/** + * A v6-compatible `Aptos` class that exposes flat (non-namespaced) methods + * for gradual migration from the v6 SDK to the v10 namespaced API. + * + * Extends the v10 {@link V10Aptos} class and adds: + * - Flat methods equivalent to `aptos.account.*`, `aptos.transaction.*`, etc. + * - A v6-style `transaction.build.simple(args)` sub-object. + * + * Import from `@aptos-labs/ts-sdk/compat` to use: + * + * @example + * ```typescript + * import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk/compat"; + * + * const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET })); + * + * // v6-style flat call + * const info = await aptos.getAccountInfo({ accountAddress: "0x1" }); + * + * // v6-style transaction build + * const txn = await aptos.transaction.build.simple({ + * sender: "0x1", + * data: { function: "0x1::coin::transfer", functionArguments: [...] }, + * }); + * ``` + * + * @deprecated Use `Aptos` from `@aptos-labs/ts-sdk` (v10 native API) instead. + * This compat class is provided for gradual migration only. + */ +export class Aptos extends V10Aptos { + // Override transaction to provide v6-compatible build sub-object + declare readonly transaction: V10Aptos["transaction"] & { + build: CompatBuild; + }; + + /** + * @param config - An `AptosConfig` instance or `AptosSettings` object. + * If omitted, defaults to `Network.DEVNET`. + */ + constructor(config?: AptosConfig | AptosSettings) { + super(config); + + // Attach the compat Build sub-object to the existing transaction namespace + const compatBuild = new CompatBuild(this.config); + Object.defineProperty(this.transaction, "build", { + value: compatBuild, + writable: false, + enumerable: true, + }); + } + + // ── General API (flat) ── + + /** + * Fetches the current ledger information (chain ID, block height, epoch, etc.). + * @returns A promise resolving to the current {@link LedgerInfo}. + */ + getLedgerInfo(): Promise { + return generalFns.getLedgerInfo(this.config); + } + + /** + * Fetches the chain ID for the configured network. + * @returns A promise resolving to the numeric chain ID. + */ + getChainId(): Promise { + return generalFns.getChainId(this.config); + } + + /** + * Fetches a block by its ledger version. + * @param args - Contains `ledgerVersion` and optional `options.withTransactions`. + * @returns A promise resolving to the {@link Block} at the given ledger version. + */ + getBlockByVersion(args: GetBlockByVersionArgs): Promise { + return generalFns.getBlockByVersion(this.config, args.ledgerVersion, args.options); + } + + /** + * Fetches a block by its block height. + * @param args - Contains `blockHeight` and optional `options.withTransactions`. + * @returns A promise resolving to the {@link Block} at the given height. + */ + getBlockByHeight(args: GetBlockByHeightArgs): Promise { + return generalFns.getBlockByHeight(this.config, args.blockHeight, args.options); + } + + /** + * Executes a Move view function and returns the result. + * @typeParam T - The expected return type of the view function. + * @param args - Contains the view function `payload` and optional `options.ledgerVersion`. + * @returns A promise resolving to the view function's return values. + */ + view(args: ViewArgs): Promise { + return generalFns.view(this.config, args.payload, args.options); + } + + /** + * Fetches the current gas price estimate from the full node. + * @returns A promise resolving to a {@link GasEstimation} object. + */ + getGasPriceEstimation(): Promise { + return generalFns.getGasPriceEstimation(this.config); + } + + // ── Account API (flat) ── + + /** + * Fetches account information (sequence number, authentication key) for a given address. + * @param args - Contains `accountAddress`. + * @returns A promise resolving to {@link AccountData}. + */ + getAccountInfo(args: GetAccountInfoArgs): Promise { + return accountFns.getAccountInfo(this.config, args.accountAddress); + } + + /** + * Fetches all published Move modules for an account. + * @param args - Contains `accountAddress` and optional `options` (limit, ledgerVersion). + * @returns A promise resolving to an array of {@link MoveModuleBytecode}. + */ + getAccountModules(args: GetAccountModulesArgs): Promise { + return accountFns.getAccountModules(this.config, args.accountAddress, args.options); + } + + /** + * Fetches a specific published Move module for an account. + * @param args - Contains `accountAddress`, `moduleName`, and optional `options.ledgerVersion`. + * @returns A promise resolving to a {@link MoveModuleBytecode}. + */ + getAccountModule(args: GetAccountModuleArgs): Promise { + return accountFns.getAccountModule(this.config, args.accountAddress, args.moduleName, args.options); + } + + /** + * Fetches a specific Move resource from an account, typed as `T`. + * @typeParam T - The expected resource data type. Defaults to `unknown`. + * @param args - Contains `accountAddress`, `resourceType`, and optional `options.ledgerVersion`. + * @returns A promise resolving to the resource data typed as `T`. + */ + getAccountResource(args: GetAccountResourceArgs): Promise { + return accountFns.getAccountResource(this.config, args.accountAddress, args.resourceType, args.options); + } + + /** + * Fetches all Move resources stored on an account. + * @param args - Contains `accountAddress` and optional `options` (limit, ledgerVersion). + * @returns A promise resolving to an array of {@link MoveResource}. + */ + getAccountResources(args: GetAccountResourcesArgs): Promise { + return accountFns.getAccountResources(this.config, args.accountAddress, args.options); + } + + /** + * Fetches the list of committed transactions sent by an account. + * @param args - Contains `accountAddress` and optional `options` (offset, limit). + * @returns A promise resolving to an array of {@link CommittedTransactionResponse}. + */ + getAccountTransactions(args: GetAccountTransactionsArgs): Promise { + return accountFns.getAccountTransactions(this.config, args.accountAddress, args.options); + } + + // ── Transaction API (flat) ── + + /** + * Signs a raw transaction with the given signer's private key. + * @param args - Contains `signer` (Account) and `transaction` (raw transaction). + * @returns An {@link AccountAuthenticator} containing the signature. + */ + signTransaction(args: SignTransactionArgs): AccountAuthenticator { + return transactionFns.signTransaction(args.signer, args.transaction); + } + + /** + * Signs a transaction and submits it to the network in a single step. + * @param args - Contains `signer` and `transaction`. + * @returns A promise resolving to a {@link PendingTransactionResponse} with the transaction hash. + */ + signAndSubmitTransaction(args: SignAndSubmitArgs): Promise { + return transactionFns.signAndSubmitTransaction(this.config, args.signer, args.transaction); + } + + /** + * Waits for a transaction to be committed on-chain. + * @param args - Contains `transactionHash` and optional `options` (timeoutSecs, checkSuccess). + * @returns A promise resolving to the {@link CommittedTransactionResponse} when committed. + * @throws If the transaction is not committed within the timeout, or if `checkSuccess` is + * `true` and the transaction failed on-chain. + */ + waitForTransaction(args: WaitForTransactionArgs): Promise { + return transactionFns.waitForTransaction(this.config, args.transactionHash, args.options); + } + + /** + * Fetches recent transactions from the ledger. + * @param args - Optional arguments containing `options` (offset, limit). + * @returns A promise resolving to an array of {@link TransactionResponse}. + */ + getTransactions(args?: GetTransactionsArgs): Promise { + return transactionFns.getTransactions(this.config, args?.options); + } + + /** + * Fetches a transaction by its hash. + * @param args - Contains `transactionHash`. + * @returns A promise resolving to a {@link TransactionResponse}. + */ + getTransactionByHash(args: GetTransactionByHashArgs): Promise { + return transactionFns.getTransactionByHash(this.config, args.transactionHash); + } + + /** + * Fetches a transaction by its ledger version. + * @param args - Contains `ledgerVersion`. + * @returns A promise resolving to a {@link TransactionResponse}. + */ + getTransactionByVersion(args: GetTransactionByVersionArgs): Promise { + return transactionFns.getTransactionByVersion(this.config, args.ledgerVersion); + } + + /** + * Returns the bytes that a signer must sign for a given raw transaction. + * @param args - Contains `transaction` (the raw transaction to get the signing message for). + * @returns A `Uint8Array` of the signing message bytes. + */ + getSigningMessage(args: GetSigningMessageArgs): Uint8Array { + return transactionFns.getSigningMessage(args.transaction); + } + + // ── Faucet API (flat) ── + + /** + * Funds a test account with APT from the configured faucet. + * Only available on devnet and testnet. + * @param args - Contains `accountAddress`, `amount` (in Octas), and optional `options`. + * @returns A promise resolving to the {@link UserTransactionResponse} of the funding transaction. + */ + fundAccount(args: FundAccountArgs): Promise { + return faucetFns.fundAccount(this.config, args.accountAddress, args.amount, args.options); + } + + // ── Coin API (flat) ── + + /** + * Builds a simple coin transfer transaction. + * @param args - Contains `sender`, `recipient`, `amount`, and optional `options`. + * @returns A promise resolving to a {@link SimpleTransaction} ready to be signed and submitted. + */ + transferCoinTransaction(args: TransferCoinArgs): Promise { + const options = args.options + ? { + maxGasAmount: args.options.maxGasAmount, + gasUnitPrice: args.options.gasUnitPrice, + expireTimestamp: args.options.expireTimestamp, + sequenceNumber: args.options.accountSequenceNumber, + } + : undefined; + return coinFns.transferCoinTransaction(this.config, args.sender, args.recipient, args.amount, options); + } + + // ── Table API (flat) ── + + /** + * Fetches an item from a Move table by its handle and key. + * @typeParam T - The expected value type. Defaults to `unknown`. + * @param args - Contains `handle` (table handle), `data` (key/value type info), and optional `options`. + * @returns A promise resolving to the table item value typed as `T`. + */ + getTableItem(args: GetTableItemArgs): Promise { + return tableFns.getTableItem(this.config, args.handle, args.data, args.options); + } +} diff --git a/v10/src/compat/index.cjs b/v10/src/compat/index.cjs new file mode 100644 index 000000000..f23e5a5a5 --- /dev/null +++ b/v10/src/compat/index.cjs @@ -0,0 +1,17 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +// CJS compatibility wrapper for @aptos-labs/ts-sdk/compat +// +// Node 22.12+ can require() ESM modules directly (no flag needed). +// Older Node 22.x users should use: const sdk = await import("@aptos-labs/ts-sdk/compat") + +"use strict"; + +try { + module.exports = require("./index.js"); +} catch { + // Node < 22.12: require(esm) not yet supported — export the import() promise. + // Consumer must await: const { Aptos } = await require("@aptos-labs/ts-sdk/compat") + module.exports = import("./index.js"); +} diff --git a/v10/src/compat/index.ts b/v10/src/compat/index.ts new file mode 100644 index 000000000..2d2677fe5 --- /dev/null +++ b/v10/src/compat/index.ts @@ -0,0 +1,86 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * @module compat + * + * v6-compatibility layer for the Aptos TypeScript SDK. + * + * Import from `@aptos-labs/ts-sdk/compat` to use the v6-style flat API on top + * of the v10 SDK. This module re-exports all v10 primitives alongside a + * compatibility {@link Aptos} class that exposes the flat method style from v6. + * + * @example + * ```typescript + * import { Aptos, AptosConfig, Network, Account } from "@aptos-labs/ts-sdk/compat"; + * + * const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET })); + * + * // v6-style flat calls + * const info = await aptos.getAccountInfo({ accountAddress: "0x1" }); + * const txn = await aptos.transaction.build.simple({ + * sender: myAccount.accountAddress, + * data: { + * function: "0x1::coin::transfer", + * functionArguments: [recipient, new U64(amount)], + * }, + * }); + * ``` + * + * @deprecated Migrate to the v10 namespaced `Aptos` class from `@aptos-labs/ts-sdk`. + */ + +// ── Compat Aptos class (v6-style flat methods) ── +export { Aptos } from "./aptos.js"; + +// ── v6 parameter types ── +export type * from "./types.js"; + +// ── Re-export everything from v10 ── + +export * from "../account/index.js"; +export { + getAccountInfo, + getAccountModule, + getAccountModules, + getAccountResource, + getAccountResources, + getAccountTransactions, +} from "../api/account.js"; +export { transferCoinTransaction } from "../api/coin.js"; +// Config +export { AptosConfig, type AptosSettings, createConfig } from "../api/config.js"; +export { fundAccount } from "../api/faucet.js"; +// Standalone API functions (for users who used them directly) +export { + getBlockByHeight, + getBlockByVersion, + getChainId, + getGasPriceEstimation, + getLedgerInfo, + view, +} from "../api/general.js"; +export { getTableItem } from "../api/table.js"; +export type { BuildSimpleTransactionOptions } from "../api/transaction.js"; +export { + buildSimpleTransaction, + getSigningMessage, + getTransactionByHash, + getTransactionByVersion, + getTransactions, + signAndSubmitTransaction, + signTransaction, + submitTransaction, + waitForTransaction, +} from "../api/transaction.js"; + +// API types +export * from "../api/types.js"; +// Layers 0-5: All primitives, crypto, core, transactions, account, client +export * from "../bcs/index.js"; +export * from "../client/index.js"; +export * from "../core/index.js"; +export * from "../crypto/index.js"; +export * from "../hex/index.js"; +export * from "../transactions/index.js"; +export { VERSION } from "../version.js"; diff --git a/v10/src/compat/types.ts b/v10/src/compat/types.ts new file mode 100644 index 000000000..7b486223f --- /dev/null +++ b/v10/src/compat/types.ts @@ -0,0 +1,201 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +// v6-compatible type aliases and parameter shapes +// These map v6's single-object-arg calling convention to v10 types. + +import type { Account } from "../account/types.js"; +import type { TableItemRequest, ViewFunctionPayload } from "../api/types.js"; +import type { AnyNumber, EntryFunctionArgument } from "../bcs/types.js"; +import type { AccountAddressInput } from "../core/account-address.js"; +import type { TypeTag } from "../core/type-tag.js"; +import type { HexInput } from "../hex/index.js"; +import type { AnyRawTransaction, MoveStructId } from "../transactions/types.js"; + +// ── v6 parameter types ── + +/** + * Configuration settings for the v6-compatible {@link Aptos} constructor. + * Mirrors the shape accepted by `AptosConfig` but uses optional fields + * that were common in v6. + */ +export type AptosConfigSettings = { + /** The Aptos network to connect to (e.g. `Network.MAINNET`). */ + network?: import("../core/network.js").Network; + /** Custom full node URL. Overrides the default URL for the selected network. */ + fullnode?: string; + /** Custom faucet URL. Overrides the default URL for the selected network. */ + faucet?: string; + /** Custom pepper service URL for Keyless accounts. */ + pepper?: string; + /** Custom ZK prover URL for Keyless accounts. */ + prover?: string; + /** Custom indexer GraphQL URL. Overrides the default URL for the selected network. */ + indexer?: string; + /** Default client configuration applied to all requests. */ + clientConfig?: import("../client/types.js").ClientConfig; + /** Default configuration applied to full node requests only. */ + fullnodeConfig?: import("../client/types.js").FullNodeConfig; + /** Default configuration applied to indexer requests only. */ + indexerConfig?: import("../client/types.js").IndexerConfig; + /** Default configuration applied to faucet requests only. */ + faucetConfig?: import("../client/types.js").FaucetConfig; +}; + +// v6 Account API args + +/** Arguments for {@link Aptos.getAccountInfo}. */ +export type GetAccountInfoArgs = { accountAddress: AccountAddressInput }; + +/** Arguments for {@link Aptos.getAccountModules}. */ +export type GetAccountModulesArgs = { + accountAddress: AccountAddressInput; + options?: { limit?: number; ledgerVersion?: AnyNumber }; +}; + +/** Arguments for {@link Aptos.getAccountModule}. */ +export type GetAccountModuleArgs = { + accountAddress: AccountAddressInput; + moduleName: string; + options?: { ledgerVersion?: AnyNumber }; +}; + +/** Arguments for {@link Aptos.getAccountResource}. */ +export type GetAccountResourceArgs = { + accountAddress: AccountAddressInput; + resourceType: MoveStructId; + options?: { ledgerVersion?: AnyNumber }; +}; + +/** Arguments for {@link Aptos.getAccountResources}. */ +export type GetAccountResourcesArgs = { + accountAddress: AccountAddressInput; + options?: { limit?: number; ledgerVersion?: AnyNumber }; +}; + +/** Arguments for {@link Aptos.getAccountTransactions}. */ +export type GetAccountTransactionsArgs = { + accountAddress: AccountAddressInput; + options?: { offset?: AnyNumber; limit?: number }; +}; + +// v6 General API args + +/** Arguments for {@link Aptos.getBlockByVersion}. */ +export type GetBlockByVersionArgs = { + ledgerVersion: AnyNumber; + options?: { withTransactions?: boolean }; +}; + +/** Arguments for {@link Aptos.getBlockByHeight}. */ +export type GetBlockByHeightArgs = { + blockHeight: AnyNumber; + options?: { withTransactions?: boolean }; +}; + +/** Arguments for {@link Aptos.view}. */ +export type ViewArgs = { + payload: ViewFunctionPayload; + options?: { ledgerVersion?: AnyNumber }; +}; + +// v6 Transaction args + +/** + * Transaction generation options in v6 style. + * Maps to `BuildSimpleTransactionOptions` in v10, except `accountSequenceNumber` + * replaces `sequenceNumber`. + */ +export type InputGenerateTransactionOptions = { + maxGasAmount?: AnyNumber; + gasUnitPrice?: AnyNumber; + expireTimestamp?: AnyNumber; + accountSequenceNumber?: AnyNumber; +}; + +/** + * The Move function payload for building a simple transaction, in v6 style. + */ +export type InputGenerateTransactionPayloadData = { + /** Fully qualified Move function identifier, e.g. `"0x1::coin::transfer"`. */ + function: MoveStructId; + /** Optional Move type arguments for generic functions. */ + typeArguments?: TypeTag[]; + /** Function arguments as BCS-serializable values. */ + functionArguments?: EntryFunctionArgument[]; +}; + +/** Arguments for {@link CompatBuild.simple} (building a simple transaction in v6 style). */ +export type BuildSimpleArgs = { + sender: AccountAddressInput; + data: InputGenerateTransactionPayloadData; + options?: InputGenerateTransactionOptions; +}; + +/** Arguments for {@link Aptos.signTransaction}. */ +export type SignTransactionArgs = { + signer: Account; + transaction: AnyRawTransaction; +}; + +/** Arguments for {@link Aptos.signAndSubmitTransaction}. */ +export type SignAndSubmitArgs = { + signer: Account; + transaction: AnyRawTransaction; +}; + +/** Arguments for {@link Aptos.waitForTransaction}. */ +export type WaitForTransactionArgs = { + transactionHash: HexInput; + options?: { timeoutSecs?: number; checkSuccess?: boolean }; +}; + +/** Arguments for {@link Aptos.getTransactions}. */ +export type GetTransactionsArgs = { + options?: { offset?: AnyNumber; limit?: number }; +}; + +/** Arguments for {@link Aptos.getTransactionByHash}. */ +export type GetTransactionByHashArgs = { + transactionHash: HexInput; +}; + +/** Arguments for {@link Aptos.getTransactionByVersion}. */ +export type GetTransactionByVersionArgs = { + ledgerVersion: AnyNumber; +}; + +/** Arguments for {@link Aptos.getSigningMessage}. */ +export type GetSigningMessageArgs = { + transaction: AnyRawTransaction; +}; + +// v6 Faucet args + +/** Arguments for {@link Aptos.fundAccount}. */ +export type FundAccountArgs = { + accountAddress: AccountAddressInput; + amount: number; + options?: { timeoutSecs?: number; checkSuccess?: boolean }; +}; + +// v6 Coin args + +/** Arguments for {@link Aptos.transferCoinTransaction}. */ +export type TransferCoinArgs = { + sender: AccountAddressInput; + recipient: AccountAddressInput; + amount: AnyNumber; + /** @deprecated The `coinType` parameter is not supported in v10. Passing it has no effect. */ + coinType?: MoveStructId; + options?: InputGenerateTransactionOptions; +}; + +// v6 Table args + +/** Arguments for {@link Aptos.getTableItem}. */ +export type GetTableItemArgs = { + handle: string; + data: TableItemRequest; + options?: { ledgerVersion?: AnyNumber }; +}; diff --git a/v10/src/core/account-address.ts b/v10/src/core/account-address.ts new file mode 100644 index 000000000..da3fecb7e --- /dev/null +++ b/v10/src/core/account-address.ts @@ -0,0 +1,334 @@ +import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js"; +import type { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; +import type { TransactionArgument } from "../bcs/types.js"; +import { ScriptTransactionArgumentVariants } from "../bcs/types.js"; +import { ParsingError, type ParsingResult } from "../hex/errors.js"; +import type { HexInput } from "../hex/index.js"; + +/** Describes the reason an account address failed validation. */ +export enum AddressInvalidReason { + /** The byte array is not exactly 32 bytes. */ + INCORRECT_NUMBER_OF_BYTES = "incorrect_number_of_bytes", + /** The hex string contains non-hex characters. */ + INVALID_HEX_CHARS = "invalid_hex_chars", + /** The hex string is shorter than the minimum allowed length. */ + TOO_SHORT = "too_short", + /** The hex string exceeds 64 hex characters. */ + TOO_LONG = "too_long", + /** The string is missing the required "0x" prefix (strict mode). */ + LEADING_ZERO_X_REQUIRED = "leading_zero_x_required", + /** Non-special addresses must be in full 64-character form (strict mode). */ + LONG_FORM_REQUIRED_UNLESS_SPECIAL = "long_form_required_unless_special", + /** A special address has unnecessary padding zeroes (strict mode). */ + INVALID_PADDING_ZEROES = "INVALID_PADDING_ZEROES", + /** The maxMissingChars parameter is out of the valid range [0, 63]. */ + INVALID_PADDING_STRICTNESS = "INVALID_PADDING_STRICTNESS", +} + +/** + * Accepted input types for constructing or parsing an {@link AccountAddress}. + * Can be a hex string, a byte array, or an existing AccountAddress instance. + */ +export type AccountAddressInput = HexInput | AccountAddress; + +/** + * Represents a 32-byte Aptos account address. + * + * Provides parsing, validation, and serialization of on-chain addresses + * in both short (special) and long (full 64-char hex) forms. + */ +export class AccountAddress extends Serializable implements TransactionArgument { + /** The raw 32-byte address data. */ + readonly data: Uint8Array; + + /** The fixed byte length of an Aptos account address. */ + static readonly LENGTH: number = 32; + /** The number of hex characters in the long (non-prefixed) string form. */ + static readonly LONG_STRING_LENGTH: number = 64; + + /** The special address `0x0`. */ + static readonly ZERO: AccountAddress = AccountAddress.from("0x0"); + /** The special address `0x1` (Aptos framework). */ + static readonly ONE: AccountAddress = AccountAddress.from("0x1"); + /** The special address `0x2`. */ + static readonly TWO: AccountAddress = AccountAddress.from("0x2"); + /** The special address `0x3`. */ + static readonly THREE: AccountAddress = AccountAddress.from("0x3"); + /** The special address `0x4`. */ + static readonly FOUR: AccountAddress = AccountAddress.from("0x4"); + /** The special address `0xA` (fungible asset metadata). */ + static readonly A: AccountAddress = AccountAddress.from("0xA"); + + /** + * Creates an AccountAddress from a 32-byte Uint8Array. + * @param input - Exactly 32 bytes representing the address. + * @throws {ParsingError} If the input is not exactly 32 bytes. + */ + constructor(input: Uint8Array) { + super(); + if (input.length !== AccountAddress.LENGTH) { + throw new ParsingError( + "AccountAddress data should be exactly 32 bytes long", + AddressInvalidReason.INCORRECT_NUMBER_OF_BYTES, + ); + } + this.data = input.slice(); + } + + /** + * Returns whether this is a "special" address (0x0 through 0xf). + * Special addresses are displayed in short form (e.g., `0xa` instead of the full 64-char hex). + */ + isSpecial(): boolean { + if (this.data[this.data.length - 1] >= 0b10000) return false; + for (let i = 0; i < this.data.length - 1; i++) { + if (this.data[i] !== 0) return false; + } + return true; + } + + /** + * Returns the canonical string representation with "0x" prefix. + * Special addresses use short form (e.g., `"0xa"`); others use full 64-char hex. + */ + toString(): `0x${string}` { + return `0x${this.toStringWithoutPrefix()}`; + } + + /** Returns the canonical string representation without the "0x" prefix. */ + toStringWithoutPrefix(): string { + if (this.data[this.data.length - 1] < 0x10) { + let special = true; + for (let i = 0; i < this.data.length - 1; i++) { + if (this.data[i] !== 0) { + special = false; + break; + } + } + if (special) return this.data[this.data.length - 1].toString(16); + } + return bytesToHex(this.data); + } + + /** Returns the full 64-character hex representation with "0x" prefix (always zero-padded). */ + toStringLong(): `0x${string}` { + return `0x${this.toStringLongWithoutPrefix()}`; + } + + /** Returns the full 64-character hex representation without the "0x" prefix. */ + toStringLongWithoutPrefix(): string { + return bytesToHex(this.data); + } + + /** Returns the shortest hex representation with "0x" prefix (leading zeroes stripped). */ + toStringShort(): `0x${string}` { + return `0x${this.toStringShortWithoutPrefix()}`; + } + + /** Returns the shortest hex representation without the "0x" prefix (leading zeroes stripped). */ + toStringShortWithoutPrefix(): string { + const hex = bytesToHex(this.data).replace(/^0+/, ""); + return hex === "" ? "0" : hex; + } + + /** Returns the underlying 32-byte array. */ + toUint8Array(): Uint8Array { + return this.data; + } + + /** Serializes the address as fixed-length bytes via BCS. */ + serialize(serializer: Serializer): void { + serializer.serializeFixedBytes(this.data); + } + + /** Serializes this address for use as an entry function argument. */ + serializeForEntryFunction(serializer: Serializer): void { + serializer.serializeAsBytes(this); + } + + /** Serializes this address for use as a script function argument. */ + serializeForScriptFunction(serializer: Serializer): void { + serializer.serializeU32AsUleb128(ScriptTransactionArgumentVariants.Address); + serializer.serialize(this); + } + + /** + * Deserializes an AccountAddress from BCS bytes. + * @param deserializer - The BCS deserializer to read from. + * @returns A new AccountAddress instance. + */ + static deserialize(deserializer: Deserializer): AccountAddress { + const bytes = deserializer.deserializeFixedBytes(AccountAddress.LENGTH); + return new AccountAddress(bytes); + } + + /** + * Parses an address from a hex string using strict validation rules. + * Requires "0x" prefix, full 64-character form for non-special addresses, + * and no padding zeroes for special addresses. + * @param input - The hex string to parse (must start with "0x"). + * @returns A new AccountAddress instance. + * @throws {ParsingError} If the input does not satisfy strict formatting rules. + */ + static fromStringStrict(input: string): AccountAddress { + if (!input.startsWith("0x")) { + throw new ParsingError("Hex string must start with a leading 0x.", AddressInvalidReason.LEADING_ZERO_X_REQUIRED); + } + + const address = AccountAddress.fromString(input); + + if (input.length !== AccountAddress.LONG_STRING_LENGTH + 2) { + if (!address.isSpecial()) { + throw new ParsingError( + `The given hex string ${input} is not a special address, it must be represented as 0x + 64 chars.`, + AddressInvalidReason.LONG_FORM_REQUIRED_UNLESS_SPECIAL, + ); + } else if (input.length !== 3) { + throw new ParsingError( + `The given hex string ${input} is a special address not in LONG form, it must be 0x0 to 0xf without padding zeroes.`, + AddressInvalidReason.INVALID_PADDING_ZEROES, + ); + } + } + + return address; + } + + /** + * Parses an address from a hex string with relaxed validation. + * Allows optional "0x" prefix and short-form addresses. + * @param input - The hex string to parse (with or without "0x" prefix). + * @param options - Options for parsing. + * @param options.maxMissingChars - Maximum leading zeroes that may be omitted (default 4). + * Special addresses (0x0-0xf) are always accepted regardless of this setting. + * @returns A new AccountAddress instance. + * @throws {ParsingError} If the input cannot be parsed as a valid address. + */ + static fromString(input: string, { maxMissingChars = 4 }: { maxMissingChars?: number } = {}): AccountAddress { + let parsedInput = input; + if (input.startsWith("0x")) { + parsedInput = input.slice(2); + } + + if (parsedInput.length === 0) { + throw new ParsingError( + "Hex string is too short, must be 1 to 64 chars long, excluding the leading 0x.", + AddressInvalidReason.TOO_SHORT, + ); + } + + if (parsedInput.length > 64) { + throw new ParsingError( + "Hex string is too long, must be 1 to 64 chars long, excluding the leading 0x.", + AddressInvalidReason.TOO_LONG, + ); + } + + if (maxMissingChars > 63 || maxMissingChars < 0) { + throw new ParsingError( + `maxMissingChars must be between or equal to 0 and 63. Received ${maxMissingChars}`, + AddressInvalidReason.INVALID_PADDING_STRICTNESS, + ); + } + + let addressBytes: Uint8Array; + try { + addressBytes = hexToBytes(parsedInput.padStart(64, "0")); + } catch (error: unknown) { + throw new ParsingError( + `Hex characters are invalid: ${error instanceof Error ? error.message : String(error)}`, + AddressInvalidReason.INVALID_HEX_CHARS, + ); + } + + const address = new AccountAddress(addressBytes); + + if (parsedInput.length < 64 - maxMissingChars) { + if (!address.isSpecial()) { + throw new ParsingError( + `Hex string is too short, must be ${64 - maxMissingChars} to 64 chars long, excluding the leading 0x. Received ${input}`, + AddressInvalidReason.TOO_SHORT, + ); + } + } + + return address; + } + + /** + * Creates an AccountAddress from a string, byte array, or existing AccountAddress. + * @param input - The address input (hex string, Uint8Array, or AccountAddress). + * @param options - Options for string parsing. + * @param options.maxMissingChars - Maximum leading zeroes that may be omitted (default 4). + * @returns A new or existing AccountAddress instance. + */ + static from(input: AccountAddressInput, { maxMissingChars = 4 }: { maxMissingChars?: number } = {}): AccountAddress { + if (typeof input === "string") { + return AccountAddress.fromString(input, { maxMissingChars }); + } + if (input instanceof Uint8Array) { + return new AccountAddress(input); + } + return input; + } + + /** + * Creates an AccountAddress from any accepted input using strict validation for strings. + * @param input - The address input (hex string, Uint8Array, or AccountAddress). + * @returns A new or existing AccountAddress instance. + * @throws {ParsingError} If a string input does not satisfy strict formatting rules. + */ + static fromStrict(input: AccountAddressInput): AccountAddress { + if (typeof input === "string") { + return AccountAddress.fromStringStrict(input); + } + if (input instanceof Uint8Array) { + return new AccountAddress(input); + } + return input; + } + + /** + * Checks whether the given input is a valid account address without throwing. + * @param args.input - The address input to validate. + * @param args.strict - If true, applies strict formatting rules (default false). + * @returns A {@link ParsingResult} indicating validity, or the reason for invalidity. + */ + static isValid(args: { input: AccountAddressInput; strict?: boolean }): ParsingResult { + try { + if (args.strict) { + AccountAddress.fromStrict(args.input); + } else { + AccountAddress.from(args.input); + } + return { valid: true }; + } catch (error: unknown) { + if (error instanceof ParsingError) { + return { + valid: false, + invalidReason: error.invalidReason as AddressInvalidReason, + invalidReasonMessage: error.message, + }; + } + return { valid: false, invalidReasonMessage: error instanceof Error ? error.message : String(error) }; + } + } + + /** + * Compares this address to another for byte-level equality. + * @param other - The address to compare against. + * @returns True if both addresses contain identical bytes. + */ + /** + * Constant-time comparison to avoid timing side-channels when comparing addresses. + */ + equals(other: AccountAddress): boolean { + if (this.data.length !== other.data.length) return false; + let result = 0; + for (let i = 0; i < this.data.length; i++) { + result |= this.data[i] ^ other.data[i]; + } + return result === 0; + } +} diff --git a/v10/src/core/authentication-key.ts b/v10/src/core/authentication-key.ts new file mode 100644 index 000000000..3434ea9e8 --- /dev/null +++ b/v10/src/core/authentication-key.ts @@ -0,0 +1,91 @@ +import { sha3_256 } from "@noble/hashes/sha3.js"; +import type { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; +import type { AccountPublicKey } from "../crypto/public-key.js"; +import type { AuthenticationKeyScheme } from "../crypto/types.js"; +import { Hex, type HexInput } from "../hex/index.js"; +import { AccountAddress } from "./account-address.js"; + +/** + * Represents a 32-byte authentication key derived from a public key and signing scheme. + * + * Authentication keys are used to derive account addresses and to verify + * that a transaction signer is authorized for a given account. + */ +export class AuthenticationKey extends Serializable { + /** The fixed byte length of an authentication key. */ + static readonly LENGTH: number = 32; + + /** The raw authentication key data as a Hex wrapper. */ + public readonly data: Hex; + + /** + * Creates an AuthenticationKey from raw hex input. + * @param args.data - The 32-byte authentication key as hex string or byte array. + * @throws If the input is not exactly 32 bytes. + */ + constructor(args: { data: HexInput }) { + super(); + const { data } = args; + const hex = Hex.fromHexInput(data); + if (hex.toUint8Array().length !== AuthenticationKey.LENGTH) { + throw new Error(`Authentication Key length should be ${AuthenticationKey.LENGTH}`); + } + this.data = hex; + } + + /** Serializes the authentication key as fixed-length bytes via BCS. */ + serialize(serializer: Serializer): void { + serializer.serializeFixedBytes(this.data.toUint8Array()); + } + + /** + * Deserializes an AuthenticationKey from BCS bytes. + * @param deserializer - The BCS deserializer to read from. + * @returns A new AuthenticationKey instance. + */ + static deserialize(deserializer: Deserializer): AuthenticationKey { + const bytes = deserializer.deserializeFixedBytes(AuthenticationKey.LENGTH); + return new AuthenticationKey({ data: bytes }); + } + + /** Returns the underlying 32-byte array. */ + toUint8Array(): Uint8Array { + return this.data.toUint8Array(); + } + + /** + * Derives an authentication key by hashing the input bytes concatenated with the scheme identifier using SHA3-256. + * @param args.scheme - The authentication key scheme (e.g., Ed25519, MultiKey). + * @param args.input - The public key bytes to derive from. + * @returns A new AuthenticationKey derived from the scheme and input. + */ + static fromSchemeAndBytes(args: { scheme: AuthenticationKeyScheme; input: HexInput }): AuthenticationKey { + const { scheme, input } = args; + const inputBytes = Hex.fromHexInput(input).toUint8Array(); + const hashInput = new Uint8Array(inputBytes.length + 1); + hashInput.set(inputBytes); + hashInput[inputBytes.length] = scheme; + const hashDigest = sha3_256.create().update(hashInput).digest(); + return new AuthenticationKey({ data: hashDigest }); + } + + /** + * Derives an authentication key from an account public key. + * @param args.publicKey - The public key to derive the authentication key from. + * @returns The authentication key for the given public key. + */ + static fromPublicKey(args: { publicKey: AccountPublicKey }): AuthenticationKey { + const { publicKey } = args; + return publicKey.authKey() as AuthenticationKey; + } + + /** + * Derives the account address from this authentication key. + * The address is the authentication key bytes interpreted as an AccountAddress. + * @returns The derived AccountAddress. + */ + derivedAddress(): AccountAddress { + return new AccountAddress(this.data.toUint8Array()); + } +} diff --git a/v10/src/core/constants.ts b/v10/src/core/constants.ts new file mode 100644 index 000000000..1749b562b --- /dev/null +++ b/v10/src/core/constants.ts @@ -0,0 +1,50 @@ +/** The type of Aptos API service being called. Used for error reporting and endpoint routing. */ +export enum AptosApiType { + /** The full node REST API. */ + FULLNODE = "Fullnode", + /** The GraphQL indexer API. */ + INDEXER = "Indexer", + /** The faucet API for funding test accounts. */ + FAUCET = "Faucet", + /** The Keyless pepper service API. */ + PEPPER = "Pepper", + /** The Keyless prover service API. */ + PROVER = "Prover", +} + +/** Default maximum gas amount for transactions (in gas units). */ +export const DEFAULT_MAX_GAS_AMOUNT = 200000; +/** Minimum value allowed for the max gas amount field. */ +export const MIN_MAX_GAS_AMOUNT = 2000; +/** Default transaction expiration in seconds from now. */ +export const DEFAULT_TXN_EXP_SEC_FROM_NOW = 20; +/** Default timeout in seconds when waiting for a transaction to be committed. */ +export const DEFAULT_TXN_TIMEOUT_SEC = 20; + +/** The fully qualified type string for the native Aptos coin (`0x1::aptos_coin::AptosCoin`). */ +export const APTOS_COIN = "0x1::aptos_coin::AptosCoin"; +/** The fungible asset metadata address for the native APT token. */ +export const APTOS_FA = "0x000000000000000000000000000000000000000000000000000000000000000a"; + +/** Domain separation salt used for hashing raw transactions before signing. */ +export const RAW_TRANSACTION_SALT = "APTOS::RawTransaction"; +/** Domain separation salt used for hashing raw transactions with additional data (e.g., multi-agent). */ +export const RAW_TRANSACTION_WITH_DATA_SALT = "APTOS::RawTransactionWithData"; +/** Domain separation salt used for hashing account abstraction signing data. */ +export const ACCOUNT_ABSTRACTION_SIGNING_DATA_SALT = "APTOS::AASigningData"; + +/** Indexer processor types, used when waiting for a specific indexer processor to sync. */ +export enum ProcessorType { + ACCOUNT_RESTORATION_PROCESSOR = "account_restoration_processor", + ACCOUNT_TRANSACTION_PROCESSOR = "account_transactions_processor", + DEFAULT = "default_processor", + EVENTS_PROCESSOR = "events_processor", + FUNGIBLE_ASSET_PROCESSOR = "fungible_asset_processor", + STAKE_PROCESSOR = "stake_processor", + TOKEN_V2_PROCESSOR = "token_v2_processor", + USER_TRANSACTION_PROCESSOR = "user_transaction_processor", + OBJECT_PROCESSOR = "objects_processor", +} + +/** Regex pattern for matching Firebase Auth JWT issuer URLs (used in Keyless federated authentication). */ +export const FIREBASE_AUTH_ISS_PATTERN = /^https:\/\/securetoken\.google\.com\/[a-zA-Z0-9-_]+$/; diff --git a/v10/src/core/errors.ts b/v10/src/core/errors.ts new file mode 100644 index 000000000..93555a47b --- /dev/null +++ b/v10/src/core/errors.ts @@ -0,0 +1,435 @@ +import { AptosApiType } from "./constants.js"; + +// ── Keyless Errors ── + +/** High-level categories for classifying Keyless authentication errors. */ +export enum KeylessErrorCategory { + /** An error from an Aptos-operated API (pepper, prover, full node). */ + API_ERROR, + /** An error from an external API (e.g., OIDC provider JWKS endpoint). */ + EXTERNAL_API_ERROR, + /** The keyless session has expired and re-authentication is required. */ + SESSION_EXPIRED, + /** The keyless account is in an invalid internal state. */ + INVALID_STATE, + /** The keyless signature failed verification on-chain. */ + INVALID_SIGNATURE, + /** An unknown or uncategorized error. */ + UNKNOWN, +} + +/** Human-readable resolution tips for Keyless errors, guiding developers toward a fix. */ +export enum KeylessErrorResolutionTip { + REAUTHENTICATE = "Re-authenticate to continue using your keyless account", + REAUTHENTICATE_UNSURE = "Try re-authenticating. If the error persists join the telegram group at https://t.me/+h5CN-W35yUFiYzkx for further support", + UPDATE_REQUEST_PARAMS = "Update the invalid request parameters and reauthenticate.", + RATE_LIMIT_EXCEEDED = "Cache the keyless account and reuse it to avoid making too many requests. Keyless accounts are valid until either the EphemeralKeyPair expires, when the JWK is rotated, or when the proof verifying key is changed, whichever comes soonest.", + SERVER_ERROR = "Try again later. See aptosApiError error for more context. For additional support join the telegram group at https://t.me/+h5CN-W35yUFiYzkx", + CALL_PRECHECK = "Call `await account.checkKeylessAccountValidity()` to wait for asynchronous changes and check for account validity before signing or serializing.", + REINSTANTIATE = "Try instantiating the account again. Avoid manipulating the account object directly", + JOIN_SUPPORT_GROUP = "For support join the telegram group at https://t.me/+h5CN-W35yUFiYzkx", + UNKNOWN = "Error unknown. For support join the telegram group at https://t.me/+h5CN-W35yUFiYzkx", +} + +/** Specific error types that can occur during Keyless account operations. */ +export enum KeylessErrorType { + EPHEMERAL_KEY_PAIR_EXPIRED, + PROOF_NOT_FOUND, + ASYNC_PROOF_FETCH_FAILED, + INVALID_PROOF_VERIFICATION_FAILED, + INVALID_PROOF_VERIFICATION_KEY_NOT_FOUND, + INVALID_JWT_SIG, + INVALID_JWT_JWK_NOT_FOUND, + INVALID_JWT_ISS_NOT_RECOGNIZED, + INVALID_JWT_FEDERATED_ISS_NOT_SUPPORTED, + INVALID_TW_SIG_VERIFICATION_FAILED, + INVALID_TW_SIG_PUBLIC_KEY_NOT_FOUND, + INVALID_EXPIRY_HORIZON, + JWT_PARSING_ERROR, + JWK_FETCH_FAILED, + JWK_FETCH_FAILED_FEDERATED, + RATE_LIMIT_EXCEEDED, + PEPPER_SERVICE_INTERNAL_ERROR, + PEPPER_SERVICE_BAD_REQUEST, + PEPPER_SERVICE_OTHER, + PROVER_SERVICE_INTERNAL_ERROR, + PROVER_SERVICE_BAD_REQUEST, + PROVER_SERVICE_OTHER, + FULL_NODE_CONFIG_LOOKUP_ERROR, + FULL_NODE_VERIFICATION_KEY_LOOKUP_ERROR, + FULL_NODE_JWKS_LOOKUP_ERROR, + FULL_NODE_OTHER, + SIGNATURE_TYPE_INVALID, + SIGNATURE_EXPIRED, + MAX_EXPIRY_HORIZON_EXCEEDED, + EPHEMERAL_SIGNATURE_VERIFICATION_FAILED, + TRAINING_WHEELS_SIGNATURE_MISSING, + TRAINING_WHEELS_SIGNATURE_VERIFICATION_FAILED, + PROOF_VERIFICATION_FAILED, + UNKNOWN, +} + +const KeylessErrors: { [key in KeylessErrorType]: [string, KeylessErrorCategory, KeylessErrorResolutionTip] } = { + [KeylessErrorType.EPHEMERAL_KEY_PAIR_EXPIRED]: [ + "The ephemeral keypair has expired.", + KeylessErrorCategory.SESSION_EXPIRED, + KeylessErrorResolutionTip.REAUTHENTICATE, + ], + [KeylessErrorType.PROOF_NOT_FOUND]: [ + "The required proof could not be found.", + KeylessErrorCategory.INVALID_STATE, + KeylessErrorResolutionTip.CALL_PRECHECK, + ], + [KeylessErrorType.ASYNC_PROOF_FETCH_FAILED]: [ + "The required proof failed to fetch.", + KeylessErrorCategory.INVALID_STATE, + KeylessErrorResolutionTip.REAUTHENTICATE_UNSURE, + ], + [KeylessErrorType.INVALID_PROOF_VERIFICATION_FAILED]: [ + "The provided proof is invalid.", + KeylessErrorCategory.INVALID_STATE, + KeylessErrorResolutionTip.REAUTHENTICATE_UNSURE, + ], + [KeylessErrorType.INVALID_PROOF_VERIFICATION_KEY_NOT_FOUND]: [ + "The verification key used to authenticate was updated.", + KeylessErrorCategory.SESSION_EXPIRED, + KeylessErrorResolutionTip.REAUTHENTICATE, + ], + [KeylessErrorType.INVALID_JWT_SIG]: [ + "The JWK was found, but JWT failed verification", + KeylessErrorCategory.INVALID_STATE, + KeylessErrorResolutionTip.REAUTHENTICATE_UNSURE, + ], + [KeylessErrorType.INVALID_JWT_JWK_NOT_FOUND]: [ + "The JWK required to verify the JWT could not be found. The JWK may have been rotated out.", + KeylessErrorCategory.SESSION_EXPIRED, + KeylessErrorResolutionTip.REAUTHENTICATE, + ], + [KeylessErrorType.INVALID_JWT_ISS_NOT_RECOGNIZED]: [ + "The JWT issuer is not recognized.", + KeylessErrorCategory.INVALID_STATE, + KeylessErrorResolutionTip.UPDATE_REQUEST_PARAMS, + ], + [KeylessErrorType.INVALID_JWT_FEDERATED_ISS_NOT_SUPPORTED]: [ + "The JWT issuer is not supported by Federated Keyless.", + KeylessErrorCategory.API_ERROR, + KeylessErrorResolutionTip.REAUTHENTICATE_UNSURE, + ], + [KeylessErrorType.INVALID_TW_SIG_VERIFICATION_FAILED]: [ + "The training wheels signature is invalid.", + KeylessErrorCategory.INVALID_STATE, + KeylessErrorResolutionTip.REAUTHENTICATE_UNSURE, + ], + [KeylessErrorType.INVALID_TW_SIG_PUBLIC_KEY_NOT_FOUND]: [ + "The public key used to verify the training wheels signature was not found.", + KeylessErrorCategory.SESSION_EXPIRED, + KeylessErrorResolutionTip.REAUTHENTICATE, + ], + [KeylessErrorType.INVALID_EXPIRY_HORIZON]: [ + "The expiry horizon is invalid.", + KeylessErrorCategory.SESSION_EXPIRED, + KeylessErrorResolutionTip.REAUTHENTICATE, + ], + [KeylessErrorType.JWT_PARSING_ERROR]: [ + "Error when parsing JWT.", + KeylessErrorCategory.INVALID_STATE, + KeylessErrorResolutionTip.REINSTANTIATE, + ], + [KeylessErrorType.JWK_FETCH_FAILED]: [ + "Failed to fetch JWKS.", + KeylessErrorCategory.EXTERNAL_API_ERROR, + KeylessErrorResolutionTip.JOIN_SUPPORT_GROUP, + ], + [KeylessErrorType.JWK_FETCH_FAILED_FEDERATED]: [ + "Failed to fetch JWKS for Federated Keyless provider.", + KeylessErrorCategory.EXTERNAL_API_ERROR, + KeylessErrorResolutionTip.JOIN_SUPPORT_GROUP, + ], + [KeylessErrorType.RATE_LIMIT_EXCEEDED]: [ + "Rate limit exceeded. Too many requests in a short period.", + KeylessErrorCategory.API_ERROR, + KeylessErrorResolutionTip.RATE_LIMIT_EXCEEDED, + ], + [KeylessErrorType.PEPPER_SERVICE_INTERNAL_ERROR]: [ + "Internal error from Pepper service.", + KeylessErrorCategory.API_ERROR, + KeylessErrorResolutionTip.SERVER_ERROR, + ], + [KeylessErrorType.PEPPER_SERVICE_BAD_REQUEST]: [ + "Bad request sent to Pepper service.", + KeylessErrorCategory.API_ERROR, + KeylessErrorResolutionTip.UPDATE_REQUEST_PARAMS, + ], + [KeylessErrorType.PEPPER_SERVICE_OTHER]: [ + "Unknown error from Pepper service.", + KeylessErrorCategory.API_ERROR, + KeylessErrorResolutionTip.SERVER_ERROR, + ], + [KeylessErrorType.PROVER_SERVICE_INTERNAL_ERROR]: [ + "Internal error from Prover service.", + KeylessErrorCategory.API_ERROR, + KeylessErrorResolutionTip.SERVER_ERROR, + ], + [KeylessErrorType.PROVER_SERVICE_BAD_REQUEST]: [ + "Bad request sent to Prover service.", + KeylessErrorCategory.API_ERROR, + KeylessErrorResolutionTip.UPDATE_REQUEST_PARAMS, + ], + [KeylessErrorType.PROVER_SERVICE_OTHER]: [ + "Unknown error from Prover service.", + KeylessErrorCategory.API_ERROR, + KeylessErrorResolutionTip.SERVER_ERROR, + ], + [KeylessErrorType.FULL_NODE_CONFIG_LOOKUP_ERROR]: [ + "Error when looking up on-chain keyless configuration.", + KeylessErrorCategory.API_ERROR, + KeylessErrorResolutionTip.SERVER_ERROR, + ], + [KeylessErrorType.FULL_NODE_VERIFICATION_KEY_LOOKUP_ERROR]: [ + "Error when looking up on-chain verification key.", + KeylessErrorCategory.API_ERROR, + KeylessErrorResolutionTip.SERVER_ERROR, + ], + [KeylessErrorType.FULL_NODE_JWKS_LOOKUP_ERROR]: [ + "Error when looking up on-chain JWKS.", + KeylessErrorCategory.API_ERROR, + KeylessErrorResolutionTip.SERVER_ERROR, + ], + [KeylessErrorType.FULL_NODE_OTHER]: [ + "Unknown error from full node.", + KeylessErrorCategory.API_ERROR, + KeylessErrorResolutionTip.SERVER_ERROR, + ], + [KeylessErrorType.SIGNATURE_TYPE_INVALID]: [ + "The signature is not a valid Keyless signature.", + KeylessErrorCategory.INVALID_SIGNATURE, + KeylessErrorResolutionTip.JOIN_SUPPORT_GROUP, + ], + [KeylessErrorType.SIGNATURE_EXPIRED]: [ + "The ephemeral key pair used to sign the message has expired.", + KeylessErrorCategory.INVALID_SIGNATURE, + KeylessErrorResolutionTip.REAUTHENTICATE, + ], + [KeylessErrorType.MAX_EXPIRY_HORIZON_EXCEEDED]: [ + "The expiry horizon on the signature exceeds the maximum allowed value.", + KeylessErrorCategory.INVALID_SIGNATURE, + KeylessErrorResolutionTip.REAUTHENTICATE, + ], + [KeylessErrorType.EPHEMERAL_SIGNATURE_VERIFICATION_FAILED]: [ + "Failed to verify the ephemeral signature with the ephemeral public key.", + KeylessErrorCategory.INVALID_SIGNATURE, + KeylessErrorResolutionTip.REAUTHENTICATE, + ], + [KeylessErrorType.TRAINING_WHEELS_SIGNATURE_MISSING]: [ + "The training wheels signature is missing but is required by the Keyless configuration.", + KeylessErrorCategory.INVALID_SIGNATURE, + KeylessErrorResolutionTip.REAUTHENTICATE, + ], + [KeylessErrorType.TRAINING_WHEELS_SIGNATURE_VERIFICATION_FAILED]: [ + "Failed to verify the training wheels signature with the training wheels public key.", + KeylessErrorCategory.INVALID_SIGNATURE, + KeylessErrorResolutionTip.REAUTHENTICATE, + ], + [KeylessErrorType.PROOF_VERIFICATION_FAILED]: [ + "The proof verification failed.", + KeylessErrorCategory.INVALID_SIGNATURE, + KeylessErrorResolutionTip.REAUTHENTICATE, + ], + [KeylessErrorType.UNKNOWN]: [ + "An unknown error has occurred.", + KeylessErrorCategory.UNKNOWN, + KeylessErrorResolutionTip.UNKNOWN, + ], +}; + +/** + * Error class for Keyless authentication failures. + * + * Provides structured error information including category, type, resolution tips, + * and optional inner errors from API calls. + */ +export class KeylessError extends Error { + /** The underlying error that caused this KeylessError, if any. */ + readonly innerError?: unknown; + /** The broad category of this error. */ + readonly category: KeylessErrorCategory; + /** A human-readable tip for resolving this error. */ + readonly resolutionTip: KeylessErrorResolutionTip; + /** The specific error type. */ + readonly type: KeylessErrorType; + /** Additional details about the error context. */ + readonly details?: string; + + /** + * @param args.innerError - The underlying error, if any. + * @param args.category - The error category. + * @param args.resolutionTip - A resolution tip for the developer. + * @param args.type - The specific error type. + * @param args.message - Optional custom message (defaults to the message for the given type). + * @param args.details - Optional additional details. + */ + constructor(args: { + innerError?: unknown; + category: KeylessErrorCategory; + resolutionTip: KeylessErrorResolutionTip; + type: KeylessErrorType; + message?: string; + details?: string; + }) { + const { innerError, category, resolutionTip, type, message = KeylessErrors[type][0], details } = args; + super(message); + this.name = "KeylessError"; + this.innerError = innerError; + this.category = category; + this.resolutionTip = resolutionTip; + this.type = type; + this.details = details; + this.message = KeylessError.constructMessage(message, resolutionTip, innerError, details); + } + + static constructMessage( + message: string, + tip: KeylessErrorResolutionTip, + innerError?: unknown, + details?: string, + ): string { + let result = `\nMessage: ${message}`; + if (details) { + result += `\nDetails: ${details}`; + } + if (innerError instanceof AptosApiError) { + result += `\nAptosApiError: ${innerError.message}`; + } else if (innerError !== undefined) { + result += `\nError: ${innerError instanceof Error ? innerError.message : String(innerError)}`; + } + result += `\nKeylessErrorResolutionTip: ${tip}`; + return result; + } + + /** + * Creates a KeylessError from a {@link KeylessErrorType}, automatically looking up + * the default message, category, and resolution tip. + * @param args.type - The specific error type. + * @param args.error - The underlying error, if any. + * @param args.details - Optional additional details. + * @returns A new KeylessError instance. + */ + static fromErrorType(args: { type: KeylessErrorType; error?: unknown; details?: string }): KeylessError { + const { error, type, details } = args; + const [message, category, resolutionTip] = KeylessErrors[type]; + return new KeylessError({ message, details, innerError: error, category, resolutionTip, type }); + } +} + +// ── API Errors ── + +/** Describes the parameters of an outgoing HTTP request to an Aptos API. */ +export interface AptosRequest { + url: string; + method: string; + body?: unknown; + contentType?: string; + params?: Record; + overrides?: Record; + originMethod?: string; + headers?: Record; +} + +/** + * Describes an HTTP response received from an Aptos API. + * @typeParam Res - The type of the response body data. + * @typeParam Req - The type of the underlying request object. + */ +export interface AptosResponse { + status: number; + statusText: string; + data: Res; + url: string; + headers?: Record; + config?: unknown; + request?: Req; +} + +/** + * Error thrown when an Aptos API request fails. + * Contains the request details, HTTP status, and response body for debugging. + */ +export class AptosApiError extends Error { + /** The URL that was requested. */ + readonly url: string; + /** The HTTP status code of the response. */ + readonly status: number; + /** The HTTP status text of the response. */ + readonly statusText: string; + /** The response body data. */ + readonly data: unknown; + /** The original request parameters. */ + readonly request: AptosRequest; + + /** + * @param args.apiType - The type of API that was called (fullnode, indexer, etc.). + * @param args.aptosRequest - The original request parameters. + * @param args.aptosResponse - The response received from the API. + */ + constructor(args: { + apiType: AptosApiType; + aptosRequest: AptosRequest; + aptosResponse: AptosResponse; + }) { + const { apiType, aptosRequest, aptosResponse } = args; + super(deriveErrorMessage({ apiType, aptosRequest, aptosResponse })); + this.name = "AptosApiError"; + this.url = aptosResponse.url; + this.status = aptosResponse.status; + this.statusText = aptosResponse.statusText; + this.data = aptosResponse.data; + this.request = aptosRequest; + } +} + +/** Strips query string and userinfo from a URL to avoid leaking credentials in error messages. */ +function sanitizeUrl(url: string): string { + try { + const parsed = new URL(url); + parsed.search = ""; + parsed.username = ""; + parsed.password = ""; + return parsed.toString(); + } catch { + return url.split("?")[0]; + } +} + +function deriveErrorMessage(args: { + apiType: AptosApiType; + aptosRequest: AptosRequest; + aptosResponse: AptosResponse; +}): string { + const { apiType, aptosRequest, aptosResponse } = args; + const traceId = aptosResponse.headers?.traceparent?.split("-")[1]; + const traceIdString = traceId ? `(trace_id:${traceId}) ` : ""; + + const displayUrl = sanitizeUrl(aptosResponse.url ?? aptosRequest.url); + const errorPrelude = `Request to [${apiType}]: ${aptosRequest.method} ${displayUrl} ${traceIdString}failed with`; + + const data = aptosResponse.data as Record | undefined; + if ( + apiType === AptosApiType.INDEXER && + (data?.errors as Array<{ message?: string }> | undefined)?.[0]?.message != null + ) { + return `${errorPrelude}: ${(data?.errors as Array<{ message: string }>)[0].message}`; + } + + if (data?.message != null && data?.error_code != null) { + return `${errorPrelude}: ${JSON.stringify(aptosResponse.data)}`; + } + + const MAX_LEN = 400; + const serialized = JSON.stringify(aptosResponse.data); + const body = + serialized.length <= MAX_LEN + ? serialized + : `truncated(original_size:${serialized.length}): ${serialized.slice(0, MAX_LEN / 2)}...${serialized.slice(-MAX_LEN / 2)}`; + + return `${errorPrelude} status: ${aptosResponse.statusText}(code:${aptosResponse.status}) and response body: ${body}`; +} diff --git a/v10/src/core/index.ts b/v10/src/core/index.ts new file mode 100644 index 000000000..cd266ace7 --- /dev/null +++ b/v10/src/core/index.ts @@ -0,0 +1,21 @@ +export * from "./account-address.js"; +export * from "./authentication-key.js"; +export * from "./constants.js"; +export { + AptosApiError, + KeylessError, + KeylessErrorCategory, + KeylessErrorResolutionTip, + KeylessErrorType, +} from "./errors.js"; +export * from "./network.js"; +export * from "./type-tag.js"; + +// Register auth key derivation factory for crypto module. +// This breaks the circular dep: crypto can't import core, but core can import crypto. +import { registerAuthKeyFactory } from "../crypto/public-key.js"; +import { AuthenticationKey } from "./authentication-key.js"; + +registerAuthKeyFactory((scheme, publicKeyBytes) => + AuthenticationKey.fromSchemeAndBytes({ scheme, input: publicKeyBytes }), +); diff --git a/v10/src/core/network.ts b/v10/src/core/network.ts new file mode 100644 index 000000000..ac2221c9c --- /dev/null +++ b/v10/src/core/network.ts @@ -0,0 +1,83 @@ +/** The available Aptos network environments. Used to configure SDK clients. */ +export enum Network { + /** The production Aptos mainnet. */ + MAINNET = "mainnet", + /** The long-lived Aptos testnet for integration testing. */ + TESTNET = "testnet", + /** The Aptos devnet for development (resets frequently). */ + DEVNET = "devnet", + /** The Shelby experimental network. */ + SHELBYNET = "shelbynet", + /** The Netna staging network. */ + NETNA = "netna", + /** A locally running Aptos node (e.g., via `aptos node run-localnet`). */ + LOCAL = "local", + /** A custom network with user-specified endpoints. */ + CUSTOM = "custom", +} + +/** Maps network names to their GraphQL indexer API endpoints. */ +export const NetworkToIndexerAPI: Partial> = { + mainnet: "https://api.mainnet.aptoslabs.com/v1/graphql", + testnet: "https://api.testnet.aptoslabs.com/v1/graphql", + devnet: "https://api.devnet.aptoslabs.com/v1/graphql", + shelbynet: "https://api.shelbynet.shelby.xyz/v1/graphql", + netna: "https://api.netna.staging.aptoslabs.com/v1/graphql", + local: "http://127.0.0.1:8090/v1/graphql", +}; + +/** Maps network names to their full node REST API endpoints. */ +export const NetworkToNodeAPI: Partial> = { + mainnet: "https://api.mainnet.aptoslabs.com/v1", + testnet: "https://api.testnet.aptoslabs.com/v1", + devnet: "https://api.devnet.aptoslabs.com/v1", + shelbynet: "https://api.shelbynet.shelby.xyz/v1", + netna: "https://api.netna.staging.aptoslabs.com/v1", + local: "http://127.0.0.1:8080/v1", +}; + +/** Maps network names to their faucet API endpoints (not all networks have faucets). */ +export const NetworkToFaucetAPI: Partial> = { + devnet: "https://faucet.devnet.aptoslabs.com", + shelbynet: "https://faucet.shelbynet.shelby.xyz", + netna: "https://faucet-dev-netna-us-central1-410192433417.us-central1.run.app", + local: "http://127.0.0.1:8081", +}; + +/** Maps network names to their Keyless pepper service API endpoints. */ +export const NetworkToPepperAPI: Partial> = { + mainnet: "https://api.mainnet.aptoslabs.com/keyless/pepper/v0", + testnet: "https://api.testnet.aptoslabs.com/keyless/pepper/v0", + devnet: "https://api.devnet.aptoslabs.com/keyless/pepper/v0", + shelbynet: "https://api.shelbynet.aptoslabs.com/keyless/pepper/v0", + netna: "https://api.devnet.aptoslabs.com/keyless/pepper/v0", + local: "https://api.devnet.aptoslabs.com/keyless/pepper/v0", +}; + +/** Maps network names to their Keyless prover service API endpoints. */ +export const NetworkToProverAPI: Partial> = { + mainnet: "https://api.mainnet.aptoslabs.com/keyless/prover/v0", + testnet: "https://api.testnet.aptoslabs.com/keyless/prover/v0", + devnet: "https://api.devnet.aptoslabs.com/keyless/prover/v0", + shelbynet: "https://api.shelbynet.aptoslabs.com/keyless/prover/v0", + netna: "https://api.devnet.aptoslabs.com/keyless/prover/v0", + local: "https://api.devnet.aptoslabs.com/keyless/prover/v0", +}; + +/** Maps network names to their known chain IDs (only networks with stable chain IDs are included). */ +export const NetworkToChainId: Partial> = { + mainnet: 1, + testnet: 2, + local: 4, +}; + +/** Maps network name strings to their corresponding {@link Network} enum values. */ +export const NetworkToNetworkName: Record = { + mainnet: Network.MAINNET, + testnet: Network.TESTNET, + devnet: Network.DEVNET, + shelbynet: Network.SHELBYNET, + netna: Network.NETNA, + local: Network.LOCAL, + custom: Network.CUSTOM, +}; diff --git a/v10/src/core/type-tag.ts b/v10/src/core/type-tag.ts new file mode 100644 index 000000000..b1b6abd88 --- /dev/null +++ b/v10/src/core/type-tag.ts @@ -0,0 +1,658 @@ +import type { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; +import { AccountAddress } from "./account-address.js"; + +/** BCS variant indices for each Move type tag kind, used during serialization and deserialization. */ +export enum TypeTagVariants { + Bool = 0, + U8 = 1, + U64 = 2, + U128 = 3, + Address = 4, + Signer = 5, + Vector = 6, + Struct = 7, + U16 = 8, + U32 = 9, + U256 = 10, + I8 = 11, + I16 = 12, + I32 = 13, + I64 = 14, + I128 = 15, + I256 = 16, + /** A reference type tag (used internally, not valid in transaction arguments). */ + Reference = 254, + /** A generic type parameter placeholder (e.g., T0, T1). */ + Generic = 255, +} + +const MAX_TYPE_TAG_NESTING = 128; + +// ── Identifier (simple BCS string wrapper) ── + +/** + * A BCS-serializable wrapper around a string, used for Move module and struct names. + */ +export class Identifier extends Serializable { + /** The raw identifier string. */ + public identifier: string; + + /** + * @param identifier - The identifier string (e.g., a module name or struct name). + */ + constructor(identifier: string) { + super(); + this.identifier = identifier; + } + + public serialize(serializer: Serializer): void { + serializer.serializeStr(this.identifier); + } + + /** + * Deserializes an Identifier from BCS bytes. + * @param deserializer - The BCS deserializer to read from. + * @returns A new Identifier instance. + */ + static deserialize(deserializer: Deserializer): Identifier { + const identifier = deserializer.deserializeStr(); + return new Identifier(identifier); + } +} + +// ── TypeTag hierarchy ── + +/** + * Abstract base class for all Move type tags. + * + * Type tags represent Move types (bool, u8, address, vector, struct, etc.) + * and are used in transaction building to specify type arguments. + */ +export abstract class TypeTag extends Serializable { + abstract serialize(serializer: Serializer): void; + /** Returns the Move string representation of this type tag (e.g., `"bool"`, `"vector"`). */ + abstract toString(): string; + + /** + * Deserializes a TypeTag from BCS bytes by reading the variant index and dispatching. + * @param deserializer - The BCS deserializer to read from. + * @param depth - Current recursion depth (guards against stack overflow from malicious payloads). + * @returns The deserialized TypeTag subclass instance. + */ + static deserialize(deserializer: Deserializer, depth = 0): TypeTag { + if (depth > MAX_TYPE_TAG_NESTING) { + throw new Error(`TypeTag deserialization exceeded maximum nesting depth of ${MAX_TYPE_TAG_NESTING}`); + } + const index = deserializer.deserializeUleb128AsU32(); + switch (index) { + case TypeTagVariants.Bool: + return TypeTagBool.load(deserializer); + case TypeTagVariants.U8: + return TypeTagU8.load(deserializer); + case TypeTagVariants.U64: + return TypeTagU64.load(deserializer); + case TypeTagVariants.U128: + return TypeTagU128.load(deserializer); + case TypeTagVariants.Address: + return TypeTagAddress.load(deserializer); + case TypeTagVariants.Signer: + return TypeTagSigner.load(deserializer); + case TypeTagVariants.Vector: + return TypeTagVector.load(deserializer, depth + 1); + case TypeTagVariants.Struct: + return TypeTagStruct.load(deserializer, depth + 1); + case TypeTagVariants.U16: + return TypeTagU16.load(deserializer); + case TypeTagVariants.U32: + return TypeTagU32.load(deserializer); + case TypeTagVariants.U256: + return TypeTagU256.load(deserializer); + case TypeTagVariants.I8: + return TypeTagI8.load(deserializer); + case TypeTagVariants.I16: + return TypeTagI16.load(deserializer); + case TypeTagVariants.I32: + return TypeTagI32.load(deserializer); + case TypeTagVariants.I64: + return TypeTagI64.load(deserializer); + case TypeTagVariants.I128: + return TypeTagI128.load(deserializer); + case TypeTagVariants.I256: + return TypeTagI256.load(deserializer); + case TypeTagVariants.Generic: + return TypeTagGeneric.load(deserializer); + default: + throw new Error(`Unknown variant index for TypeTag: ${index}`); + } + } + + /** Type guard: narrows to {@link TypeTagBool}. */ + isBool(): this is TypeTagBool { + return this instanceof TypeTagBool; + } + /** Type guard: narrows to {@link TypeTagAddress}. */ + isAddress(): this is TypeTagAddress { + return this instanceof TypeTagAddress; + } + /** Type guard: narrows to {@link TypeTagGeneric}. */ + isGeneric(): this is TypeTagGeneric { + return this instanceof TypeTagGeneric; + } + /** Type guard: narrows to {@link TypeTagSigner}. */ + isSigner(): this is TypeTagSigner { + return this instanceof TypeTagSigner; + } + /** Type guard: narrows to {@link TypeTagVector}. */ + isVector(): this is TypeTagVector { + return this instanceof TypeTagVector; + } + /** Type guard: narrows to {@link TypeTagStruct}. */ + isStruct(): this is TypeTagStruct { + return this instanceof TypeTagStruct; + } + /** Type guard: narrows to {@link TypeTagU8}. */ + isU8(): this is TypeTagU8 { + return this instanceof TypeTagU8; + } + /** Type guard: narrows to {@link TypeTagU16}. */ + isU16(): this is TypeTagU16 { + return this instanceof TypeTagU16; + } + /** Type guard: narrows to {@link TypeTagU32}. */ + isU32(): this is TypeTagU32 { + return this instanceof TypeTagU32; + } + /** Type guard: narrows to {@link TypeTagU64}. */ + isU64(): this is TypeTagU64 { + return this instanceof TypeTagU64; + } + /** Type guard: narrows to {@link TypeTagU128}. */ + isU128(): this is TypeTagU128 { + return this instanceof TypeTagU128; + } + /** Type guard: narrows to {@link TypeTagU256}. */ + isU256(): this is TypeTagU256 { + return this instanceof TypeTagU256; + } + /** Type guard: narrows to {@link TypeTagI8}. */ + isI8(): this is TypeTagI8 { + return this instanceof TypeTagI8; + } + /** Type guard: narrows to {@link TypeTagI16}. */ + isI16(): this is TypeTagI16 { + return this instanceof TypeTagI16; + } + /** Type guard: narrows to {@link TypeTagI32}. */ + isI32(): this is TypeTagI32 { + return this instanceof TypeTagI32; + } + /** Type guard: narrows to {@link TypeTagI64}. */ + isI64(): this is TypeTagI64 { + return this instanceof TypeTagI64; + } + /** Type guard: narrows to {@link TypeTagI128}. */ + isI128(): this is TypeTagI128 { + return this instanceof TypeTagI128; + } + /** Type guard: narrows to {@link TypeTagI256}. */ + isI256(): this is TypeTagI256 { + return this instanceof TypeTagI256; + } + + /** Returns true if this type tag represents a primitive Move type (numeric, bool, address, or signer). */ + isPrimitive(): boolean { + return ( + this instanceof TypeTagSigner || + this instanceof TypeTagAddress || + this instanceof TypeTagBool || + this instanceof TypeTagU8 || + this instanceof TypeTagU16 || + this instanceof TypeTagU32 || + this instanceof TypeTagU64 || + this instanceof TypeTagU128 || + this instanceof TypeTagU256 || + this instanceof TypeTagI8 || + this instanceof TypeTagI16 || + this instanceof TypeTagI32 || + this instanceof TypeTagI64 || + this instanceof TypeTagI128 || + this instanceof TypeTagI256 + ); + } +} + +// ── Primitive TypeTags ── + +/** Represents the Move `bool` type. */ +export class TypeTagBool extends TypeTag { + toString(): string { + return "bool"; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.Bool); + } + static load(_deserializer: Deserializer): TypeTagBool { + return new TypeTagBool(); + } +} + +/** Represents the Move `u8` type. */ +export class TypeTagU8 extends TypeTag { + toString(): string { + return "u8"; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.U8); + } + static load(_deserializer: Deserializer): TypeTagU8 { + return new TypeTagU8(); + } +} + +/** Represents the Move `i8` type. */ +export class TypeTagI8 extends TypeTag { + toString(): string { + return "i8"; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.I8); + } + static load(_deserializer: Deserializer): TypeTagI8 { + return new TypeTagI8(); + } +} + +/** Represents the Move `u16` type. */ +export class TypeTagU16 extends TypeTag { + toString(): string { + return "u16"; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.U16); + } + static load(_deserializer: Deserializer): TypeTagU16 { + return new TypeTagU16(); + } +} + +/** Represents the Move `i16` type. */ +export class TypeTagI16 extends TypeTag { + toString(): string { + return "i16"; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.I16); + } + static load(_deserializer: Deserializer): TypeTagI16 { + return new TypeTagI16(); + } +} + +/** Represents the Move `u32` type. */ +export class TypeTagU32 extends TypeTag { + toString(): string { + return "u32"; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.U32); + } + static load(_deserializer: Deserializer): TypeTagU32 { + return new TypeTagU32(); + } +} + +/** Represents the Move `i32` type. */ +export class TypeTagI32 extends TypeTag { + toString(): string { + return "i32"; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.I32); + } + static load(_deserializer: Deserializer): TypeTagI32 { + return new TypeTagI32(); + } +} + +/** Represents the Move `u64` type. */ +export class TypeTagU64 extends TypeTag { + toString(): string { + return "u64"; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.U64); + } + static load(_deserializer: Deserializer): TypeTagU64 { + return new TypeTagU64(); + } +} + +/** Represents the Move `i64` type. */ +export class TypeTagI64 extends TypeTag { + toString(): string { + return "i64"; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.I64); + } + static load(_deserializer: Deserializer): TypeTagI64 { + return new TypeTagI64(); + } +} + +/** Represents the Move `u128` type. */ +export class TypeTagU128 extends TypeTag { + toString(): string { + return "u128"; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.U128); + } + static load(_deserializer: Deserializer): TypeTagU128 { + return new TypeTagU128(); + } +} + +/** Represents the Move `i128` type. */ +export class TypeTagI128 extends TypeTag { + toString(): string { + return "i128"; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.I128); + } + static load(_deserializer: Deserializer): TypeTagI128 { + return new TypeTagI128(); + } +} + +/** Represents the Move `u256` type. */ +export class TypeTagU256 extends TypeTag { + toString(): string { + return "u256"; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.U256); + } + static load(_deserializer: Deserializer): TypeTagU256 { + return new TypeTagU256(); + } +} + +/** Represents the Move `i256` type. */ +export class TypeTagI256 extends TypeTag { + toString(): string { + return "i256"; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.I256); + } + static load(_deserializer: Deserializer): TypeTagI256 { + return new TypeTagI256(); + } +} + +/** Represents the Move `address` type. */ +export class TypeTagAddress extends TypeTag { + toString(): string { + return "address"; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.Address); + } + static load(_deserializer: Deserializer): TypeTagAddress { + return new TypeTagAddress(); + } +} + +/** Represents the Move `signer` type. */ +export class TypeTagSigner extends TypeTag { + toString(): string { + return "signer"; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.Signer); + } + static load(_deserializer: Deserializer): TypeTagSigner { + return new TypeTagSigner(); + } +} + +/** Represents a Move reference type (`&T`). Used internally; not valid in transaction arguments. */ +export class TypeTagReference extends TypeTag { + constructor(public readonly value: TypeTag) { + super(); + } + toString(): `&${string}` { + return `&${this.value.toString()}`; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.Reference); + } + static load(deserializer: Deserializer, depth = 0): TypeTagReference { + const value = TypeTag.deserialize(deserializer, depth); + return new TypeTagReference(value); + } +} + +/** + * Represents a generic type parameter placeholder (e.g., T0, T1). + * Used when building type tags for functions with generic type parameters. + */ +export class TypeTagGeneric extends TypeTag { + /** + * @param value - The zero-based index of the generic type parameter. + * @throws If the index is negative. + */ + constructor(public readonly value: number) { + super(); + if (value < 0) throw new Error("Generic type parameter index cannot be negative"); + } + toString(): `T${number}` { + return `T${this.value}`; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.Generic); + serializer.serializeU32(this.value); + } + static load(deserializer: Deserializer): TypeTagGeneric { + const value = deserializer.deserializeU32(); + return new TypeTagGeneric(value); + } +} + +// ── Composite TypeTags ── + +/** Represents the Move `vector` type, parameterized by an inner element type tag. */ +export class TypeTagVector extends TypeTag { + /** + * @param value - The type tag of the vector's element type. + */ + constructor(public readonly value: TypeTag) { + super(); + } + toString(): `vector<${string}>` { + return `vector<${this.value.toString()}>`; + } + /** Convenience factory for `vector`, commonly used for byte strings. */ + static readonly VECTOR_U8: TypeTagVector = new TypeTagVector(new TypeTagU8()); + static u8(): TypeTagVector { + return TypeTagVector.VECTOR_U8; + } + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.Vector); + this.value.serialize(serializer); + } + static load(deserializer: Deserializer, depth = 0): TypeTagVector { + const value = TypeTag.deserialize(deserializer, depth); + return new TypeTagVector(value); + } +} + +/** + * Represents a Move struct type (e.g., `0x1::aptos_coin::AptosCoin`). + * Wraps a {@link StructTag} that holds the address, module, name, and type arguments. + */ +export class TypeTagStruct extends TypeTag { + /** + * @param value - The StructTag describing the struct's fully qualified name and type arguments. + */ + constructor(public readonly value: StructTag) { + super(); + } + + toString(): `0x${string}::${string}::${string}` { + let typePredicate = ""; + if (this.value.typeArgs.length > 0) { + typePredicate = `<${this.value.typeArgs.map((typeArg) => typeArg.toString()).join(", ")}>`; + } + return `${this.value.address.toString()}::${this.value.moduleName.identifier}::${this.value.name.identifier}${typePredicate}`; + } + + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TypeTagVariants.Struct); + this.value.serialize(serializer); + } + + static load(deserializer: Deserializer, depth = 0): TypeTagStruct { + const value = StructTag.deserialize(deserializer, depth); + return new TypeTagStruct(value); + } + + /** + * Checks whether this struct type tag matches the given address, module, and struct name. + * @param address - The module's account address. + * @param moduleName - The module name. + * @param structName - The struct name. + * @returns True if all three components match. + */ + isTypeTag(address: AccountAddress, moduleName: string, structName: string): boolean { + return ( + this.value.moduleName.identifier === moduleName && + this.value.name.identifier === structName && + this.value.address.equals(address) + ); + } + + /** Returns true if this is the `0x1::string::String` type. */ + isString(): boolean { + return this.isTypeTag(AccountAddress.ONE, "string", "String"); + } + /** Returns true if this is the `0x1::option::Option` type. */ + isOption(): boolean { + return this.isTypeTag(AccountAddress.ONE, "option", "Option"); + } + /** Returns true if this is the `0x1::object::Object` type. */ + isObject(): boolean { + return this.isTypeTag(AccountAddress.ONE, "object", "Object"); + } + /** Returns true if this is the `0x1::permissioned_delegation::DelegationKey` type. */ + isDelegationKey(): boolean { + return this.isTypeTag(AccountAddress.ONE, "permissioned_delegation", "DelegationKey"); + } + /** Returns true if this is the `0x1::rate_limiter::RateLimiter` type. */ + isRateLimiter(): boolean { + return this.isTypeTag(AccountAddress.ONE, "rate_limiter", "RateLimiter"); + } +} + +// ── StructTag ── + +/** + * Represents a fully qualified Move struct type: `address::module::StructName`. + * + * Used inside {@link TypeTagStruct} and for building struct type references + * in transaction payloads. + */ +export class StructTag extends Serializable { + /** The account address where the module is published. */ + public readonly address: AccountAddress; + /** The module name containing the struct. */ + public readonly moduleName: Identifier; + /** The struct name. */ + public readonly name: Identifier; + /** The generic type arguments applied to the struct (empty if none). */ + public readonly typeArgs: Array; + + /** + * @param address - The account address of the module. + * @param moduleName - The module name as an Identifier. + * @param name - The struct name as an Identifier. + * @param typeArgs - The type arguments applied to the struct. + */ + constructor(address: AccountAddress, moduleName: Identifier, name: Identifier, typeArgs: Array) { + super(); + this.address = address; + this.moduleName = moduleName; + this.name = name; + this.typeArgs = typeArgs; + } + + serialize(serializer: Serializer): void { + serializer.serialize(this.address); + serializer.serialize(this.moduleName); + serializer.serialize(this.name); + serializer.serializeVector(this.typeArgs); + } + + static deserialize(deserializer: Deserializer, depth = 0): StructTag { + const address = AccountAddress.deserialize(deserializer); + const moduleName = Identifier.deserialize(deserializer); + const name = Identifier.deserialize(deserializer); + const length = deserializer.deserializeUleb128AsU32(); + if (length > 32) { + throw new Error(`StructTag typeArgs count ${length} exceeds maximum 32`); + } + const typeArgs: TypeTag[] = []; + for (let i = 0; i < length; i += 1) { + typeArgs.push(TypeTag.deserialize(deserializer, depth + 1)); + } + return new StructTag(address, moduleName, name, typeArgs); + } +} + +// ── Factory helpers ── + +const APTOS_COIN_STRUCT_TAG = new StructTag( + AccountAddress.ONE, + new Identifier("aptos_coin"), + new Identifier("AptosCoin"), + [], +); + +const STRING_STRUCT_TAG = new StructTag(AccountAddress.ONE, new Identifier("string"), new Identifier("String"), []); + +/** + * Creates a StructTag for `0x1::aptos_coin::AptosCoin`. + * @returns The StructTag for the native Aptos coin. + */ +export function aptosCoinStructTag(): StructTag { + return APTOS_COIN_STRUCT_TAG; +} + +/** + * Creates a StructTag for `0x1::string::String`. + * @returns The StructTag for the Move String type. + */ +export function stringStructTag(): StructTag { + return STRING_STRUCT_TAG; +} + +/** + * Creates a StructTag for `0x1::option::Option`. + * @param typeArg - The type tag for the Option's inner type. + * @returns The StructTag for Option parameterized by the given type. + */ +export function optionStructTag(typeArg: TypeTag): StructTag { + return new StructTag(AccountAddress.ONE, new Identifier("option"), new Identifier("Option"), [typeArg]); +} + +/** + * Creates a StructTag for `0x1::object::Object`. + * @param typeArg - The type tag for the Object's inner type. + * @returns The StructTag for Object parameterized by the given type. + */ +export function objectStructTag(typeArg: TypeTag): StructTag { + return new StructTag(AccountAddress.ONE, new Identifier("object"), new Identifier("Object"), [typeArg]); +} diff --git a/v10/src/crypto/abstraction.ts b/v10/src/crypto/abstraction.ts new file mode 100644 index 000000000..746507d37 --- /dev/null +++ b/v10/src/crypto/abstraction.ts @@ -0,0 +1,109 @@ +import type { Deserializer } from "../bcs/deserializer.js"; +import type { Serializer } from "../bcs/serializer.js"; +import { Hex, type HexInput } from "../hex/index.js"; +import { AccountPublicKey, type VerifySignatureArgs } from "./public-key.js"; +import { Signature } from "./signature.js"; + +/** + * A generic signature container used for account abstraction. + * + * `AbstractSignature` stores arbitrary bytes that represent a signature + * produced by an abstracted account (e.g. a smart-contract account that + * implements its own verification logic). + * + * @example + * ```ts + * const sig = new AbstractSignature(signatureBytes); + * ``` + */ +export class AbstractSignature extends Signature { + /** The raw signature bytes. */ + readonly value: Uint8Array; + + /** + * Creates an `AbstractSignature` from raw bytes or a hex string. + * + * @param value - The signature data as a hex string or `Uint8Array`. + */ + constructor(value: HexInput) { + super(); + this.value = Hex.fromHexInput(value).toUint8Array(); + } + + /** + * BCS-serialises the signature by writing its bytes with a length prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.value); + } + + /** + * Deserialises an `AbstractSignature` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `AbstractSignature`. + */ + static deserialize(deserializer: Deserializer): AbstractSignature { + return new AbstractSignature(deserializer.deserializeBytes()); + } +} + +/** + * A placeholder public key for account abstraction (smart-contract accounts). + * + * `AbstractPublicKey` stores the account address rather than a traditional + * cryptographic public key, because abstracted accounts authenticate via + * on-chain Move logic rather than a fixed signing algorithm. + * + * @example + * ```ts + * const pubKey = new AbstractPublicKey(accountAddress); + * ``` + */ +export class AbstractPublicKey extends AccountPublicKey { + /** The account address — typed as `unknown` because AccountAddress is in core (L2). */ + readonly accountAddress: unknown; + + /** + * Creates an `AbstractPublicKey` from an account address. + * + * @param accountAddress - The `AccountAddress` associated with this abstract + * account. Typed as `unknown` to avoid a circular dependency with the + * `core` package. + */ + constructor(accountAddress: unknown) { + super(); + this.accountAddress = accountAddress; + } + + /** + * Not implemented for abstracted accounts. + * + * @throws Always throws indicating that the `core` module must be ported first. + */ + authKey(): unknown { + throw new Error("authKey() not yet available; port the core module first"); + } + + /** + * Not implemented for abstracted accounts. + * + * Abstracted accounts perform signature verification on-chain in Move. + * + * @throws Always throws. + */ + verifySignature(_args: VerifySignatureArgs): boolean { + throw new Error("This function is not implemented for AbstractPublicKey."); + } + + /** + * Not implemented for abstracted accounts. + * + * @throws Always throws. + */ + serialize(_serializer: Serializer): void { + throw new Error("This function is not implemented for AbstractPublicKey."); + } +} diff --git a/v10/src/crypto/deserialization-utils.ts b/v10/src/crypto/deserialization-utils.ts new file mode 100644 index 000000000..7aa8f751e --- /dev/null +++ b/v10/src/crypto/deserialization-utils.ts @@ -0,0 +1,137 @@ +import { Deserializer } from "../bcs/deserializer.js"; +import type { HexInput } from "../hex/index.js"; +import { Ed25519PublicKey, Ed25519Signature } from "./ed25519.js"; +import { FederatedKeylessPublicKey } from "./federated-keyless.js"; +import { KeylessPublicKey, KeylessSignature } from "./keyless.js"; +import { MultiEd25519PublicKey, MultiEd25519Signature } from "./multi-ed25519.js"; +import { MultiKey, MultiKeySignature } from "./multi-key.js"; +import type { PublicKey } from "./public-key.js"; +import { Secp256k1PublicKey, Secp256k1Signature } from "./secp256k1.js"; +import type { Signature } from "./signature.js"; +import { AnyPublicKey, AnySignature } from "./single-key.js"; + +const MULTIPLE_DESERIALIZATIONS_ERROR_MSG = "Multiple possible deserializations found"; +const MAX_ERROR_INPUT_LENGTH = 128; +function truncateForError(input: HexInput): string { + const str = typeof input === "string" ? input : `Uint8Array(${input.length})`; + return str.length > MAX_ERROR_INPUT_LENGTH ? `${str.slice(0, MAX_ERROR_INPUT_LENGTH)}...` : str; +} + +/** + * Attempts to deserialise a `PublicKey` from hex-encoded BCS bytes by trying + * each known public-key type in order. + * + * The function succeeds if exactly one type deserialises the bytes without + * error and consumes all input. If no type matches, or if more than one type + * matches, an error is thrown. + * + * Supported types (tried in order): + * `Ed25519PublicKey`, `AnyPublicKey`, `MultiEd25519PublicKey`, `MultiKey`, + * `KeylessPublicKey`, `FederatedKeylessPublicKey`, `Secp256k1PublicKey`. + * + * @param publicKey - The BCS-encoded public key as a hex string or `Uint8Array`. + * @returns The deserialised {@link PublicKey}. + * @throws If the bytes match no known public-key type. + * @throws If the bytes match more than one public-key type (ambiguous). + * + * @example + * ```ts + * const key = deserializePublicKey("0x..."); + * ``` + */ +export function deserializePublicKey(publicKey: HexInput): PublicKey { + const publicKeyTypes = [ + Ed25519PublicKey, + AnyPublicKey, + MultiEd25519PublicKey, + MultiKey, + KeylessPublicKey, + FederatedKeylessPublicKey, + Secp256k1PublicKey, + ]; + + let result: PublicKey | undefined; + for (const KeyType of publicKeyTypes) { + try { + const deserializer = Deserializer.fromHex(publicKey); + const key = KeyType.deserialize(deserializer); + deserializer.assertFinished(); + if (result) { + throw new Error(`${MULTIPLE_DESERIALIZATIONS_ERROR_MSG}: ${truncateForError(publicKey)}`); + } + result = key; + } catch (error) { + if (error instanceof Error && error.message.includes(MULTIPLE_DESERIALIZATIONS_ERROR_MSG)) { + throw error; + } + if (!(error instanceof Error) || error instanceof TypeError || error instanceof RangeError) { + throw error; + } + } + } + + if (!result) { + throw new Error(`Failed to deserialize public key: ${truncateForError(publicKey)}`); + } + + return result; +} + +/** + * Attempts to deserialise a `Signature` from hex-encoded BCS bytes by trying + * each known signature type in order. + * + * The function succeeds if exactly one type deserialises the bytes without + * error and consumes all input. If no type matches, or if more than one type + * matches, an error is thrown. + * + * Supported types (tried in order): + * `Ed25519Signature`, `AnySignature`, `MultiEd25519Signature`, + * `MultiKeySignature`, `KeylessSignature`, `Secp256k1Signature`. + * + * @param signature - The BCS-encoded signature as a hex string or `Uint8Array`. + * @returns The deserialised {@link Signature}. + * @throws If the bytes match no known signature type. + * @throws If the bytes match more than one signature type (ambiguous). + * + * @example + * ```ts + * const sig = deserializeSignature("0x..."); + * ``` + */ +export function deserializeSignature(signature: HexInput): Signature { + const signatureTypes = [ + Ed25519Signature, + AnySignature, + MultiEd25519Signature, + MultiKeySignature, + KeylessSignature, + Secp256k1Signature, + ]; + + let result: Signature | undefined; + for (const SignatureType of signatureTypes) { + try { + const deserializer = Deserializer.fromHex(signature); + const sig = SignatureType.deserialize(deserializer); + deserializer.assertFinished(); + if (result) { + throw new Error(`${MULTIPLE_DESERIALIZATIONS_ERROR_MSG}: ${truncateForError(signature)}`); + } + result = sig; + } catch (error) { + if (error instanceof Error && error.message.includes(MULTIPLE_DESERIALIZATIONS_ERROR_MSG)) { + throw error; + } + if (!(error instanceof Error) || error instanceof TypeError || error instanceof RangeError) { + throw error; + } + } + } + + if (!result) { + throw new Error(`Failed to deserialize signature: ${truncateForError(signature)}`); + } + + return result; +} diff --git a/v10/src/crypto/ed25519.ts b/v10/src/crypto/ed25519.ts new file mode 100644 index 000000000..727e5a309 --- /dev/null +++ b/v10/src/crypto/ed25519.ts @@ -0,0 +1,426 @@ +import { ed25519 } from "@noble/curves/ed25519.js"; +import type { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; +import type { HexInput } from "../hex/index.js"; +import { Hex } from "../hex/index.js"; +import { CKDPriv, deriveKey, HARDENED_OFFSET, isValidHardenedPath, mnemonicToSeed, splitPath } from "./hd-key.js"; +import { type PrivateKey, PrivateKeyUtils } from "./private-key.js"; +import { AccountPublicKey, createAuthKey, type VerifySignatureArgs } from "./public-key.js"; +import { Signature } from "./signature.js"; +import { PrivateKeyVariants, SigningScheme } from "./types.js"; +import { convertSigningMessage } from "./utils.js"; + +const L: number[] = [ + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, +]; + +/** + * Checks whether an Ed25519 signature is canonical (i.e. the scalar `s` is + * reduced modulo the group order `L`). + * + * Non-canonical signatures are rejected by the Aptos chain and should be + * rejected before submitting a transaction. + * + * @param signature - The {@link Signature} to check. + * @returns `true` if the signature is canonical, `false` otherwise. + */ +export function isCanonicalEd25519Signature(signature: Signature): boolean { + const sigBytes = signature.toUint8Array(); + for (let i = L.length - 1; i >= 0; i -= 1) { + if (sigBytes[i + 32] < L[i]) return true; + if (sigBytes[i + 32] > L[i]) return false; + } + return false; +} + +/** + * Represents an Ed25519 public key. + * + * Ed25519 is the default signing scheme for Aptos accounts and supports fast + * signature verification. + * + * @example + * ```ts + * const privateKey = Ed25519PrivateKey.generate(); + * const publicKey = privateKey.publicKey(); + * const isValid = publicKey.verifySignature({ message: "0xdeadbeef", signature }); + * ``` + */ +export class Ed25519PublicKey extends AccountPublicKey { + /** The expected byte length of an Ed25519 public key. */ + static readonly LENGTH: number = 32; + + private readonly key: Hex; + + /** + * Creates an `Ed25519PublicKey` from raw bytes or a hex string. + * + * @param hexInput - A 32-byte public key as a hex string or `Uint8Array`. + * @throws If the input is not exactly 32 bytes. + */ + constructor(hexInput: HexInput) { + super(); + const hex = Hex.fromHexInput(hexInput); + if (hex.toUint8Array().length !== Ed25519PublicKey.LENGTH) { + throw new Error(`PublicKey length should be ${Ed25519PublicKey.LENGTH}`); + } + this.key = hex; + } + + /** + * Verifies that `signature` was produced by signing `message` with the + * corresponding private key. + * + * Non-canonical signatures are rejected before verification. + * + * @param args - Object containing `message` and `signature`. + * @returns `true` if the signature is valid, `false` otherwise. + */ + verifySignature(args: VerifySignatureArgs): boolean { + const { message, signature } = args; + if (!isCanonicalEd25519Signature(signature)) return false; + const messageToVerify = convertSigningMessage(message); + const messageBytes = Hex.fromHexInput(messageToVerify).toUint8Array(); + const signatureBytes = signature.toUint8Array(); + const publicKeyBytes = this.key.toUint8Array(); + return ed25519.verify(signatureBytes, messageBytes, publicKeyBytes); + } + + /** + * Derives the Aptos authentication key for this public key using the + * Ed25519 signing scheme. + * + * @returns The `AccountAddress` that represents the authentication key. + */ + authKey(): unknown { + return createAuthKey(SigningScheme.Ed25519, this.toUint8Array()); + } + + /** + * Returns the raw 32-byte public key. + * + * @returns The public key as a `Uint8Array`. + */ + toUint8Array(): Uint8Array { + return this.key.toUint8Array(); + } + + /** + * BCS-serialises the public key by writing its bytes with a length prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.key.toUint8Array()); + } + + /** + * Deserialises an `Ed25519PublicKey` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `Ed25519PublicKey`. + */ + static deserialize(deserializer: Deserializer): Ed25519PublicKey { + const bytes = deserializer.deserializeBytes(); + return new Ed25519PublicKey(bytes); + } +} + +/** + * Represents an Ed25519 private key. + * + * Supports key generation, BIP-39/SLIP-0010 derivation, signing, and secure + * memory clearing. Private keys can be serialised as AIP-80 strings (e.g. + * `"ed25519-priv-0x..."`). + * + * @example + * ```ts + * const privateKey = Ed25519PrivateKey.generate(); + * const signature = privateKey.sign("0xdeadbeef"); + * privateKey.clear(); // wipe key material from memory + * ``` + */ +export class Ed25519PrivateKey extends Serializable implements PrivateKey { + /** The expected byte length of an Ed25519 private key. */ + static readonly LENGTH: number = 32; + /** SLIP-0010 seed string for Ed25519 key derivation. */ + static readonly SLIP_0010_SEED = "ed25519 seed"; + + private signingKey: Hex; + private cleared: boolean = false; + + /** + * Creates an `Ed25519PrivateKey` from raw bytes or a hex/AIP-80 string. + * + * @param hexInput - A 32-byte private key as raw bytes, a hex string, or an + * AIP-80 string (`"ed25519-priv-0x..."`). + * @param strict - When `true`, the input must be AIP-80 formatted. + * Defaults to `false`. + * @throws If the decoded key is not exactly 32 bytes. + */ + constructor(hexInput: HexInput, strict?: boolean) { + super(); + const privateKeyHex = PrivateKeyUtils.parseHexInput(hexInput, PrivateKeyVariants.Ed25519, strict); + if (privateKeyHex.toUint8Array().length !== Ed25519PrivateKey.LENGTH) { + throw new Error(`PrivateKey length should be ${Ed25519PrivateKey.LENGTH}`); + } + this.signingKey = privateKeyHex; + } + + /** + * Generates a random Ed25519 private key. + * + * @returns A new randomly generated `Ed25519PrivateKey`. + * + * @example + * ```ts + * const key = Ed25519PrivateKey.generate(); + * ``` + */ + static generate(): Ed25519PrivateKey { + const keyPair = ed25519.utils.randomSecretKey(); + return new Ed25519PrivateKey(keyPair, false); + } + + /** + * Derives an Ed25519 private key from a BIP-39 mnemonic and a SLIP-0010 + * hardened derivation path. + * + * @param path - A hardened derivation path, e.g. `"m/44'/637'/0'/0'/0'"`. + * @param mnemonics - A BIP-39 mnemonic phrase. + * @returns The derived `Ed25519PrivateKey`. + * @throws If the derivation path is not a valid hardened path. + * + * @example + * ```ts + * const key = Ed25519PrivateKey.fromDerivationPath( + * "m/44'/637'/0'/0'/0'", + * "your twelve word mnemonic phrase here ...", + * ); + * ``` + */ + static fromDerivationPath(path: string, mnemonics: string): Ed25519PrivateKey { + if (!isValidHardenedPath(path)) { + throw new Error(`Invalid derivation path ${path}`); + } + return Ed25519PrivateKey.fromDerivationPathInner(path, mnemonicToSeed(mnemonics)); + } + + private static fromDerivationPathInner(path: string, seed: Uint8Array, offset = HARDENED_OFFSET): Ed25519PrivateKey { + const segments = splitPath(path).map((el) => { + const n = parseInt(el, 10); + if (!Number.isInteger(n) || n < 0 || n >= HARDENED_OFFSET) { + throw new Error(`Invalid derivation path segment: ${el} (must be 0–2147483647)`); + } + return n; + }); + let current = deriveKey(Ed25519PrivateKey.SLIP_0010_SEED, seed); + for (const segment of segments) { + const next = CKDPriv(current, segment + offset); + current.key.fill(0); + current.chainCode.fill(0); + current = next; + } + current.chainCode.fill(0); + try { + return new Ed25519PrivateKey(current.key, false); + } catch (err) { + current.key.fill(0); + throw err; + } + } + + private ensureNotCleared(): void { + if (this.cleared) { + throw new Error("Private key has been cleared from memory and can no longer be used"); + } + } + + /** + * Overwrites the private key material in memory with random and zero bytes, + * then marks the key as cleared. + * + * After calling this method any further use of the key will throw. + * + * **Note:** If the key was constructed from a hex string, the original string + * cannot be zeroed (JavaScript strings are immutable). For maximum security, + * construct keys from `Uint8Array` sources when possible and ensure the + * original string reference is not retained. + */ + clear(): void { + if (!this.cleared) { + const keyBytes = this.signingKey.toUint8Array(); + crypto.getRandomValues(keyBytes); + keyBytes.fill(0xff); + crypto.getRandomValues(keyBytes); + keyBytes.fill(0); + this.cleared = true; + } + } + + /** + * Returns whether the key has been cleared from memory. + * + * @returns `true` if {@link clear} has been called, `false` otherwise. + */ + isCleared(): boolean { + return this.cleared; + } + + /** + * Derives and returns the Ed25519 public key corresponding to this private key. + * + * @returns The associated {@link Ed25519PublicKey}. + * @throws If the key has been cleared. + */ + publicKey(): Ed25519PublicKey { + this.ensureNotCleared(); + const bytes = ed25519.getPublicKey(this.signingKey.toUint8Array()); + return new Ed25519PublicKey(bytes); + } + + /** + * Signs a message and returns an {@link Ed25519Signature}. + * + * The message is normalised via {@link convertSigningMessage} before signing + * so that plain-text strings are UTF-8 encoded automatically. + * + * @param message - The message to sign, as raw bytes, a hex string, or a + * plain UTF-8 string. + * @returns The resulting {@link Ed25519Signature}. + * @throws If the key has been cleared. + */ + sign(message: HexInput): Ed25519Signature { + this.ensureNotCleared(); + const messageToSign = convertSigningMessage(message); + const messageBytes = Hex.fromHexInput(messageToSign).toUint8Array(); + const signatureBytes = ed25519.sign(messageBytes, this.signingKey.toUint8Array()); + return new Ed25519Signature(signatureBytes); + } + + /** + * Returns the raw 32-byte private key material. + * + * @returns The private key as a `Uint8Array`. + * @throws If the key has been cleared. + */ + toUint8Array(): Uint8Array { + this.ensureNotCleared(); + return this.signingKey.toUint8Array(); + } + + /** + * Returns the private key as an AIP-80 formatted string (equivalent to + * {@link toAIP80String}). + * + * @returns An AIP-80 string, e.g. `"ed25519-priv-0x..."`. + * @throws If the key has been cleared. + */ + toString(): string { + this.ensureNotCleared(); + return this.toAIP80String(); + } + + /** + * Returns the private key as a plain hex string (without AIP-80 prefix). + * + * @returns A `0x`-prefixed hex string. + * @throws If the key has been cleared. + */ + toHexString(): string { + this.ensureNotCleared(); + return this.signingKey.toString(); + } + + /** + * Returns the private key as an AIP-80 compliant string. + * + * @returns A string of the form `"ed25519-priv-0x"`. + * @throws If the key has been cleared. + */ + toAIP80String(): string { + this.ensureNotCleared(); + return PrivateKeyUtils.formatPrivateKey(this.signingKey.toString(), PrivateKeyVariants.Ed25519); + } + + /** + * BCS-serialises the private key by writing its bytes with a length prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.toUint8Array()); + } + + /** + * Deserialises an `Ed25519PrivateKey` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `Ed25519PrivateKey`. + */ + static deserialize(deserializer: Deserializer): Ed25519PrivateKey { + const bytes = deserializer.deserializeBytes(); + return new Ed25519PrivateKey(bytes, false); + } +} + +/** + * Represents an Ed25519 signature (64 bytes). + * + * @example + * ```ts + * const sig = privateKey.sign("0xdeadbeef"); + * console.log(sig.toString()); // "0x..." + * ``` + */ +export class Ed25519Signature extends Signature { + /** The expected byte length of an Ed25519 signature. */ + static readonly LENGTH = 64; + + private readonly data: Hex; + + /** + * Creates an `Ed25519Signature` from raw bytes or a hex string. + * + * @param hexInput - A 64-byte signature as a hex string or `Uint8Array`. + * @throws If the input is not exactly 64 bytes. + */ + constructor(hexInput: HexInput) { + super(); + const data = Hex.fromHexInput(hexInput); + if (data.toUint8Array().length !== Ed25519Signature.LENGTH) { + throw new Error(`Signature length should be ${Ed25519Signature.LENGTH}`); + } + this.data = data; + } + + /** + * Returns the raw 64-byte signature. + * + * @returns The signature as a `Uint8Array`. + */ + toUint8Array(): Uint8Array { + return this.data.toUint8Array(); + } + + /** + * BCS-serialises the signature by writing its bytes with a length prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.data.toUint8Array()); + } + + /** + * Deserialises an `Ed25519Signature` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `Ed25519Signature`. + */ + static deserialize(deserializer: Deserializer): Ed25519Signature { + const bytes = deserializer.deserializeBytes(); + return new Ed25519Signature(bytes); + } +} diff --git a/v10/src/crypto/ephemeral.ts b/v10/src/crypto/ephemeral.ts new file mode 100644 index 000000000..ec233dd59 --- /dev/null +++ b/v10/src/crypto/ephemeral.ts @@ -0,0 +1,172 @@ +import { Deserializer } from "../bcs/deserializer.js"; +import type { Serializer } from "../bcs/serializer.js"; +import type { HexInput } from "../hex/index.js"; +import { Hex } from "../hex/index.js"; +import { Ed25519PublicKey, Ed25519Signature } from "./ed25519.js"; +import { PublicKey, type VerifySignatureArgs } from "./public-key.js"; +import { Signature } from "./signature.js"; +import { EphemeralPublicKeyVariant, EphemeralSignatureVariant } from "./types.js"; + +/** + * A short-lived public key used during Keyless authentication. + * + * An `EphemeralPublicKey` wraps a concrete {@link PublicKey} (currently only + * Ed25519 is supported) and tags it with a variant discriminant so that it can + * be BCS-serialised as part of a {@link KeylessSignature}. + * + * @example + * ```ts + * const epk = new EphemeralPublicKey(new Ed25519PublicKey(ephemeralKeyBytes)); + * ``` + */ +export class EphemeralPublicKey extends PublicKey { + /** The underlying concrete public key. */ + public readonly publicKey: PublicKey; + /** The variant discriminant identifying the key algorithm. */ + public readonly variant: EphemeralPublicKeyVariant; + + /** + * Creates an `EphemeralPublicKey` wrapping the given public key. + * + * @param publicKey - The concrete public key to wrap. Currently only + * {@link Ed25519PublicKey} is supported. + * @throws If the provided public key type is not supported. + */ + constructor(publicKey: PublicKey) { + super(); + if (publicKey instanceof Ed25519PublicKey) { + this.publicKey = publicKey; + this.variant = EphemeralPublicKeyVariant.Ed25519; + } else { + throw new Error(`Unsupported key for EphemeralPublicKey - ${publicKey.constructor.name}`); + } + } + + /** + * Verifies that `signature` was produced by the corresponding ephemeral + * private key signing `message`. + * + * If `signature` is an {@link EphemeralSignature} the inner signature is + * unwrapped before verification. + * + * @param args - Object containing `message` and `signature`. + * @returns `true` if the signature is valid, `false` otherwise. + */ + verifySignature(args: VerifySignatureArgs): boolean { + const { message, signature } = args; + if (signature instanceof EphemeralSignature) { + return this.publicKey.verifySignature({ message, signature: signature.signature }); + } + return this.publicKey.verifySignature({ message, signature }); + } + + /** + * BCS-serialises the ephemeral public key, writing a ULEB128 variant tag + * followed by the serialised inner public key. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + if (this.publicKey instanceof Ed25519PublicKey) { + serializer.serializeU32AsUleb128(EphemeralPublicKeyVariant.Ed25519); + this.publicKey.serialize(serializer); + } else { + throw new Error("Unknown public key type"); + } + } + + /** + * Deserialises an `EphemeralPublicKey` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `EphemeralPublicKey`. + * @throws If the variant index is not recognised. + */ + static deserialize(deserializer: Deserializer): EphemeralPublicKey { + const index = deserializer.deserializeUleb128AsU32(); + switch (index) { + case EphemeralPublicKeyVariant.Ed25519: + return new EphemeralPublicKey(Ed25519PublicKey.deserialize(deserializer)); + default: + throw new Error(`Unknown variant index for EphemeralPublicKey: ${index}`); + } + } +} + +/** + * A short-lived signature used during Keyless authentication. + * + * An `EphemeralSignature` wraps a concrete {@link Signature} (currently only + * Ed25519 is supported) and tags it with a variant discriminant for BCS + * serialisation inside a {@link KeylessSignature}. + * + * @example + * ```ts + * const ephemeralSig = new EphemeralSignature(ed25519PrivKey.sign(message)); + * ``` + */ +export class EphemeralSignature extends Signature { + /** The underlying concrete signature. */ + public readonly signature: Signature; + + /** + * Creates an `EphemeralSignature` wrapping the given signature. + * + * @param signature - The concrete signature to wrap. Currently only + * {@link Ed25519Signature} is supported. + * @throws If the provided signature type is not supported. + */ + constructor(signature: Signature) { + super(); + if (signature instanceof Ed25519Signature) { + this.signature = signature; + } else { + throw new Error(`Unsupported signature for EphemeralSignature - ${signature.constructor.name}`); + } + } + + /** + * Constructs an `EphemeralSignature` by deserialising BCS bytes supplied as + * a hex string or `Uint8Array`. + * + * @param hexInput - BCS-encoded `EphemeralSignature` bytes. + * @returns A new `EphemeralSignature`. + */ + static fromHex(hexInput: HexInput): EphemeralSignature { + const data = Hex.fromHexInput(hexInput); + const deserializer = new Deserializer(data.toUint8Array()); + return EphemeralSignature.deserialize(deserializer); + } + + /** + * BCS-serialises the ephemeral signature, writing a ULEB128 variant tag + * followed by the serialised inner signature. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + if (this.signature instanceof Ed25519Signature) { + serializer.serializeU32AsUleb128(EphemeralSignatureVariant.Ed25519); + this.signature.serialize(serializer); + } else { + throw new Error("Unknown signature type"); + } + } + + /** + * Deserialises an `EphemeralSignature` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `EphemeralSignature`. + * @throws If the variant index is not recognised. + */ + static deserialize(deserializer: Deserializer): EphemeralSignature { + const index = deserializer.deserializeUleb128AsU32(); + switch (index) { + case EphemeralSignatureVariant.Ed25519: + return new EphemeralSignature(Ed25519Signature.deserialize(deserializer)); + default: + throw new Error(`Unknown variant index for EphemeralSignature: ${index}`); + } + } +} diff --git a/v10/src/crypto/federated-keyless.ts b/v10/src/crypto/federated-keyless.ts new file mode 100644 index 000000000..d72cde2ec --- /dev/null +++ b/v10/src/crypto/federated-keyless.ts @@ -0,0 +1,172 @@ +import type { Deserializer } from "../bcs/deserializer.js"; +import type { Serializer } from "../bcs/serializer.js"; +import type { HexInput } from "../hex/index.js"; +import { KeylessPublicKey } from "./keyless.js"; +import { AccountPublicKey, type PublicKey, type VerifySignatureArgs } from "./public-key.js"; + +/** Lazy-loaded interface for AccountAddress to break circular deps between crypto and core. */ +interface LazyAccountAddressClass { + from(input: unknown): { serialize(s: Serializer): void }; + deserialize(d: Deserializer): unknown; +} + +// Forward-declared: AccountAddress lives in core (L2), so we use lazy registration. +// registerAccountAddressForKeyless() is called from core/index.ts during module init. +let _AccountAddress: LazyAccountAddressClass | undefined; + +/** + * Registers the `AccountAddress` class with the federated-keyless module. + * + * This function is called once during module initialisation by the `core` + * package so that `FederatedKeylessPublicKey` can serialise and deserialise + * the `jwkAddress` field without a circular dependency between `crypto` and + * `core`. + * + * @param accountAddress - The `AccountAddress` class (or compatible + * constructor) from the `core` package. + */ +export function registerAccountAddressForKeyless(accountAddress: LazyAccountAddressClass): void { + _AccountAddress = accountAddress; +} + +/** + * The public key for a Federated Keyless account. + * + * A `FederatedKeylessPublicKey` extends the standard {@link KeylessPublicKey} + * by associating it with a specific on-chain address (`jwkAddress`) that holds + * the JWK set used to verify the OIDC provider's tokens. This allows + * organisations to host their own JWK endpoint as a Move resource. + * + * Like {@link KeylessPublicKey}, signature verification is asynchronous; the + * synchronous {@link verifySignature} method always throws. + * + * @example + * ```ts + * const pubKey = FederatedKeylessPublicKey.create({ + * iss: "https://accounts.google.com", + * uidKey: "sub", + * uidVal: "1234567890", + * aud: "my-app-client-id", + * pepper: pepperBytes, + * jwkAddress: "0x", + * }); + * ``` + */ +export class FederatedKeylessPublicKey extends AccountPublicKey { + /** The address that contains the JWK set to be used for verification. */ + readonly jwkAddress: unknown; // AccountAddress at runtime + + /** The inner public key which contains the standard Keyless public key. */ + readonly keylessPublicKey: KeylessPublicKey; + + /** + * Creates a `FederatedKeylessPublicKey`. + * + * @param jwkAddress - The on-chain address of the JWK resource. May be + * an `AccountAddress` instance, a hex string, or any value accepted by + * `AccountAddress.from`. + * @param keylessPublicKey - The inner {@link KeylessPublicKey}. + */ + constructor(jwkAddress: unknown, keylessPublicKey: KeylessPublicKey) { + super(); + if (_AccountAddress) { + this.jwkAddress = _AccountAddress.from(jwkAddress); + } else { + this.jwkAddress = jwkAddress; + } + this.keylessPublicKey = keylessPublicKey; + } + + /** + * Not supported for Federated Keyless keys — auth keys are derived through + * `AnyPublicKey` wrapping. + * + * @throws Always throws. + */ + authKey(): unknown { + // FederatedKeyless keys are wrapped in AnyPublicKey for on-chain use; + // the auth key is derived via the SingleKey scheme by the caller. + throw new Error("FederatedKeyless auth keys are derived through AnyPublicKey wrapping"); + } + + /** + * Not supported synchronously — use `verifySignatureAsync` instead. + * + * @throws Always throws. + */ + verifySignature(_args: VerifySignatureArgs): boolean { + throw new Error("Use verifySignatureAsync to verify FederatedKeyless signatures"); + } + + /** + * BCS-serialises the public key by writing the JWK address followed by the + * inner Keyless public key. + * + * @param serializer - The BCS serializer to write into. + * @throws If `AccountAddress` has not been registered via + * {@link registerAccountAddressForKeyless}. + */ + serialize(serializer: Serializer): void { + if (!_AccountAddress) { + throw new Error("AccountAddress not registered. Import core module first."); + } + (this.jwkAddress as { serialize(s: Serializer): void }).serialize(serializer); + this.keylessPublicKey.serialize(serializer); + } + + /** + * Deserialises a `FederatedKeylessPublicKey` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `FederatedKeylessPublicKey`. + * @throws If `AccountAddress` has not been registered via + * {@link registerAccountAddressForKeyless}. + */ + static deserialize(deserializer: Deserializer): FederatedKeylessPublicKey { + if (!_AccountAddress) { + throw new Error("AccountAddress not registered. Import core module first."); + } + const jwkAddress = _AccountAddress.deserialize(deserializer); + const keylessPublicKey = KeylessPublicKey.deserialize(deserializer); + return new FederatedKeylessPublicKey(jwkAddress, keylessPublicKey); + } + + /** + * Convenience factory that creates a `FederatedKeylessPublicKey` by + * computing the identity commitment from the provided JWT claims and pepper. + * + * @param args - The JWT identity parameters and JWK address. + * @param args.iss - The OIDC issuer URL. + * @param args.uidKey - The JWT claim name used as the user identifier. + * @param args.uidVal - The value of the UID claim. + * @param args.aud - The `aud` (audience) claim. + * @param args.pepper - The secret pepper bytes. + * @param args.jwkAddress - The on-chain address of the JWK resource. + * @returns A new `FederatedKeylessPublicKey`. + */ + static create(args: { + iss: string; + uidKey: string; + uidVal: string; + aud: string; + pepper: HexInput; + jwkAddress: unknown; + }): FederatedKeylessPublicKey { + return new FederatedKeylessPublicKey(args.jwkAddress, KeylessPublicKey.create(args)); + } + + /** + * Duck-type check that returns `true` if `publicKey` has the shape of a + * `FederatedKeylessPublicKey`. + * + * @param publicKey - The public key to inspect. + * @returns `true` if the key looks like a `FederatedKeylessPublicKey`. + */ + static isInstance(publicKey: PublicKey) { + return ( + "jwkAddress" in publicKey && + "keylessPublicKey" in publicKey && + publicKey.keylessPublicKey instanceof KeylessPublicKey + ); + } +} diff --git a/v10/src/crypto/hd-key.ts b/v10/src/crypto/hd-key.ts new file mode 100644 index 000000000..9ad0cb8fe --- /dev/null +++ b/v10/src/crypto/hd-key.ts @@ -0,0 +1,169 @@ +import { hmac } from "@noble/hashes/hmac.js"; +import { sha512 } from "@noble/hashes/sha2.js"; +import * as bip39 from "@scure/bip39"; + +/** + * A derived key and its associated chain code, as produced by the SLIP-0010 + * hierarchical-deterministic key derivation algorithm. + */ +export type DerivedKeys = { + /** The 32-byte derived private key. */ + key: Uint8Array; + /** The 32-byte chain code used for further child derivation. */ + chainCode: Uint8Array; +}; + +/** + * Regular expression that matches Aptos hardened derivation paths of the form + * `m/44'/637'/'/'/'?`. + * + * Used by {@link isValidHardenedPath}. + */ +export const APTOS_HARDENED_REGEX = /^m\/44'\/637'\/[0-9]+'\/[0-9]+'\/[0-9]+'?$/; + +/** + * Regular expression that matches Aptos BIP-44 derivation paths of the form + * `m/44'/637'/'//`. + * + * Used by {@link isValidBIP44Path}. + */ +export const APTOS_BIP44_REGEX = /^m\/44'\/637'\/[0-9]+'\/[0-9]+\/[0-9]+$/; + +/** + * The SLIP-0010 hardened key offset (2^31). + * + * Add this offset to a path segment index to request a hardened child key. + */ +export const HARDENED_OFFSET = 0x80000000; + +/** + * Returns whether `path` is a valid Aptos BIP-44 derivation path. + * + * A valid BIP-44 path for Aptos matches `m/44'/637'/'//`, + * where the last two segments are **not** hardened. + * + * @param path - The derivation path string to validate. + * @returns `true` if the path is valid, `false` otherwise. + * + * @example + * ```ts + * isValidBIP44Path("m/44'/637'/0'/0/0"); // true + * isValidBIP44Path("m/44'/637'/0'/0'/0'"); // false (hardened) + * ``` + */ +export function isValidBIP44Path(path: string): boolean { + return APTOS_BIP44_REGEX.test(path); +} + +/** + * Returns whether `path` is a valid Aptos hardened derivation path. + * + * A valid hardened path matches `m/44'/637'/'/'/'?`, + * where all segments are hardened (primed). + * + * @param path - The derivation path string to validate. + * @returns `true` if the path is a valid hardened path, `false` otherwise. + * + * @example + * ```ts + * isValidHardenedPath("m/44'/637'/0'/0'/0'"); // true + * isValidHardenedPath("m/44'/637'/0'/0/0"); // false + * ``` + */ +export function isValidHardenedPath(path: string): boolean { + return APTOS_HARDENED_REGEX.test(path); +} + +const textEncoder = new TextEncoder(); +const toBytes = (input: Uint8Array | string): Uint8Array => + typeof input === "string" ? textEncoder.encode(input) : input; + +const removeApostrophes = (val: string): string => val.replace(/'/g, ""); + +/** + * Derives a key and chain code from a seed using HMAC-SHA512, as specified by + * SLIP-0010. + * + * @param hashSeed - The HMAC key (a curve-specific seed string such as + * `"ed25519 seed"` or raw bytes). + * @param data - The data to derive from (the BIP-39 seed bytes for the master + * key, or child data for subsequent levels). + * @returns A {@link DerivedKeys} object containing the 32-byte `key` and + * 32-byte `chainCode`. + * + * @example + * ```ts + * const { key, chainCode } = deriveKey("ed25519 seed", seedBytes); + * ``` + */ +export const deriveKey = (hashSeed: Uint8Array | string, data: Uint8Array | string): DerivedKeys => { + const digest = hmac.create(sha512, toBytes(hashSeed)).update(toBytes(data)).digest(); + const key = digest.slice(0, 32); + const chainCode = digest.slice(32); + digest.fill(0); + return { key, chainCode }; +}; + +/** + * Derives a hardened child key from a parent key and chain code using the + * SLIP-0010 `CKDpriv` function. + * + * @param param0 - The parent {@link DerivedKeys} (`key` and `chainCode`). + * @param index - The child index. Pass a hardened index (i.e. `index + HARDENED_OFFSET`) + * to derive a hardened child. + * @returns The child {@link DerivedKeys}. + * + * @example + * ```ts + * const child = CKDPriv(parentKeys, 0 + HARDENED_OFFSET); + * ``` + */ +export const CKDPriv = ({ key, chainCode }: DerivedKeys, index: number): DerivedKeys => { + const data = new Uint8Array(1 + key.length + 4); + // data[0] = 0 (already zero-initialized) + data.set(key, 1); + new DataView(data.buffer).setUint32(1 + key.length, index); + const result = deriveKey(chainCode, data); + data.fill(0); + return result; +}; + +/** + * Splits a BIP-32/SLIP-0010 derivation path into its individual numeric + * segment strings, stripping the leading `"m"` component and removing + * hardened apostrophes. + * + * @param path - A derivation path such as `"m/44'/637'/0'/0'/0'"`. + * @returns An array of segment strings without apostrophes, e.g. + * `["44", "637", "0", "0", "0"]`. + * + * @example + * ```ts + * splitPath("m/44'/637'/0'/0'/0'"); // ["44", "637", "0", "0", "0"] + * ``` + */ +export const splitPath = (path: string): Array => path.split("/").slice(1).map(removeApostrophes); + +/** + * Converts a BIP-39 mnemonic phrase to its 64-byte seed using + * `bip39.mnemonicToSeedSync`. + * + * The mnemonic is normalised (trimmed, collapsed whitespace, lower-cased) + * before derivation. + * + * @param mnemonic - A space-separated BIP-39 mnemonic phrase. + * @returns The 64-byte seed as a `Uint8Array`. + * + * @example + * ```ts + * const seed = mnemonicToSeed("abandon abandon abandon ... about"); + * ``` + */ +export const mnemonicToSeed = (mnemonic: string): Uint8Array => { + const normalizedMnemonic = mnemonic + .trim() + .split(/\s+/) + .map((part) => part.toLowerCase()) + .join(" "); + return bip39.mnemonicToSeedSync(normalizedMnemonic); +}; diff --git a/v10/src/crypto/index.ts b/v10/src/crypto/index.ts new file mode 100644 index 000000000..ddb9b06c6 --- /dev/null +++ b/v10/src/crypto/index.ts @@ -0,0 +1,42 @@ +/** + * @module crypto + * + * Public API for the Aptos TypeScript SDK cryptographic primitives. + * + * Re-exports all public types and utilities from the individual crypto + * sub-modules (Ed25519, Secp256k1, Secp256r1, multi-key schemes, Keyless + * authentication, BIP-39/SLIP-0010 HD key derivation, Poseidon hashing, + * BCS serialisation helpers, and more). + * + * Side-effect: registers the Keyless public-key types with the `AnyPublicKey` + * / `AnySignature` deserialisers to break the circular dependency between + * `single-key.ts` and `keyless.ts`. + */ +export * from "./abstraction.js"; +export * from "./deserialization-utils.js"; +export * from "./ed25519.js"; +export * from "./ephemeral.js"; +export * from "./federated-keyless.js"; +export * from "./hd-key.js"; +export * from "./keyless.js"; +export * from "./multi-ed25519.js"; +export * from "./multi-key.js"; +export * from "./poseidon.js"; +export * from "./private-key.js"; +export * from "./proof.js"; +export * from "./public-key.js"; +export * from "./secp256k1.js"; +export * from "./secp256r1.js"; +export * from "./signature.js"; +export * from "./single-key.js"; +export * from "./types.js"; +export * from "./utils.js"; + +import { FederatedKeylessPublicKey } from "./federated-keyless.js"; +import { KeylessPublicKey, KeylessSignature } from "./keyless.js"; +// Register keyless types so that single-key.ts can deserialize them +// without circular imports. This side-effect runs once when +// `@aptos-labs/ts-sdk/crypto` is imported. +import { registerKeylessTypes } from "./single-key.js"; + +registerKeylessTypes(KeylessPublicKey, FederatedKeylessPublicKey, KeylessSignature); diff --git a/v10/src/crypto/keyless.ts b/v10/src/crypto/keyless.ts new file mode 100644 index 000000000..788a5972f --- /dev/null +++ b/v10/src/crypto/keyless.ts @@ -0,0 +1,1144 @@ +import type { Fp2 } from "@noble/curves/abstract/tower.js"; +import type { WeierstrassPoint } from "@noble/curves/abstract/weierstrass.js"; +import { bn254 } from "@noble/curves/bn254.js"; +import { bytesToNumberBE } from "@noble/curves/utils.js"; +import { sha3_256 } from "@noble/hashes/sha3.js"; +import { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, Serializer } from "../bcs/serializer.js"; +import { Hex, type HexInput } from "../hex/index.js"; +import { generateSigningMessage } from "../transactions/signing-message.js"; +import { Ed25519PublicKey, Ed25519Signature } from "./ed25519.js"; +import { EphemeralPublicKey, EphemeralSignature } from "./ephemeral.js"; +import { bigIntToBytesLE, bytesToBigIntLE, hashStrToField, poseidonHash } from "./poseidon.js"; +import { Proof } from "./proof.js"; +import { AccountPublicKey, type PublicKey, type VerifySignatureArgs } from "./public-key.js"; +import { Signature } from "./signature.js"; +import { EphemeralCertificateVariant, ZkpVariant } from "./types.js"; + +const TEXT_ENCODER = new TextEncoder(); + +/** Maximum seconds in the future an ephemeral key may expire. */ +export const EPK_HORIZON_SECS = 10000000; +/** Maximum byte length of an `aud` (audience) value. */ +export const MAX_AUD_VAL_BYTES = 120; +/** Maximum byte length of a UID key (e.g. `"sub"`). */ +export const MAX_UID_KEY_BYTES = 30; +/** Maximum byte length of a UID value. */ +export const MAX_UID_VAL_BYTES = 330; +/** Maximum byte length of an `iss` (issuer) value. */ +export const MAX_ISS_VAL_BYTES = 120; +/** Maximum byte length of an extra JWT field. */ +export const MAX_EXTRA_FIELD_BYTES = 350; +/** Maximum byte length of the base64-URL encoded JWT header. */ +export const MAX_JWT_HEADER_B64_BYTES = 300; +/** Maximum byte length of the committed ephemeral public key. */ +export const MAX_COMMITED_EPK_BYTES = 93; + +/** + * Computes the identity commitment (IdC) used in Keyless public keys. + * + * The commitment is a Poseidon hash of the pepper, audience, UID value, and + * UID key, binding the ephemeral key to the user's JWT identity without + * revealing it on-chain. + * + * @param args - The inputs to the commitment. + * @param args.uidKey - The JWT claim name used as the user identifier (e.g. + * `"sub"`). + * @param args.uidVal - The value of the UID claim. + * @param args.aud - The `aud` (audience) claim of the JWT. + * @param args.pepper - A secret random value that hides the identity on-chain. + * @returns The 32-byte identity commitment as a `Uint8Array`. + * + * @example + * ```ts + * const idc = computeIdCommitment({ + * uidKey: "sub", + * uidVal: "1234567890", + * aud: "my-app-client-id", + * pepper: pepperBytes, + * }); + * ``` + */ +export function computeIdCommitment(args: { + uidKey: string; + uidVal: string; + aud: string; + pepper: HexInput; +}): Uint8Array { + const { uidKey, uidVal, aud, pepper } = args; + const fields = [ + bytesToBigIntLE(Hex.fromHexInput(pepper).toUint8Array()), + hashStrToField(aud, MAX_AUD_VAL_BYTES), + hashStrToField(uidVal, MAX_UID_VAL_BYTES), + hashStrToField(uidKey, MAX_UID_KEY_BYTES), + ]; + return bigIntToBytesLE(poseidonHash(fields), KeylessPublicKey.ID_COMMITMENT_LENGTH); +} + +/** + * The public key used by Aptos Keyless accounts. + * + * A `KeylessPublicKey` consists of an `iss` (OIDC issuer) and an identity + * commitment (`idCommitment`) that cryptographically binds the on-chain key to + * the user's off-chain JWT identity without revealing it publicly. + * + * Keyless signatures must be verified asynchronously — the synchronous + * {@link verifySignature} method always throws. + * + * @example + * ```ts + * const pubKey = KeylessPublicKey.create({ + * iss: "https://accounts.google.com", + * uidKey: "sub", + * uidVal: "1234567890", + * aud: "my-app-client-id", + * pepper: pepperBytes, + * }); + * ``` + */ +export class KeylessPublicKey extends AccountPublicKey { + /** Byte length of the identity commitment. */ + static readonly ID_COMMITMENT_LENGTH: number = 32; + + /** The OIDC issuer URL (e.g. `"https://accounts.google.com"`). */ + readonly iss: string; + /** The 32-byte identity commitment. */ + readonly idCommitment: Uint8Array; + + /** + * Creates a `KeylessPublicKey` from an issuer URL and a raw identity + * commitment. + * + * @param iss - The OIDC issuer URL. + * @param idCommitment - The 32-byte identity commitment as bytes or hex. + * @throws If `idCommitment` is not exactly 32 bytes. + */ + constructor(iss: string, idCommitment: HexInput) { + super(); + const idcBytes = Hex.fromHexInput(idCommitment).toUint8Array(); + if (idcBytes.length !== KeylessPublicKey.ID_COMMITMENT_LENGTH) { + throw new Error(`Id Commitment length in bytes should be ${KeylessPublicKey.ID_COMMITMENT_LENGTH}`); + } + this.iss = iss; + this.idCommitment = idcBytes; + } + + /** + * Not supported for Keyless keys — auth keys are derived through + * `AnyPublicKey` wrapping. + * + * @throws Always throws. + */ + authKey(): unknown { + // Keyless keys are wrapped in AnyPublicKey for on-chain use; + // the auth key is derived via the SingleKey scheme by the caller. + // This method is not typically called directly on KeylessPublicKey. + throw new Error("Keyless auth keys are derived through AnyPublicKey wrapping"); + } + + /** + * Not supported synchronously — use `verifySignatureAsync` instead. + * + * @throws Always throws. + */ + verifySignature(_args: VerifySignatureArgs): boolean { + throw new Error("Use verifySignatureAsync to verify Keyless signatures"); + } + + /** + * BCS-serialises the public key by writing the issuer string followed by + * the identity commitment bytes. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeStr(this.iss); + serializer.serializeBytes(this.idCommitment); + } + + /** + * Deserialises a `KeylessPublicKey` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `KeylessPublicKey`. + */ + static deserialize(deserializer: Deserializer): KeylessPublicKey { + const iss = deserializer.deserializeStr(); + const addressSeed = deserializer.deserializeBytes(); + return new KeylessPublicKey(iss, addressSeed); + } + + /** + * Alias for {@link deserialize} — deserialises a `KeylessPublicKey` from a + * BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `KeylessPublicKey`. + */ + static load(deserializer: Deserializer): KeylessPublicKey { + const iss = deserializer.deserializeStr(); + const addressSeed = deserializer.deserializeBytes(); + return new KeylessPublicKey(iss, addressSeed); + } + + /** + * Convenience factory that creates a `KeylessPublicKey` by computing the + * identity commitment from the provided JWT claims and pepper. + * + * @param args - The JWT identity parameters. + * @param args.iss - The OIDC issuer URL. + * @param args.uidKey - The JWT claim name used as the user identifier. + * @param args.uidVal - The value of the UID claim. + * @param args.aud - The `aud` (audience) claim. + * @param args.pepper - The secret pepper bytes. + * @returns A new `KeylessPublicKey`. + */ + static create(args: { + iss: string; + uidKey: string; + uidVal: string; + aud: string; + pepper: HexInput; + }): KeylessPublicKey { + return new KeylessPublicKey(args.iss, computeIdCommitment(args)); + } + + /** + * Duck-type check that returns `true` if `publicKey` has the shape of a + * `KeylessPublicKey`. + * + * @param publicKey - The public key to inspect. + * @returns `true` if the key looks like a `KeylessPublicKey`. + */ + static isInstance(publicKey: PublicKey) { + return ( + "iss" in publicKey && + typeof publicKey.iss === "string" && + "idCommitment" in publicKey && + publicKey.idCommitment instanceof Uint8Array + ); + } +} + +/** + * The on-chain signature produced by a Keyless account. + * + * A `KeylessSignature` bundles together: + * - An {@link EphemeralCertificate} (the zero-knowledge proof or training- + * wheels signature that authorises the ephemeral key). + * - The base64-encoded JWT header. + * - The ephemeral key expiry timestamp. + * - The {@link EphemeralPublicKey} used for the inner signature. + * - The {@link EphemeralSignature} over the transaction. + */ +export class KeylessSignature extends Signature { + /** The zero-knowledge proof (or training-wheels certificate) for the ephemeral key. */ + readonly ephemeralCertificate: EphemeralCertificate; + /** The base64-URL encoded JWT header. */ + readonly jwtHeader: string; + /** Unix timestamp (seconds) at which the ephemeral key expires. */ + readonly expiryDateSecs: number; + /** The ephemeral public key used to sign the transaction. */ + readonly ephemeralPublicKey: EphemeralPublicKey; + /** The signature over the transaction produced by the ephemeral private key. */ + readonly ephemeralSignature: EphemeralSignature; + + /** + * Creates a `KeylessSignature`. + * + * @param args - The components of the Keyless signature. + * @param args.jwtHeader - The base64-URL encoded JWT header string. + * @param args.ephemeralCertificate - The ZK proof or training-wheels cert. + * @param args.expiryDateSecs - Expiry of the ephemeral key in Unix seconds. + * @param args.ephemeralPublicKey - The ephemeral public key. + * @param args.ephemeralSignature - The signature over the transaction. + */ + constructor(args: { + jwtHeader: string; + ephemeralCertificate: EphemeralCertificate; + expiryDateSecs: number; + ephemeralPublicKey: EphemeralPublicKey; + ephemeralSignature: EphemeralSignature; + }) { + super(); + const { jwtHeader, ephemeralCertificate, expiryDateSecs, ephemeralPublicKey, ephemeralSignature } = args; + this.jwtHeader = jwtHeader; + this.ephemeralCertificate = ephemeralCertificate; + this.expiryDateSecs = expiryDateSecs; + this.ephemeralPublicKey = ephemeralPublicKey; + this.ephemeralSignature = ephemeralSignature; + } + + /** + * Parses the JWT header and returns the `kid` (key ID) field. + * + * @returns The `kid` value from the JWT header. + * @throws If the header does not contain a `kid` field. + */ + getJwkKid(): string { + return parseJwtHeader(this.jwtHeader).kid; + } + + /** + * BCS-serialises the Keyless signature. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + this.ephemeralCertificate.serialize(serializer); + serializer.serializeStr(this.jwtHeader); + serializer.serializeU64(this.expiryDateSecs); + this.ephemeralPublicKey.serialize(serializer); + this.ephemeralSignature.serialize(serializer); + } + + /** + * Deserialises a `KeylessSignature` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `KeylessSignature`. + */ + static deserialize(deserializer: Deserializer): KeylessSignature { + const ephemeralCertificate = EphemeralCertificate.deserialize(deserializer); + const jwtHeader = deserializer.deserializeStr(); + if (TEXT_ENCODER.encode(jwtHeader).length > MAX_JWT_HEADER_B64_BYTES) { + throw new Error(`JWT header exceeds maximum length of ${MAX_JWT_HEADER_B64_BYTES} bytes`); + } + const expiryDateSecsBig = deserializer.deserializeU64(); + if (expiryDateSecsBig > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new Error(`expiryDateSecs ${expiryDateSecsBig} exceeds safe integer range`); + } + const expiryDateSecs = Number(expiryDateSecsBig); + const ephemeralPublicKey = EphemeralPublicKey.deserialize(deserializer); + const ephemeralSignature = EphemeralSignature.deserialize(deserializer); + return new KeylessSignature({ + jwtHeader, + expiryDateSecs: Number(expiryDateSecs), + ephemeralCertificate, + ephemeralPublicKey, + ephemeralSignature, + }); + } + + /** + * Returns a placeholder `KeylessSignature` suitable for transaction + * simulation (all proof and signature bytes are zeroed). + * + * @returns A zeroed-out `KeylessSignature`. + */ + static getSimulationSignature(): KeylessSignature { + return new KeylessSignature({ + jwtHeader: "{}", + ephemeralCertificate: new EphemeralCertificate( + new ZeroKnowledgeSig({ + proof: new ZkProof( + new Groth16Zkp({ a: new Uint8Array(32), b: new Uint8Array(64), c: new Uint8Array(32) }), + ZkpVariant.Groth16, + ), + expHorizonSecs: 0, + }), + EphemeralCertificateVariant.ZkProof, + ), + expiryDateSecs: 0, + ephemeralPublicKey: new EphemeralPublicKey(new Ed25519PublicKey(new Uint8Array(32))), + ephemeralSignature: new EphemeralSignature(new Ed25519Signature(new Uint8Array(64))), + }); + } +} + +/** + * A type-tagged certificate that authorises an ephemeral key for use in a + * Keyless signature. + * + * Currently only the `ZkProof` (zero-knowledge proof) variant is supported. + */ +export class EphemeralCertificate extends Signature { + /** The underlying certificate (currently a {@link ZeroKnowledgeSig}). */ + public readonly signature: Signature; + /** The variant discriminant. */ + readonly variant: EphemeralCertificateVariant; + + /** + * Creates an `EphemeralCertificate`. + * + * @param signature - The proof or signature that certifies the ephemeral key. + * @param variant - The variant discriminant (currently only `ZkProof`). + */ + constructor(signature: Signature, variant: EphemeralCertificateVariant) { + super(); + this.signature = signature; + this.variant = variant; + } + + /** + * Returns the raw bytes of the inner certificate signature. + * + * @returns The certificate bytes as a `Uint8Array`. + */ + toUint8Array(): Uint8Array { + return this.signature.toUint8Array(); + } + + /** + * BCS-serialises the certificate by writing the ULEB128 variant index + * followed by the inner signature. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(this.variant); + this.signature.serialize(serializer); + } + + /** + * Deserialises an `EphemeralCertificate` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `EphemeralCertificate`. + * @throws If the variant index is not recognised. + */ + static deserialize(deserializer: Deserializer): EphemeralCertificate { + const variant = deserializer.deserializeUleb128AsU32(); + switch (variant) { + case EphemeralCertificateVariant.ZkProof: + return new EphemeralCertificate(ZeroKnowledgeSig.deserialize(deserializer), variant); + default: + throw new Error(`Unknown variant index for EphemeralCertificate: ${variant}`); + } + } +} + +// ── BN254 G1/G2 Helpers ── + +function bytesToBn254FpBE(bytes: Uint8Array): bigint { + if (bytes.length !== 32) { + throw new Error("Input should be 32 bytes"); + } + const result = new Uint8Array(bytes); + result[0] &= 0x3f; + return bytesToNumberBE(result); +} + +class G1Bytes extends Serializable { + private static readonly B = bn254.fields.Fp.create(3n); + + readonly data: Uint8Array; + + constructor(data: HexInput) { + super(); + const bytes = Hex.fromHexInput(data).toUint8Array(); + if (bytes.length !== 32) { + throw new Error("Input needs to be 32 bytes"); + } + this.data = bytes.slice(); + } + + serialize(serializer: Serializer): void { + serializer.serializeFixedBytes(this.data); + } + + static deserialize(deserializer: Deserializer): G1Bytes { + const bytes = deserializer.deserializeFixedBytes(32); + return new G1Bytes(bytes); + } + + toArray(): string[] { + const point = this.toProjectivePoint(); + return [point.x.toString(), point.y.toString(), point.Z.toString()]; + } + + toProjectivePoint(): WeierstrassPoint { + const bytes = new Uint8Array(this.data); + bytes.reverse(); + const yFlag = (bytes[0] & 0x80) >> 7; + const { Fp } = bn254.fields; + const x = Fp.create(bytesToBn254FpBE(bytes)); + const y = Fp.sqrt(Fp.add(Fp.pow(x, 3n), G1Bytes.B)); + const negY = Fp.neg(y); + const yToUse = y > negY === (yFlag === 1) ? y : negY; + return bn254.G1.Point.fromAffine({ x, y: yToUse }); + } +} + +class G2Bytes extends Serializable { + private static readonly B = bn254.fields.Fp2.fromBigTuple([ + 19485874751759354771024239261021720505790618469301721065564631296452457478373n, + 266929791119991161246907387137283842545076965332900288569378510910307636690n, + ]); + + readonly data: Uint8Array; + + constructor(data: HexInput) { + super(); + const bytes = Hex.fromHexInput(data).toUint8Array(); + if (bytes.length !== 64) { + throw new Error("Input needs to be 64 bytes"); + } + this.data = bytes.slice(); + } + + serialize(serializer: Serializer): void { + serializer.serializeFixedBytes(this.data); + } + + static deserialize(deserializer: Deserializer): G2Bytes { + const bytes = deserializer.deserializeFixedBytes(64); + return new G2Bytes(bytes); + } + + toArray(): [string, string][] { + const point = this.toProjectivePoint(); + return [ + [point.x.c0.toString(), point.x.c1.toString()], + [point.y.c0.toString(), point.y.c1.toString()], + [point.Z.c0.toString(), point.Z.c1.toString()], + ]; + } + + toProjectivePoint(): WeierstrassPoint { + const bytes = new Uint8Array(this.data); + const x0 = bytes.subarray(0, 32); + const x1 = bytes.subarray(32, 64); + x0.reverse(); + x1.reverse(); + const yFlag = (x1[0] & 0x80) >> 7; + const { Fp2 } = bn254.fields; + const x = Fp2.fromBigTuple([bytesToBn254FpBE(x0), bytesToBn254FpBE(x1)]); + const y = Fp2.sqrt(Fp2.add(Fp2.pow(x, 3n), G2Bytes.B)); + const negY = Fp2.neg(y); + const isYGreaterThanNegY = y.c1 > negY.c1 || (y.c1 === negY.c1 && y.c0 > negY.c0); + const yToUse = isYGreaterThanNegY === (yFlag === 1) ? y : negY; + return bn254.G2.Point.fromAffine({ x, y: yToUse }); + } +} + +// ── Groth16 Proof Types ── + +/** + * A Groth16 zero-knowledge proof, consisting of three BN254 elliptic curve + * points: `a` (G1), `b` (G2), and `c` (G1). + * + * @example + * ```ts + * const proof = new Groth16Zkp({ a: aBytes, b: bBytes, c: cBytes }); + * ``` + */ +export class Groth16Zkp extends Proof { + /** The `a` point of the proof (BN254 G1, 32 bytes). */ + a: G1Bytes; + /** The `b` point of the proof (BN254 G2, 64 bytes). */ + b: G2Bytes; + /** The `c` point of the proof (BN254 G1, 32 bytes). */ + c: G1Bytes; + + /** + * Creates a `Groth16Zkp` from its three constituent point byte arrays. + * + * @param args - Object with `a` (32 bytes), `b` (64 bytes), `c` (32 bytes). + */ + constructor(args: { a: HexInput; b: HexInput; c: HexInput }) { + super(); + const { a, b, c } = args; + this.a = new G1Bytes(a); + this.b = new G2Bytes(b); + this.c = new G1Bytes(c); + } + + /** + * BCS-serialises the proof by writing `a`, `b`, and `c` in order. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + this.a.serialize(serializer); + this.b.serialize(serializer); + this.c.serialize(serializer); + } + + /** + * Deserialises a `Groth16Zkp` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `Groth16Zkp`. + */ + static deserialize(deserializer: Deserializer): Groth16Zkp { + const a = G1Bytes.deserialize(deserializer).data; + const b = G2Bytes.deserialize(deserializer).data; + const c = G1Bytes.deserialize(deserializer).data; + return new Groth16Zkp({ a, b, c }); + } + + /** + * Returns the proof formatted as a SnarkJS-compatible JSON object. + * + * @returns A plain object with `protocol`, `curve`, `pi_a`, `pi_b`, and + * `pi_c` fields. + */ + toSnarkJsJson() { + return { + protocol: "groth16", + curve: "bn128", + pi_a: this.a.toArray(), + pi_b: this.b.toArray(), + pi_c: this.c.toArray(), + }; + } +} + +/** + * Bundles a {@link Groth16Zkp} together with the hash of its public inputs, + * for use in domain-separated signing and verification. + */ +export class Groth16ProofAndStatement extends Serializable { + /** The Groth16 proof. */ + proof: Groth16Zkp; + /** The 32-byte hash of the public inputs. */ + publicInputsHash: Uint8Array; + /** Domain separator used in the signing message construction. */ + readonly domainSeparator = "APTOS::Groth16ProofAndStatement"; + + /** + * Creates a `Groth16ProofAndStatement`. + * + * @param proof - The Groth16 proof. + * @param publicInputsHash - The 32-byte public-inputs hash as bytes, a hex + * string, or a `bigint` (encoded as 32-byte little-endian). + * @throws If the public-inputs hash is not 32 bytes. + */ + constructor(proof: Groth16Zkp, publicInputsHash: HexInput | bigint) { + super(); + this.proof = proof; + this.publicInputsHash = + typeof publicInputsHash === "bigint" + ? bigIntToBytesLE(publicInputsHash, 32) + : Hex.fromHexInput(publicInputsHash).toUint8Array(); + if (this.publicInputsHash.length !== 32) { + throw new Error("Invalid public inputs hash"); + } + } + + /** + * BCS-serialises the proof and its public-inputs hash. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + this.proof.serialize(serializer); + serializer.serializeFixedBytes(this.publicInputsHash); + } + + /** + * Deserialises a `Groth16ProofAndStatement` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `Groth16ProofAndStatement`. + */ + static deserialize(deserializer: Deserializer): Groth16ProofAndStatement { + return new Groth16ProofAndStatement(Groth16Zkp.deserialize(deserializer), deserializer.deserializeFixedBytes(32)); + } + + /** + * Computes a domain-separated SHA3-256 hash of the BCS-encoded proof and + * statement, suitable for use as a signing message. + * + * @returns A 32-byte hash `Uint8Array`. + */ + hash(): Uint8Array { + return generateSigningMessage(this.bcsToBytes(), this.domainSeparator); + } +} + +/** + * A type-tagged zero-knowledge proof container. + * + * `ZkProof` wraps a concrete {@link Proof} implementation (currently only + * {@link Groth16Zkp}) with a variant discriminant for BCS serialisation. + */ +export class ZkProof extends Serializable { + /** The underlying ZK proof. */ + public readonly proof: Proof; + /** The variant discriminant identifying the proof system. */ + readonly variant: ZkpVariant; + + /** + * Creates a `ZkProof`. + * + * @param proof - The concrete proof object. + * @param variant - The ZK proof system variant. + */ + constructor(proof: Proof, variant: ZkpVariant) { + super(); + this.proof = proof; + this.variant = variant; + } + + /** + * BCS-serialises the proof by writing the ULEB128 variant index followed by + * the inner proof bytes. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(this.variant); + this.proof.serialize(serializer); + } + + /** + * Deserialises a `ZkProof` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `ZkProof`. + * @throws If the variant index is not recognised. + */ + static deserialize(deserializer: Deserializer): ZkProof { + const variant = deserializer.deserializeUleb128AsU32(); + switch (variant) { + case ZkpVariant.Groth16: + return new ZkProof(Groth16Zkp.deserialize(deserializer), variant); + default: + throw new Error(`Unknown variant index for ZkProof: ${variant}`); + } + } +} + +/** + * A zero-knowledge signature that certifies an ephemeral key. + * + * `ZeroKnowledgeSig` contains the ZK proof and expiry horizon, and optionally + * an extra JWT field, an audience override, and a training-wheels signature. + */ +export class ZeroKnowledgeSig extends Signature { + /** The ZK proof that certifies the ephemeral key. */ + readonly proof: ZkProof; + /** The maximum expiry horizon (in seconds) accepted by the chain. */ + readonly expHorizonSecs: number; + /** An optional extra JWT field included in the proof statement. */ + readonly extraField?: string; + /** An optional audience value override used during recovery. */ + readonly overrideAudVal?: string; + /** An optional training-wheels ephemeral signature from the OIDC provider. */ + readonly trainingWheelsSignature?: EphemeralSignature; + + /** + * Creates a `ZeroKnowledgeSig`. + * + * @param args - The ZK signature components. + * @param args.proof - The ZK proof. + * @param args.expHorizonSecs - The accepted expiry horizon in seconds. + * @param args.extraField - Optional extra JWT field. + * @param args.overrideAudVal - Optional audience override. + * @param args.trainingWheelsSignature - Optional training-wheels signature. + */ + constructor(args: { + proof: ZkProof; + expHorizonSecs: number; + extraField?: string; + overrideAudVal?: string; + trainingWheelsSignature?: EphemeralSignature; + }) { + super(); + const { proof, expHorizonSecs, trainingWheelsSignature, extraField, overrideAudVal } = args; + this.proof = proof; + this.expHorizonSecs = expHorizonSecs; + this.trainingWheelsSignature = trainingWheelsSignature; + this.extraField = extraField; + this.overrideAudVal = overrideAudVal; + } + + /** + * Constructs a `ZeroKnowledgeSig` by deserialising BCS bytes. + * + * @param bytes - The raw BCS-encoded bytes. + * @returns A new `ZeroKnowledgeSig`. + */ + static fromBytes(bytes: Uint8Array): ZeroKnowledgeSig { + return ZeroKnowledgeSig.deserialize(new Deserializer(bytes)); + } + + /** + * BCS-serialises the ZK signature. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + this.proof.serialize(serializer); + serializer.serializeU64(this.expHorizonSecs); + serializer.serializeOption(this.extraField); + serializer.serializeOption(this.overrideAudVal); + serializer.serializeOption(this.trainingWheelsSignature); + } + + /** + * Deserialises a `ZeroKnowledgeSig` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `ZeroKnowledgeSig`. + */ + static deserialize(deserializer: Deserializer): ZeroKnowledgeSig { + const proof = ZkProof.deserialize(deserializer); + const expHorizonSecsBig = deserializer.deserializeU64(); + if (expHorizonSecsBig > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new Error(`expHorizonSecs ${expHorizonSecsBig} exceeds safe integer range`); + } + const expHorizonSecs = Number(expHorizonSecsBig); + const extraField = deserializer.deserializeOption("string"); + const overrideAudVal = deserializer.deserializeOption("string"); + const trainingWheelsSignature = deserializer.deserializeOption(EphemeralSignature); + return new ZeroKnowledgeSig({ proof, expHorizonSecs, trainingWheelsSignature, extraField, overrideAudVal }); + } +} + +// ── KeylessConfiguration (pure data, no network calls) ── + +/** + * Immutable configuration for the Keyless authentication scheme, typically + * fetched from the on-chain configuration resource. + * + * Includes the Groth16 verification key and various byte-length limits used + * during proof verification. + */ +export class KeylessConfiguration { + /** The Groth16 verification key. */ + readonly verificationKey: Groth16VerificationKey; + /** The maximum accepted ephemeral key expiry horizon in seconds. */ + readonly maxExpHorizonSecs: number; + /** Optional training-wheels ephemeral public key from the OIDC provider. */ + readonly trainingWheelsPubkey?: EphemeralPublicKey; + /** Maximum byte length of extra JWT fields. */ + readonly maxExtraFieldBytes: number; + /** Maximum byte length of the base64-URL encoded JWT header. */ + readonly maxJwtHeaderB64Bytes: number; + /** Maximum byte length of the `iss` (issuer) value. */ + readonly maxIssValBytes: number; + /** Maximum byte length of the committed ephemeral public key. */ + readonly maxCommitedEpkBytes: number; + + /** + * Creates a `KeylessConfiguration`. + * + * @param args - Configuration parameters. + * @param args.verificationKey - The Groth16 verification key. + * @param args.trainingWheelsPubkey - Optional training-wheels Ed25519 public key + * bytes. + * @param args.maxExpHorizonSecs - Maximum ephemeral key expiry horizon. + * @param args.maxExtraFieldBytes - Maximum extra JWT field byte length. + * @param args.maxJwtHeaderB64Bytes - Maximum JWT header base64 byte length. + * @param args.maxIssValBytes - Maximum issuer value byte length. + * @param args.maxCommitedEpkBytes - Maximum committed EPK byte length. + */ + constructor(args: { + verificationKey: Groth16VerificationKey; + trainingWheelsPubkey?: HexInput; + maxExpHorizonSecs?: number; + maxExtraFieldBytes?: number; + maxJwtHeaderB64Bytes?: number; + maxIssValBytes?: number; + maxCommitedEpkBytes?: number; + }) { + const { + verificationKey, + trainingWheelsPubkey, + maxExpHorizonSecs = EPK_HORIZON_SECS, + maxExtraFieldBytes = MAX_EXTRA_FIELD_BYTES, + maxJwtHeaderB64Bytes = MAX_JWT_HEADER_B64_BYTES, + maxIssValBytes = MAX_ISS_VAL_BYTES, + maxCommitedEpkBytes = MAX_COMMITED_EPK_BYTES, + } = args; + + this.verificationKey = verificationKey; + this.maxExpHorizonSecs = maxExpHorizonSecs; + if (trainingWheelsPubkey) { + this.trainingWheelsPubkey = new EphemeralPublicKey(new Ed25519PublicKey(trainingWheelsPubkey)); + } + this.maxExtraFieldBytes = maxExtraFieldBytes; + this.maxJwtHeaderB64Bytes = maxJwtHeaderB64Bytes; + this.maxIssValBytes = maxIssValBytes; + this.maxCommitedEpkBytes = maxCommitedEpkBytes; + } +} + +/** + * The Groth16 verification key used to verify Keyless ZK proofs on-chain. + * + * Contains the five BN254 elliptic-curve elements that define the verification + * key: `alphaG1`, `betaG2`, `deltaG2`, `gammaAbcG1`, and `gammaG2`. + */ +export class Groth16VerificationKey { + /** The `alpha_1` G1 element of the verification key. */ + readonly alphaG1: G1Bytes; + /** The `beta_2` G2 element of the verification key. */ + readonly betaG2: G2Bytes; + /** The `delta_2` G2 element of the verification key. */ + readonly deltaG2: G2Bytes; + /** The `gamma_abc_1` G1 elements (IC) of the verification key. */ + readonly gammaAbcG1: [G1Bytes, G1Bytes]; + /** The `gamma_2` G2 element of the verification key. */ + readonly gammaG2: G2Bytes; + + /** + * Creates a `Groth16VerificationKey`. + * + * @param args - The verification key elements. + * @param args.alphaG1 - The 32-byte alpha G1 element. + * @param args.betaG2 - The 64-byte beta G2 element. + * @param args.deltaG2 - The 64-byte delta G2 element. + * @param args.gammaAbcG1 - Tuple of two 32-byte gamma-ABC G1 elements. + * @param args.gammaG2 - The 64-byte gamma G2 element. + */ + constructor(args: { + alphaG1: HexInput; + betaG2: HexInput; + deltaG2: HexInput; + gammaAbcG1: [HexInput, HexInput]; + gammaG2: HexInput; + }) { + const { alphaG1, betaG2, deltaG2, gammaAbcG1, gammaG2 } = args; + this.alphaG1 = new G1Bytes(alphaG1); + this.betaG2 = new G2Bytes(betaG2); + this.deltaG2 = new G2Bytes(deltaG2); + this.gammaAbcG1 = [new G1Bytes(gammaAbcG1[0]), new G1Bytes(gammaAbcG1[1])]; + this.gammaG2 = new G2Bytes(gammaG2); + } + + /** + * Computes a SHA3-256 hash of the BCS-serialised verification key. + * + * Useful for fingerprinting or comparing verification keys. + * + * @returns A 32-byte hash of the verification key. + */ + public hash(): Uint8Array { + const serializer = new Serializer(); + this.serialize(serializer); + return sha3_256.create().update(serializer.toUint8Array()).digest(); + } + + /** + * BCS-serialises the verification key by writing its five elements in order. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + this.alphaG1.serialize(serializer); + this.betaG2.serialize(serializer); + this.deltaG2.serialize(serializer); + this.gammaAbcG1[0].serialize(serializer); + this.gammaAbcG1[1].serialize(serializer); + this.gammaG2.serialize(serializer); + } + + /** + * Verifies a Groth16 proof against this verification key using BN254 + * bilinear pairings. + * + * Checks the pairing equation: + * `e(A, B) == e(alpha, beta) * e(IC_0 + hash * IC_1, gamma) * e(C, delta)` + * + * @param args - Object containing `publicInputsHash` (bigint) and + * `groth16Proof` ({@link Groth16Zkp}). + * @returns `true` if the proof verifies, `false` otherwise. + */ + verifyProof(args: { publicInputsHash: bigint; groth16Proof: Groth16Zkp }): boolean { + const { publicInputsHash, groth16Proof } = args; + + const proofA = groth16Proof.a.toProjectivePoint(); + const proofB = groth16Proof.b.toProjectivePoint(); + const proofC = groth16Proof.c.toProjectivePoint(); + + const vkAlpha1 = this.alphaG1.toProjectivePoint(); + const vkBeta2 = this.betaG2.toProjectivePoint(); + const vkGamma2 = this.gammaG2.toProjectivePoint(); + const vkDelta2 = this.deltaG2.toProjectivePoint(); + const vkIC = this.gammaAbcG1.map((g1) => g1.toProjectivePoint()); + + const { Fp12 } = bn254.fields; + + // e(A_1, B_2) = e(alpha_1, beta_2) * e(ic_0 + public_inputs_hash * ic_1, gamma_2) * e(C_1, delta_2) + const accum = vkIC[0].add(vkIC[1].multiply(publicInputsHash)); + const pairingAccumGamma = bn254.pairing(accum, vkGamma2); + const pairingAB = bn254.pairing(proofA, proofB); + const pairingAlphaBeta = bn254.pairing(vkAlpha1, vkBeta2); + const pairingCDelta = bn254.pairing(proofC, vkDelta2); + const product = Fp12.mul(pairingAlphaBeta, Fp12.mul(pairingAccumGamma, pairingCDelta)); + return Fp12.eql(pairingAB, product); + } + + /** + * Returns the verification key formatted as a SnarkJS-compatible JSON + * object. + * + * @returns A plain object with `protocol`, `curve`, `nPublic`, and the five + * verification key elements. + */ + toSnarkJsJson() { + return { + protocol: "groth16", + curve: "bn128", + nPublic: 1, + vk_alpha_1: this.alphaG1.toArray(), + vk_beta_2: this.betaG2.toArray(), + vk_gamma_2: this.gammaG2.toArray(), + vk_delta_2: this.deltaG2.toArray(), + IC: this.gammaAbcG1.map((g1) => g1.toArray()), + }; + } +} + +// ── MoveJWK ── + +/** + * Represents a JSON Web Key (JWK) as stored in the Aptos Move state. + * + * Used to verify OIDC provider signatures on JWT tokens during Keyless + * authentication. + */ +export class MoveJWK extends Serializable { + /** The key ID (`kid`) field from the JWK. */ + public kid: string; + /** The key type (`kty`) field, e.g. `"RSA"`. */ + public kty: string; + /** The algorithm (`alg`) field, e.g. `"RS256"`. */ + public alg: string; + /** The RSA public exponent (`e`) in base64-URL encoding. */ + public e: string; + /** The RSA modulus (`n`) in base64-URL encoding. */ + public n: string; + + /** + * Creates a `MoveJWK`. + * + * @param args - The JWK fields. + * @param args.kid - Key ID. + * @param args.kty - Key type (e.g. `"RSA"`). + * @param args.alg - Algorithm (e.g. `"RS256"`). + * @param args.e - RSA public exponent in base64-URL encoding. + * @param args.n - RSA modulus in base64-URL encoding. + */ + constructor(args: { kid: string; kty: string; alg: string; e: string; n: string }) { + super(); + const { kid, kty, alg, e, n } = args; + this.kid = kid; + this.kty = kty; + this.alg = alg; + this.e = e; + this.n = n; + } + + /** + * BCS-serialises the JWK by writing each field as a string. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeStr(this.kid); + serializer.serializeStr(this.kty); + serializer.serializeStr(this.alg); + serializer.serializeStr(this.e); + serializer.serializeStr(this.n); + } + + /** + * Converts this JWK to a Poseidon field element for use in ZK circuits. + * + * Only RSA-256 (`alg === "RS256"`) keys are supported. The modulus `n` + * is decoded from base64-URL, reversed, chunked into 24-byte scalars, and + * hashed with Poseidon together with the modulus size. + * + * @returns The Poseidon hash of the JWK as a `bigint`. + * @throws If the algorithm is not `"RS256"`. + */ + toScalar(): bigint { + if (this.alg !== "RS256") { + throw new Error("Only RSA 256 is supported for JWK to scalar conversion"); + } + const uint8Array = base64UrlToBytes(this.n); + // RSA-4096 has a 512-byte modulus; reject anything larger to prevent DoS + if (uint8Array.length > 512) { + throw new Error(`RSA modulus too large: ${uint8Array.length} bytes (max 512)`); + } + const modulusBits = uint8Array.length * 8; + const chunks = chunkInto24Bytes(uint8Array.reverse()); + const scalars = chunks.map((chunk) => bytesToBigIntLE(chunk)); + scalars.push(BigInt(modulusBits)); + return poseidonHash(scalars); + } + + /** + * Deserialises a `MoveJWK` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `MoveJWK`. + */ + static deserialize(deserializer: Deserializer): MoveJWK { + const kid = deserializer.deserializeStr(); + const kty = deserializer.deserializeStr(); + const alg = deserializer.deserializeStr(); + const e = deserializer.deserializeStr(); + const n = deserializer.deserializeStr(); + return new MoveJWK({ kid, kty, alg, n, e }); + } +} + +// ── Helpers ── + +function base64UrlToBytes(base64url: string): Uint8Array { + const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/"); + const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "="); + let binary: string; + try { + binary = atob(padded); + } catch { + throw new Error("Invalid base64url encoding"); + } + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i += 1) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} + +function chunkInto24Bytes(data: Uint8Array): Uint8Array[] { + const chunks: Uint8Array[] = []; + for (let i = 0; i < data.length; i += 24) { + const chunk = data.slice(i, Math.min(i + 24, data.length)); + if (chunk.length < 24) { + const paddedChunk = new Uint8Array(24); + paddedChunk.set(chunk); + chunks.push(paddedChunk); + } else { + chunks.push(chunk); + } + } + return chunks; +} + +interface JwtHeader { + kid: string; +} + +/** + * Parses a base64-decoded JWT header JSON string and returns the header + * fields. + * + * @param jwtHeader - The JWT header as a JSON string (already base64-decoded). + * @returns An object with the `kid` field. + * @throws If the header does not contain a `kid` field. + * + * @example + * ```ts + * const header = parseJwtHeader('{"alg":"RS256","kid":"abc123"}'); + * console.log(header.kid); // "abc123" + * ``` + */ +const MAX_JWT_HEADER_JSON_BYTES = 4096; + +export function parseJwtHeader(jwtHeader: string): JwtHeader { + if (jwtHeader.length > MAX_JWT_HEADER_JSON_BYTES) { + throw new Error(`JWT header exceeds maximum size of ${MAX_JWT_HEADER_JSON_BYTES} bytes`); + } + const header = JSON.parse(jwtHeader); + if (typeof header.kid !== "string") { + throw new Error("Invalid JWT header: missing or non-string kid field"); + } + return { kid: header.kid }; +} diff --git a/v10/src/crypto/multi-ed25519.ts b/v10/src/crypto/multi-ed25519.ts new file mode 100644 index 000000000..c8277bd24 --- /dev/null +++ b/v10/src/crypto/multi-ed25519.ts @@ -0,0 +1,321 @@ +import type { Deserializer } from "../bcs/deserializer.js"; +import type { Serializer } from "../bcs/serializer.js"; +import { Ed25519PublicKey, Ed25519Signature } from "./ed25519.js"; +import { AbstractMultiKey } from "./multi-key.js"; +import { createAuthKey, type VerifySignatureArgs } from "./public-key.js"; +import { Signature } from "./signature.js"; +import { SigningScheme } from "./types.js"; + +/** + * A K-of-N multi-signature public key using the legacy MultiEd25519 scheme. + * + * All constituent keys must be Ed25519 keys. The `threshold` specifies how + * many signatures are required to authorise a transaction. + * + * @example + * ```ts + * const multiKey = new MultiEd25519PublicKey({ + * publicKeys: [key1, key2, key3], + * threshold: 2, + * }); + * ``` + */ +export class MultiEd25519PublicKey extends AbstractMultiKey { + /** Maximum number of public keys in a MultiEd25519 key set. */ + static readonly MAX_KEYS = 32; + /** Minimum number of public keys in a MultiEd25519 key set. */ + static readonly MIN_KEYS = 2; + /** Minimum acceptable signature threshold. */ + static readonly MIN_THRESHOLD = 1; + + /** The ordered list of constituent Ed25519 public keys. */ + public readonly publicKeys: Ed25519PublicKey[]; + /** The minimum number of valid signatures required to authenticate. */ + public readonly threshold: number; + + /** + * Creates a `MultiEd25519PublicKey`. + * + * @param args - Configuration object. + * @param args.publicKeys - Between 2 and 32 Ed25519 public keys. + * @param args.threshold - The minimum number of signatures required (1 ≤ + * threshold ≤ `publicKeys.length`). + * @throws If the number of keys is not in the range [2, 32]. + * @throws If `threshold` is not in the range [1, `publicKeys.length`]. + */ + constructor(args: { publicKeys: Ed25519PublicKey[]; threshold: number }) { + const { publicKeys, threshold } = args; + super({ publicKeys }); + + if (publicKeys.length > MultiEd25519PublicKey.MAX_KEYS || publicKeys.length < MultiEd25519PublicKey.MIN_KEYS) { + throw new Error( + `Must have between ${MultiEd25519PublicKey.MIN_KEYS} and ${MultiEd25519PublicKey.MAX_KEYS} public keys, inclusive`, + ); + } + if (threshold < MultiEd25519PublicKey.MIN_THRESHOLD || threshold > publicKeys.length) { + throw new Error( + `Threshold must be between ${MultiEd25519PublicKey.MIN_THRESHOLD} and ${publicKeys.length}, inclusive`, + ); + } + + this.publicKeys = publicKeys; + this.threshold = threshold; + } + + /** + * Returns the signature threshold (the minimum number of signatures + * required to authorise a transaction). + * + * @returns The `threshold` value. + */ + getSignaturesRequired(): number { + return this.threshold; + } + + /** + * Verifies a {@link MultiEd25519Signature} against this multi-key. + * + * The bitmap in the signature specifies which keys signed; each corresponding + * Ed25519 signature is verified individually. + * + * @param args - Object containing `message` and `signature`. + * @returns `true` if all signed signatures are valid and the count meets the + * threshold, `false` if any signature is invalid. + * @throws If `signature` is not a `MultiEd25519Signature`. + * @throws If the bitmap and signature array lengths do not match. + * @throws If fewer signatures are provided than the threshold requires. + */ + verifySignature(args: VerifySignatureArgs): boolean { + const { message, signature } = args; + if (!(signature instanceof MultiEd25519Signature)) return false; + + const indices: number[] = []; + for (let i = 0; i < 4; i += 1) { + for (let j = 0; j < 8; j += 1) { + if ((signature.bitmap[i] & (1 << (7 - j))) !== 0) { + indices.push(i * 8 + j); + } + } + } + + if (indices.length !== signature.signatures.length) return false; + if (indices.length < this.threshold) return false; + + for (let i = 0; i < indices.length; i += 1) { + const publicKey = this.publicKeys[indices[i]]; + if (!publicKey.verifySignature({ message, signature: signature.signatures[i] })) { + return false; + } + } + return true; + } + + /** + * Derives the on-chain authentication key for this multi-key using the + * `MultiEd25519` signing scheme. + * + * @returns The `AccountAddress` representing the authentication key. + */ + authKey(): unknown { + return createAuthKey(SigningScheme.MultiEd25519, this.toUint8Array()); + } + + /** + * Returns the raw byte representation: the concatenation of all public key + * bytes followed by the single-byte threshold. + * + * @returns The multi-key bytes as a `Uint8Array`. + */ + toUint8Array(): Uint8Array { + const bytes = new Uint8Array(this.publicKeys.length * Ed25519PublicKey.LENGTH + 1); + this.publicKeys.forEach((k: Ed25519PublicKey, i: number) => { + bytes.set(k.toUint8Array(), i * Ed25519PublicKey.LENGTH); + }); + bytes[this.publicKeys.length * Ed25519PublicKey.LENGTH] = this.threshold; + return bytes; + } + + /** + * BCS-serialises the multi-key by writing its raw bytes with a length prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.toUint8Array()); + } + + /** + * Deserialises a `MultiEd25519PublicKey` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `MultiEd25519PublicKey`. + */ + static deserialize(deserializer: Deserializer): MultiEd25519PublicKey { + const bytes = deserializer.deserializeBytes(); + if (bytes.length < Ed25519PublicKey.LENGTH + 1) { + throw new Error( + `MultiEd25519PublicKey bytes too short: expected at least ${Ed25519PublicKey.LENGTH + 1}, got ${bytes.length}`, + ); + } + const keyBytes = bytes.length - 1; + if (keyBytes % Ed25519PublicKey.LENGTH !== 0) { + throw new Error( + `MultiEd25519PublicKey key bytes are not aligned: ${keyBytes} is not a multiple of ${Ed25519PublicKey.LENGTH}`, + ); + } + const threshold = bytes[bytes.length - 1]; + const keys: Ed25519PublicKey[] = []; + for (let i = 0; i < keyBytes; i += Ed25519PublicKey.LENGTH) { + keys.push(new Ed25519PublicKey(bytes.slice(i, i + Ed25519PublicKey.LENGTH))); + } + return new MultiEd25519PublicKey({ publicKeys: keys, threshold }); + } +} + +/** + * A multi-signature for the legacy `MultiEd25519` scheme, consisting of + * individual Ed25519 signatures and a bitmap indicating which keys signed. + * + * @example + * ```ts + * const multiSig = new MultiEd25519Signature({ + * signatures: [sig0, sig1], + * bitmap: MultiEd25519Signature.createBitmap({ bits: [0, 1] }), + * }); + * ``` + */ +export class MultiEd25519Signature extends Signature { + /** Maximum number of signatures supported. */ + static MAX_SIGNATURES_SUPPORTED = 32; + /** Byte length of the bitmap. */ + static BITMAP_LEN: number = 4; + + /** The individual Ed25519 signatures from each signer. */ + public readonly signatures: Ed25519Signature[]; + /** A 4-byte bitmap indicating which key indices signed. */ + public readonly bitmap: Uint8Array; + + /** + * Creates a `MultiEd25519Signature`. + * + * @param args - Configuration object. + * @param args.signatures - The individual Ed25519 signatures. + * @param args.bitmap - Either a pre-built 4-byte `Uint8Array` or an array of + * signer index numbers from which the bitmap is constructed. + * @throws If the number of signatures exceeds `MAX_SIGNATURES_SUPPORTED`. + * @throws If the bitmap length is not 4 bytes. + */ + constructor(args: { signatures: Ed25519Signature[]; bitmap: Uint8Array | number[] }) { + super(); + const { signatures, bitmap } = args; + + if (signatures.length > MultiEd25519Signature.MAX_SIGNATURES_SUPPORTED) { + throw new Error( + `The number of signatures cannot be greater than ${MultiEd25519Signature.MAX_SIGNATURES_SUPPORTED}`, + ); + } + this.signatures = signatures; + + if (!(bitmap instanceof Uint8Array)) { + this.bitmap = MultiEd25519Signature.createBitmap({ bits: bitmap }); + } else if (bitmap.length !== MultiEd25519Signature.BITMAP_LEN) { + throw new Error(`"bitmap" length should be ${MultiEd25519Signature.BITMAP_LEN}`); + } else { + this.bitmap = bitmap; + } + } + + /** + * Returns the raw byte representation: the concatenation of all signature + * bytes followed by the 4-byte bitmap. + * + * @returns The multi-signature bytes as a `Uint8Array`. + */ + toUint8Array(): Uint8Array { + const bytes = new Uint8Array(this.signatures.length * Ed25519Signature.LENGTH + MultiEd25519Signature.BITMAP_LEN); + this.signatures.forEach((k: Ed25519Signature, i: number) => { + bytes.set(k.toUint8Array(), i * Ed25519Signature.LENGTH); + }); + bytes.set(this.bitmap, this.signatures.length * Ed25519Signature.LENGTH); + return bytes; + } + + /** + * BCS-serialises the multi-signature by writing its raw bytes with a length + * prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.toUint8Array()); + } + + /** + * Deserialises a `MultiEd25519Signature` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `MultiEd25519Signature`. + */ + static deserialize(deserializer: Deserializer): MultiEd25519Signature { + const bytes = deserializer.deserializeBytes(); + if (bytes.length < MultiEd25519Signature.BITMAP_LEN) { + throw new Error( + `MultiEd25519Signature bytes too short: expected at least ${MultiEd25519Signature.BITMAP_LEN}, got ${bytes.length}`, + ); + } + const sigBytes = bytes.length - MultiEd25519Signature.BITMAP_LEN; + if (sigBytes % Ed25519Signature.LENGTH !== 0) { + throw new Error( + `MultiEd25519Signature signature bytes are not aligned: ${sigBytes} is not a multiple of ${Ed25519Signature.LENGTH}`, + ); + } + const bitmap = bytes.slice(bytes.length - MultiEd25519Signature.BITMAP_LEN); + const signatures: Ed25519Signature[] = []; + for (let i = 0; i < sigBytes; i += Ed25519Signature.LENGTH) { + signatures.push(new Ed25519Signature(bytes.slice(i, i + Ed25519Signature.LENGTH))); + } + return new MultiEd25519Signature({ signatures, bitmap }); + } + + /** + * Builds a 4-byte bitmap from an array of signer index numbers. + * + * Indices must be in strictly ascending order. + * + * @param args - Object containing the `bits` array of signer indices. + * @returns A 4-byte `Uint8Array` with the corresponding bits set. + * @throws If any index is ≥ `MAX_SIGNATURES_SUPPORTED`. + * @throws If any index appears more than once. + * @throws If the indices are not in strictly ascending order. + * + * @example + * ```ts + * const bitmap = MultiEd25519Signature.createBitmap({ bits: [0, 2] }); + * ``` + */ + static createBitmap(args: { bits: number[] }): Uint8Array { + const { bits } = args; + const firstBitInByte = 128; + const bitmap = new Uint8Array([0, 0, 0, 0]); + const dupCheckSet = new Set(); + + bits.forEach((bit: number, index) => { + if (bit >= MultiEd25519Signature.MAX_SIGNATURES_SUPPORTED) { + throw new Error(`Cannot have a signature larger than ${MultiEd25519Signature.MAX_SIGNATURES_SUPPORTED - 1}.`); + } + if (dupCheckSet.has(bit)) { + throw new Error("Duplicate bits detected."); + } + if (index > 0 && bit <= bits[index - 1]) { + throw new Error("The bits need to be sorted in ascending order."); + } + dupCheckSet.add(bit); + const byteOffset = Math.floor(bit / 8); + let byte = bitmap[byteOffset]; + byte |= firstBitInByte >> (bit % 8); + bitmap[byteOffset] = byte; + }); + + return bitmap; + } +} diff --git a/v10/src/crypto/multi-key.ts b/v10/src/crypto/multi-key.ts new file mode 100644 index 000000000..8802fc129 --- /dev/null +++ b/v10/src/crypto/multi-key.ts @@ -0,0 +1,406 @@ +import type { Deserializer } from "../bcs/deserializer.js"; +import type { Serializer } from "../bcs/serializer.js"; +import type { HexInput } from "../hex/index.js"; +import { AccountPublicKey, createAuthKey, type PublicKey } from "./public-key.js"; +import { Signature } from "./signature.js"; +import { AnyPublicKey, AnySignature } from "./single-key.js"; +import { AnyPublicKeyVariant, SigningScheme } from "./types.js"; + +function bitCount(byte: number) { + let n = byte; + n -= (n >> 1) & 0x55555555; + n = (n & 0x33333333) + ((n >> 2) & 0x33333333); + return (((n + (n >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; +} + +const MAX_NUM_KEYLESS_PUBLIC_FOR_MULTI_KEY = 3; + +/** + * Abstract base class shared by {@link MultiKey} and {@link MultiEd25519PublicKey}. + * + * Provides bitmap construction and key-index lookup utilities used by both + * multi-signature scheme implementations. + */ +export abstract class AbstractMultiKey extends AccountPublicKey { + /** The ordered list of public keys that make up this multi-key set. */ + publicKeys: PublicKey[]; + + /** + * Creates an `AbstractMultiKey` with the given set of public keys. + * + * @param args - Object containing the `publicKeys` array. + */ + constructor(args: { publicKeys: PublicKey[] }) { + super(); + this.publicKeys = args.publicKeys; + } + + /** + * Constructs a 4-byte bitmap where each set bit corresponds to a signer + * index within `publicKeys`. + * + * @param args - Object with a `bits` array of signer indices (0-based). + * @returns A 4-byte `Uint8Array` bitmap with the specified bits set. + * @throws If any index exceeds the number of public keys. + * @throws If the same index appears more than once. + * + * @example + * ```ts + * const bitmap = multiKey.createBitmap({ bits: [0, 2] }); + * ``` + */ + createBitmap(args: { bits: number[] }): Uint8Array { + const { bits } = args; + const firstBitInByte = 128; + const bitmap = new Uint8Array([0, 0, 0, 0]); + const dupCheckSet = new Set(); + + bits.forEach((bit: number) => { + if (bit >= this.publicKeys.length) { + throw new Error(`Signature index ${bit} is out of public keys range ${this.publicKeys.length}.`); + } + if (bit >= 32) { + throw new Error(`Signature index ${bit} exceeds maximum bitmap capacity of 32.`); + } + if (dupCheckSet.has(bit)) { + throw new Error(`Duplicate bit ${bit} detected.`); + } + dupCheckSet.add(bit); + const byteOffset = Math.floor(bit / 8); + let byte = bitmap[byteOffset]; + byte |= firstBitInByte >> (bit % 8); + bitmap[byteOffset] = byte; + }); + + return bitmap; + } + + /** + * Returns the index of a public key within this multi-key set. + * + * @param publicKey - The public key to look up. + * @returns The 0-based index of the key. + * @throws If the public key is not found in the set. + */ + getIndex(publicKey: PublicKey): number { + const target = publicKey.toUint8Array(); + const index = this.publicKeys.findIndex((pk) => { + const other = pk.toUint8Array(); + if (other.length !== target.length) return false; + for (let i = 0; i < target.length; i++) { + if (other[i] !== target[i]) return false; + } + return true; + }); + if (index !== -1) return index; + throw new Error(`Public key ${publicKey} not found in multi key set ${this.publicKeys}`); + } + + /** + * Returns the number of signatures required to authorise a transaction. + */ + abstract getSignaturesRequired(): number; +} + +/** + * A K-of-N multi-key public key that supports any combination of signing + * schemes supported by {@link AnyPublicKey} (Ed25519, Secp256k1, Keyless, etc.). + * + * On-chain this corresponds to the `MultiKey` authenticator introduced in + * AIP-55. + * + * @example + * ```ts + * const multiKey = new MultiKey({ + * publicKeys: [ed25519Key, secp256k1Key], + * signaturesRequired: 2, + * }); + * ``` + */ +export class MultiKey extends AbstractMultiKey { + /** The ordered list of {@link AnyPublicKey} wrappers. */ + public readonly publicKeys: AnyPublicKey[]; + /** The minimum number of valid signatures required to authenticate. */ + public readonly signaturesRequired: number; + + /** + * Creates a `MultiKey` from an array of public keys and a required- + * signatures threshold. + * + * @param args - Configuration object. + * @param args.publicKeys - The public keys that form the multi-key set. + * Each key may be any concrete {@link PublicKey}; they are wrapped in + * {@link AnyPublicKey} automatically if not already. + * @param args.signaturesRequired - The minimum number of signatures needed + * to authorise a transaction (must be ≥ 1 and ≤ `publicKeys.length`). + * @throws If `signaturesRequired` is less than 1. + * @throws If `publicKeys.length` is less than `signaturesRequired`. + * @throws If `signaturesRequired > 3` and the set contains more than 3 + * Keyless public keys. + */ + /** Maximum number of public keys in a MultiKey set (constrained by 4-byte bitmap). */ + static readonly MAX_KEYS = 32; + + constructor(args: { publicKeys: Array; signaturesRequired: number }) { + const { publicKeys, signaturesRequired } = args; + super({ publicKeys }); + + if (publicKeys.length > MultiKey.MAX_KEYS) { + throw new Error(`MultiKey supports at most ${MultiKey.MAX_KEYS} public keys, received ${publicKeys.length}`); + } + if (signaturesRequired < 1) { + throw new Error("The number of required signatures needs to be greater than 0"); + } + if (publicKeys.length < signaturesRequired) { + throw new Error( + `Provided ${publicKeys.length} public keys is smaller than the ${signaturesRequired} required signatures`, + ); + } + + this.publicKeys = publicKeys.map((pk) => (pk instanceof AnyPublicKey ? pk : new AnyPublicKey(pk))); + + if (signaturesRequired > MAX_NUM_KEYLESS_PUBLIC_FOR_MULTI_KEY) { + const keylessCount = this.publicKeys.filter( + (pk) => pk.variant === AnyPublicKeyVariant.Keyless || pk.variant === AnyPublicKeyVariant.FederatedKeyless, + ).length; + if (keylessCount > MAX_NUM_KEYLESS_PUBLIC_FOR_MULTI_KEY) { + throw new Error( + `Construction of MultiKey with more than ${MAX_NUM_KEYLESS_PUBLIC_FOR_MULTI_KEY} keyless public keys is not allowed when signaturesRequired is greater than ${MAX_NUM_KEYLESS_PUBLIC_FOR_MULTI_KEY}.`, + ); + } + } + + this.signaturesRequired = signaturesRequired; + } + + /** + * Returns the minimum number of signatures required to authorise a + * transaction with this multi-key. + * + * @returns The `signaturesRequired` threshold. + */ + getSignaturesRequired(): number { + return this.signaturesRequired; + } + + /** + * Verifies a {@link MultiKeySignature} against this multi-key public key. + * + * The number of signatures in `signature` must equal `signaturesRequired`, + * and each signature must be valid for its corresponding public key as + * indicated by the bitmap. + * + * @param args - Object containing `message` and `signature`. + * @returns `true` if all required signatures are valid, `false` otherwise. + * @throws If the signature count does not match `signaturesRequired`. + */ + verifySignature(args: { message: HexInput; signature: MultiKeySignature }): boolean { + const { message, signature } = args; + if (signature.signatures.length !== this.signaturesRequired) { + return false; + } + const signerIndices = signature.bitMapToSignerIndices(); + for (let i = 0; i < signature.signatures.length; i += 1) { + const singleSignature = signature.signatures[i]; + const publicKey = this.publicKeys[signerIndices[i]]; + if (!publicKey.verifySignature({ message, signature: singleSignature })) { + return false; + } + } + return true; + } + + /** + * Derives the on-chain authentication key for this multi-key using the + * `MultiKey` signing scheme. + * + * @returns The `AccountAddress` representing the authentication key. + */ + authKey(): unknown { + return createAuthKey(SigningScheme.MultiKey, this.bcsToBytes()); + } + + /** + * BCS-serialises the multi-key by writing the public key vector followed by + * the `signaturesRequired` byte. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeVector(this.publicKeys); + serializer.serializeU8(this.signaturesRequired); + } + + /** + * Deserialises a `MultiKey` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `MultiKey`. + */ + static deserialize(deserializer: Deserializer): MultiKey { + const keys = deserializer.deserializeVector(AnyPublicKey); + const signaturesRequired = deserializer.deserializeU8(); + return new MultiKey({ publicKeys: keys, signaturesRequired }); + } + + /** + * Returns the index of a public key within this multi-key set. + * + * The key is wrapped in {@link AnyPublicKey} before lookup if it is not + * already an `AnyPublicKey` instance. + * + * @param publicKey - The public key to locate. + * @returns The 0-based index of the key. + * @throws If the key is not found. + */ + getIndex(publicKey: PublicKey): number { + const anyPublicKey = publicKey instanceof AnyPublicKey ? publicKey : new AnyPublicKey(publicKey); + return super.getIndex(anyPublicKey); + } +} + +/** + * A multi-signature over a single message, produced by K-of-N signers from a + * {@link MultiKey} key set. + * + * The `bitmap` field encodes which keys in the set signed; the `signatures` + * array contains the corresponding signatures in the same order. + * + * @example + * ```ts + * const sig = new MultiKeySignature({ + * signatures: [anySignature1, anySignature2], + * bitmap: multiKey.createBitmap({ bits: [0, 1] }), + * }); + * ``` + */ +export class MultiKeySignature extends Signature { + /** The byte length of the bitmap (supports up to 32 signers). */ + static BITMAP_LEN: number = 4; + /** Maximum number of signers supported by the bitmap. */ + static MAX_SIGNATURES_SUPPORTED = MultiKeySignature.BITMAP_LEN * 8; + + /** The individual signatures from each signer. */ + public readonly signatures: AnySignature[]; + /** A 4-byte bitmap indicating which key indices signed. */ + public readonly bitmap: Uint8Array; + + /** + * Creates a `MultiKeySignature`. + * + * @param args - Configuration object. + * @param args.signatures - The individual signatures. Each may be a + * concrete {@link Signature} or an {@link AnySignature}; plain signatures + * are wrapped automatically. + * @param args.bitmap - Either a pre-built 4-byte `Uint8Array` or an array + * of signer index numbers from which the bitmap is constructed. + * @throws If the number of signatures exceeds {@link MAX_SIGNATURES_SUPPORTED}. + * @throws If the bitmap length is not 4 bytes. + * @throws If the number of set bits in the bitmap does not match the number + * of signatures. + */ + constructor(args: { signatures: Array; bitmap: Uint8Array | number[] }) { + super(); + const { signatures, bitmap } = args; + + if (signatures.length > MultiKeySignature.MAX_SIGNATURES_SUPPORTED) { + throw new Error(`The number of signatures cannot be greater than ${MultiKeySignature.MAX_SIGNATURES_SUPPORTED}`); + } + + this.signatures = signatures.map((sig) => (sig instanceof AnySignature ? sig : new AnySignature(sig))); + + if (!(bitmap instanceof Uint8Array)) { + this.bitmap = MultiKeySignature.createBitmap({ bits: bitmap }); + } else if (bitmap.length !== MultiKeySignature.BITMAP_LEN) { + throw new Error(`"bitmap" length should be ${MultiKeySignature.BITMAP_LEN}`); + } else { + this.bitmap = bitmap; + } + + const nSignatures = this.bitmap.reduce((acc, byte) => acc + bitCount(byte), 0); + if (nSignatures !== this.signatures.length) { + throw new Error(`Expecting ${nSignatures} signatures from the bitmap, but got ${this.signatures.length}`); + } + } + + /** + * Builds a 4-byte bitmap from an array of signer index numbers. + * + * @param args - Object containing the `bits` array of signer indices. + * @returns A 4-byte `Uint8Array` with the corresponding bits set. + * @throws If any index is ≥ {@link MAX_SIGNATURES_SUPPORTED}. + * @throws If any index appears more than once. + * + * @example + * ```ts + * const bitmap = MultiKeySignature.createBitmap({ bits: [0, 3] }); + * ``` + */ + static createBitmap(args: { bits: number[] }): Uint8Array { + const { bits } = args; + const firstBitInByte = 128; + const bitmap = new Uint8Array([0, 0, 0, 0]); + const dupCheckSet = new Set(); + + bits.forEach((bit: number) => { + if (bit >= MultiKeySignature.MAX_SIGNATURES_SUPPORTED) { + throw new Error(`Cannot have a signature larger than ${MultiKeySignature.MAX_SIGNATURES_SUPPORTED - 1}.`); + } + if (dupCheckSet.has(bit)) { + throw new Error("Duplicate bits detected."); + } + dupCheckSet.add(bit); + const byteOffset = Math.floor(bit / 8); + let byte = bitmap[byteOffset]; + byte |= firstBitInByte >> (bit % 8); + bitmap[byteOffset] = byte; + }); + + return bitmap; + } + + /** + * Decodes the bitmap into an ordered list of signer indices. + * + * @returns An array of 0-based key indices corresponding to the set bits in + * the bitmap, in ascending order. + */ + bitMapToSignerIndices(): number[] { + const signerIndices: number[] = []; + for (let i = 0; i < this.bitmap.length; i += 1) { + const byte = this.bitmap[i]; + for (let bit = 0; bit < 8; bit += 1) { + if ((byte & (128 >> bit)) !== 0) { + signerIndices.push(i * 8 + bit); + } + } + } + return signerIndices; + } + + /** + * BCS-serialises the multi-key signature by writing the signature vector + * followed by the bitmap bytes. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeVector(this.signatures); + serializer.serializeBytes(this.bitmap); + } + + /** + * Deserialises a `MultiKeySignature` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `MultiKeySignature`. + */ + static deserialize(deserializer: Deserializer): MultiKeySignature { + const signatures = deserializer.deserializeVector(AnySignature); + const bitmap = deserializer.deserializeBytes(); + if (bitmap.length !== MultiKeySignature.BITMAP_LEN) { + throw new Error(`MultiKeySignature bitmap must be ${MultiKeySignature.BITMAP_LEN} bytes, got ${bitmap.length}`); + } + return new MultiKeySignature({ signatures, bitmap }); + } +} diff --git a/v10/src/crypto/poseidon.ts b/v10/src/crypto/poseidon.ts new file mode 100644 index 000000000..280f12be5 --- /dev/null +++ b/v10/src/crypto/poseidon.ts @@ -0,0 +1,209 @@ +import { + poseidon1, + poseidon2, + poseidon3, + poseidon4, + poseidon5, + poseidon6, + poseidon7, + poseidon8, + poseidon9, + poseidon10, + poseidon11, + poseidon12, + poseidon13, + poseidon14, + poseidon15, + poseidon16, +} from "poseidon-lite"; + +const numInputsToPoseidonFunc = [ + poseidon1, + poseidon2, + poseidon3, + poseidon4, + poseidon5, + poseidon6, + poseidon7, + poseidon8, + poseidon9, + poseidon10, + poseidon11, + poseidon12, + poseidon13, + poseidon14, + poseidon15, + poseidon16, +]; + +const BYTES_PACKED_PER_SCALAR = 31; +const MAX_NUM_INPUT_SCALARS = 16; +const MAX_NUM_INPUT_BYTES = (MAX_NUM_INPUT_SCALARS - 1) * BYTES_PACKED_PER_SCALAR; + +/** + * Hashes a UTF-8 string to a finite-field element using Poseidon, padding it + * to `maxSizeBytes` before hashing. + * + * The string is encoded as UTF-8 bytes, padded with zeros to `maxSizeBytes`, + * packed into 31-byte scalars, and then Poseidon-hashed together with the + * original byte length. + * + * @param str - The string to hash. + * @param maxSizeBytes - The maximum allowed (and padded-to) byte length of the + * string. + * @returns The Poseidon hash as a `bigint` field element. + * @throws If the UTF-8 encoding of `str` exceeds `maxSizeBytes`. + * + * @example + * ```ts + * const fieldElement = hashStrToField("user@example.com", MAX_UID_VAL_BYTES); + * ``` + */ +const TEXT_ENCODER = new TextEncoder(); + +export function hashStrToField(str: string, maxSizeBytes: number): bigint { + const strBytes = TEXT_ENCODER.encode(str); + return hashBytesWithLen(strBytes, maxSizeBytes); +} + +function hashBytesWithLen(bytes: Uint8Array, maxSizeBytes: number): bigint { + if (bytes.length > maxSizeBytes) { + throw new Error(`Inputted bytes of length ${bytes.length} is longer than ${maxSizeBytes}`); + } + const packed = padAndPackBytesWithLen(bytes, maxSizeBytes); + return poseidonHash(packed); +} + +function padAndPackBytesNoLen(bytes: Uint8Array, maxSizeBytes: number): bigint[] { + if (bytes.length > maxSizeBytes) { + throw new Error(`Input bytes of length ${bytes.length} is longer than ${maxSizeBytes}`); + } + const paddedStrBytes = padUint8ArrayWithZeros(bytes, maxSizeBytes); + return packBytes(paddedStrBytes); +} + +/** + * Pads `bytes` with zeros to `maxSizeBytes`, packs them into 31-byte little- + * endian scalars, and appends the original byte length as an additional scalar. + * + * @param bytes - The byte array to pad and pack. + * @param maxSizeBytes - The size to pad to (and the maximum permitted length). + * @returns An array of `bigint` scalars suitable for {@link poseidonHash}. + * @throws If `bytes.length` exceeds `maxSizeBytes`. + */ +export function padAndPackBytesWithLen(bytes: Uint8Array, maxSizeBytes: number): bigint[] { + if (bytes.length > maxSizeBytes) { + throw new Error(`Input bytes of length ${bytes.length} is longer than ${maxSizeBytes}`); + } + const packed = padAndPackBytesNoLen(bytes, maxSizeBytes); + packed.push(BigInt(bytes.length)); + return packed; +} + +function packBytes(bytes: Uint8Array): bigint[] { + if (bytes.length > MAX_NUM_INPUT_BYTES) { + throw new Error(`Can't pack more than ${MAX_NUM_INPUT_BYTES}. Was given ${bytes.length} bytes`); + } + return chunkUint8Array(bytes, BYTES_PACKED_PER_SCALAR).map((chunk) => bytesToBigIntLE(chunk)); +} + +function chunkUint8Array(array: Uint8Array, chunkSize: number): Uint8Array[] { + const result: Uint8Array[] = []; + for (let i = 0; i < array.length; i += chunkSize) { + result.push(array.subarray(i, i + chunkSize)); + } + return result; +} + +/** + * Interprets a `Uint8Array` as a little-endian unsigned integer and returns it + * as a `bigint`. + * + * @param bytes - The byte array to convert. + * @returns The little-endian `bigint` value. + * + * @example + * ```ts + * bytesToBigIntLE(new Uint8Array([1, 0])); // 1n + * bytesToBigIntLE(new Uint8Array([0, 1])); // 256n + * ``` + */ +const BIGINT_0 = BigInt(0); +const BIGINT_8 = BigInt(8); +const BIGINT_0xFF = BigInt(0xff); +const BYTE_TO_BIGINT = Array.from({ length: 256 }, (_, i) => BigInt(i)); + +export function bytesToBigIntLE(bytes: Uint8Array): bigint { + let result = BIGINT_0; + for (let i = bytes.length - 1; i >= 0; i -= 1) { + result = (result << BIGINT_8) | BYTE_TO_BIGINT[bytes[i]]; + } + return result; +} + +/** + * Encodes a `bigint` (or `number`) as a little-endian `Uint8Array` of exactly + * `length` bytes. + * + * @param value - The integer value to encode. + * @param length - The desired byte length of the output. + * @returns A `Uint8Array` of `length` bytes in little-endian order. + * + * @example + * ```ts + * bigIntToBytesLE(256n, 4); // Uint8Array([0, 1, 0, 0]) + * ``` + */ +export function bigIntToBytesLE(value: bigint | number, length: number): Uint8Array { + let val = BigInt(value); + if (val < 0n) { + throw new Error(`bigIntToBytesLE does not support negative values, received ${val}`); + } + const maxVal = (1n << BigInt(length * 8)) - 1n; + if (val > maxVal) { + throw new Error(`Value ${val} does not fit in ${length} bytes (max ${maxVal})`); + } + const bytes = new Uint8Array(length); + for (let i = 0; i < length; i += 1) { + bytes[i] = Number(val & BIGINT_0xFF); + val >>= BIGINT_8; + } + return bytes; +} + +function padUint8ArrayWithZeros(inputArray: Uint8Array, paddedSize: number): Uint8Array { + if (paddedSize < inputArray.length) { + throw new Error("Padded size must be greater than or equal to the input array size."); + } + const paddedArray = new Uint8Array(paddedSize); + paddedArray.set(inputArray); + return paddedArray; +} + +/** + * Computes the Poseidon hash of up to 16 field-element inputs. + * + * Selects the correct arity-specific Poseidon function from `poseidon-lite` + * based on the number of inputs. + * + * @param inputs - An array of 1–16 numeric, `bigint`, or string field-element + * inputs. + * @returns The Poseidon hash as a `bigint` field element. + * @throws If `inputs` is empty or has more than 16 elements. + * + * @example + * ```ts + * poseidonHash([1n, 2n, 3n]); // bigint hash of three field elements + * ``` + */ +export function poseidonHash(inputs: (number | bigint | string)[]): bigint { + if (inputs.length === 0) { + throw new Error("Poseidon hash requires at least 1 input"); + } + if (inputs.length > numInputsToPoseidonFunc.length) { + throw new Error( + `Unable to hash input of length ${inputs.length}. Max input length is ${numInputsToPoseidonFunc.length}`, + ); + } + return numInputsToPoseidonFunc[inputs.length - 1](inputs); +} diff --git a/v10/src/crypto/private-key.ts b/v10/src/crypto/private-key.ts new file mode 100644 index 000000000..403bceb96 --- /dev/null +++ b/v10/src/crypto/private-key.ts @@ -0,0 +1,111 @@ +import type { HexInput } from "../hex/index.js"; +import { Hex } from "../hex/index.js"; +import type { PublicKey } from "./public-key.js"; +import type { Signature } from "./signature.js"; +import { PrivateKeyVariants } from "./types.js"; + +/** + * Interface that every private key implementation must satisfy. + * + * A private key can sign messages, derive its corresponding public key, and + * export its raw bytes. + */ +export interface PrivateKey { + /** + * Signs the given message and returns a signature. + * + * @param message - The message to sign, as a hex string or `Uint8Array`. + * @returns The resulting {@link Signature}. + */ + sign(message: HexInput): Signature; + + /** + * Derives and returns the public key that corresponds to this private key. + * + * @returns The associated {@link PublicKey}. + */ + publicKey(): PublicKey; + + /** + * Returns the raw byte representation of the private key. + * + * @returns The private key as a `Uint8Array`. + */ + toUint8Array(): Uint8Array; +} + +/** + * Utility helpers shared across all private key implementations. + * + * These helpers handle AIP-80 prefix formatting and parsing so that private + * keys can be safely serialised and deserialised as human-readable strings. + */ +// Static utilities on the PrivateKey namespace +export const PrivateKeyUtils = { + /** + * AIP-80 string prefixes keyed by {@link PrivateKeyVariants}. + * + * A private key serialised as a string is always prefixed with one of these + * values to unambiguously identify its algorithm (e.g. `"ed25519-priv-"`). + */ + AIP80_PREFIXES: { + [PrivateKeyVariants.Ed25519]: "ed25519-priv-", + [PrivateKeyVariants.Secp256k1]: "secp256k1-priv-", + [PrivateKeyVariants.Secp256r1]: "secp256r1-priv-", + } as Record, + + /** + * Formats a private key value as an AIP-80 compliant string. + * + * If the value already contains the correct AIP-80 prefix only the hex + * portion is kept; otherwise the raw hex is used directly. + * + * @param privateKey - The private key bytes or string to format. + * @param type - The {@link PrivateKeyVariants} algorithm identifier. + * @returns An AIP-80 string of the form `""`. + * + * @example + * ```ts + * PrivateKeyUtils.formatPrivateKey("0xabc123", PrivateKeyVariants.Ed25519); + * // "ed25519-priv-0xabc123" + * ``` + */ + formatPrivateKey(privateKey: HexInput, type: PrivateKeyVariants): string { + const aip80Prefix = PrivateKeyUtils.AIP80_PREFIXES[type]; + let formattedPrivateKey = privateKey; + if (typeof formattedPrivateKey === "string" && formattedPrivateKey.startsWith(aip80Prefix)) { + formattedPrivateKey = formattedPrivateKey.slice(aip80Prefix.length); + } + return `${aip80Prefix}${Hex.fromHexInput(formattedPrivateKey).toString()}`; + }, + + /** + * Parses a `HexInput` value (raw hex or AIP-80 string) into a {@link Hex} + * object, stripping any AIP-80 prefix as appropriate. + * + * When `strict` is `true` the input **must** carry the correct AIP-80 prefix; + * passing a bare hex string throws. When `strict` is `false` (the default) a + * bare hex string is accepted without validation. + * + * @param value - The raw or AIP-80 encoded private key string/bytes. + * @param type - The expected {@link PrivateKeyVariants} algorithm. + * @param strict - Whether to require an AIP-80 prefix. + * @returns A {@link Hex} object containing only the raw key bytes. + * @throws If `strict` is `true` and the input does not have the expected prefix. + */ + parseHexInput(value: HexInput, type: PrivateKeyVariants, strict?: boolean): Hex { + const aip80Prefix = PrivateKeyUtils.AIP80_PREFIXES[type]; + + if (typeof value === "string") { + if (value.startsWith(aip80Prefix)) { + return Hex.fromHexString(value.slice(aip80Prefix.length)); + } + if (strict) { + throw new Error("Invalid HexString input while parsing private key. Must AIP-80 compliant string."); + } + return Hex.fromHexInput(value); + } + + return Hex.fromHexInput(value); + }, +}; diff --git a/v10/src/crypto/proof.ts b/v10/src/crypto/proof.ts new file mode 100644 index 000000000..fbdc634cd --- /dev/null +++ b/v10/src/crypto/proof.ts @@ -0,0 +1,10 @@ +import { Serializable } from "../bcs/serializer.js"; + +/** + * Abstract base class for zero-knowledge proofs. + * + * Concrete implementations (e.g. {@link Groth16Zkp}) extend this class and + * provide a `serialize` method that encodes the proof data for BCS + * transmission. + */ +export abstract class Proof extends Serializable {} diff --git a/v10/src/crypto/public-key.ts b/v10/src/crypto/public-key.ts new file mode 100644 index 000000000..2f1fc230d --- /dev/null +++ b/v10/src/crypto/public-key.ts @@ -0,0 +1,107 @@ +import { Serializable } from "../bcs/serializer.js"; +import type { HexInput } from "../hex/index.js"; +import { Hex } from "../hex/index.js"; +import type { Signature } from "./signature.js"; + +/** + * Arguments passed to {@link PublicKey.verifySignature}. + */ +export interface VerifySignatureArgs { + /** The original message that was signed, as hex bytes or a `HexInput`. */ + message: HexInput; + /** The signature to verify against this public key. */ + signature: Signature; +} + +/** + * Abstract base class for all public keys in the SDK. + * + * Concrete subclasses must implement {@link verifySignature} and the BCS + * `serialize` method inherited from {@link Serializable}. + */ +export abstract class PublicKey extends Serializable { + /** + * Verifies that a signature was produced by the private key corresponding + * to this public key. + * + * @param args - An object containing the `message` and `signature` to verify. + * @returns `true` if the signature is valid for the given message, `false` otherwise. + */ + abstract verifySignature(args: VerifySignatureArgs): boolean; + + /** + * Returns the raw byte representation of the public key. + * + * The default implementation serialises via BCS. Subclasses may override + * this to return a more efficient encoding. + * + * @returns The public key as a `Uint8Array`. + */ + toUint8Array(): Uint8Array { + return this.bcsToBytes(); + } + + /** + * Returns a hex-encoded string representation of the public key. + * + * @returns A `0x`-prefixed hex string of the public key bytes. + */ + toString(): string { + const bytes = this.toUint8Array(); + return Hex.fromHexInput(bytes).toString(); + } +} + +// ── Auth key derivation (registered by core module to break circular dep) ── + +type AuthKeyFactory = (scheme: number, publicKeyBytes: Uint8Array) => unknown; +let _authKeyFactory: AuthKeyFactory | null = null; + +/** + * Registers the authentication-key factory function used by {@link createAuthKey}. + * + * This function is called once during module initialisation by the `core` + * package to inject the `AccountAddress`-based factory without creating a + * circular dependency between the `crypto` and `core` packages. + * + * @param factory - A function that accepts a signing `scheme` discriminant and + * the raw `publicKeyBytes`, and returns an `AccountAddress` (or equivalent). + */ +export function registerAuthKeyFactory(factory: AuthKeyFactory): void { + _authKeyFactory = factory; +} + +/** + * Creates an authentication key for the given signing scheme and public key bytes. + * + * Delegates to the factory registered via {@link registerAuthKeyFactory}. + * Throws if the factory has not been registered yet (i.e. the `core` module + * has not been imported). + * + * @param scheme - The numeric {@link SigningScheme} discriminant. + * @param publicKeyBytes - The raw bytes of the public key. + * @returns An `AccountAddress` representing the authentication key. + * @throws If the auth-key factory has not been registered. + */ +export function createAuthKey(scheme: number, publicKeyBytes: Uint8Array): unknown { + if (!_authKeyFactory) { + throw new Error("AuthKey factory not registered. Import core module first."); + } + return _authKeyFactory(scheme, publicKeyBytes); +} + +/** + * Abstract base class for public keys that can derive an on-chain + * authentication key (i.e. keys that can be the root of an Aptos account). + * + * Extends {@link PublicKey} with an {@link authKey} method. + */ +export abstract class AccountPublicKey extends PublicKey { + /** + * Derives the on-chain authentication key associated with this public key. + * + * @returns The `AccountAddress` that serves as the authentication key for + * this public key. + */ + abstract authKey(): unknown; +} diff --git a/v10/src/crypto/secp256k1.ts b/v10/src/crypto/secp256k1.ts new file mode 100644 index 000000000..2d1a2a03f --- /dev/null +++ b/v10/src/crypto/secp256k1.ts @@ -0,0 +1,396 @@ +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha3_256 } from "@noble/hashes/sha3.js"; +import { HDKey } from "@scure/bip32"; +import type { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; +import type { HexInput } from "../hex/index.js"; +import { Hex } from "../hex/index.js"; +import { isValidBIP44Path, mnemonicToSeed } from "./hd-key.js"; +import { type PrivateKey, PrivateKeyUtils } from "./private-key.js"; +import { PublicKey, type VerifySignatureArgs } from "./public-key.js"; +import { Signature } from "./signature.js"; +import { PrivateKeyVariants } from "./types.js"; +import { convertSigningMessage } from "./utils.js"; + +/** + * Represents a secp256k1 public key (65-byte uncompressed or 33-byte compressed). + * + * Internally the key is always stored in uncompressed form (65 bytes). + * Compressed keys provided to the constructor are expanded automatically. + * + * @example + * ```ts + * const privateKey = Secp256k1PrivateKey.generate(); + * const publicKey = privateKey.publicKey(); + * const isValid = publicKey.verifySignature({ message: "0xdeadbeef", signature }); + * ``` + */ +export class Secp256k1PublicKey extends PublicKey { + /** The expected byte length of an uncompressed secp256k1 public key. */ + static readonly LENGTH: number = 65; + /** The expected byte length of a compressed secp256k1 public key. */ + static readonly COMPRESSED_LENGTH: number = 33; + + private readonly key: Hex; + + /** + * Creates a `Secp256k1PublicKey` from raw bytes or a hex string. + * + * Both uncompressed (65-byte) and compressed (33-byte) keys are accepted. + * Compressed keys are expanded to uncompressed form before storage. + * + * @param hexInput - The public key as a hex string or `Uint8Array`. + * @throws If the input is neither 33 nor 65 bytes. + */ + constructor(hexInput: HexInput) { + super(); + const hex = Hex.fromHexInput(hexInput); + const { length } = hex.toUint8Array(); + if (length === Secp256k1PublicKey.LENGTH) { + // Validate uncompressed key is a valid curve point + secp256k1.Point.fromBytes(hex.toUint8Array()); + this.key = hex; + } else if (length === Secp256k1PublicKey.COMPRESSED_LENGTH) { + const point = secp256k1.Point.fromBytes(hex.toUint8Array()); + this.key = Hex.fromHexInput(point.toBytes(false)); + } else { + throw new Error( + `PublicKey length should be ${Secp256k1PublicKey.LENGTH} or ${Secp256k1PublicKey.COMPRESSED_LENGTH}, received ${length}`, + ); + } + } + + /** + * Verifies that `signature` was produced by signing `message` with the + * corresponding private key. + * + * The message is hashed with SHA3-256 before verification. + * + * @param args - Object containing `message` and `signature`. + * @returns `true` if the signature is valid, `false` otherwise. + */ + verifySignature(args: VerifySignatureArgs): boolean { + const { message, signature } = args; + const messageToVerify = convertSigningMessage(message); + const messageBytes = Hex.fromHexInput(messageToVerify).toUint8Array(); + const messageSha3Bytes = sha3_256(messageBytes); + const signatureBytes = signature.toUint8Array(); + return secp256k1.verify(signatureBytes, messageSha3Bytes, this.key.toUint8Array(), { lowS: true, prehash: false }); + } + + /** + * Returns the uncompressed 65-byte public key. + * + * @returns The public key as a `Uint8Array`. + */ + toUint8Array(): Uint8Array { + return this.key.toUint8Array(); + } + + /** + * BCS-serialises the public key by writing its bytes with a length prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.key.toUint8Array()); + } + + /** + * Deserialises a `Secp256k1PublicKey` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `Secp256k1PublicKey`. + */ + static deserialize(deserializer: Deserializer): Secp256k1PublicKey { + const bytes = deserializer.deserializeBytes(); + return new Secp256k1PublicKey(bytes); + } +} + +/** + * Represents a secp256k1 ECDSA private key (32 bytes). + * + * Supports key generation, BIP-44 derivation via BIP-32/BIP-39, signing, and + * secure memory clearing. + * + * @example + * ```ts + * const privateKey = Secp256k1PrivateKey.generate(); + * const signature = privateKey.sign("0xdeadbeef"); + * ``` + */ +export class Secp256k1PrivateKey extends Serializable implements PrivateKey { + /** The expected byte length of a secp256k1 private key. */ + static readonly LENGTH: number = 32; + + private key: Hex; + private cleared: boolean = false; + + /** + * Creates a `Secp256k1PrivateKey` from raw bytes or a hex/AIP-80 string. + * + * @param hexInput - A 32-byte private key as raw bytes, a hex string, or an + * AIP-80 string (`"secp256k1-priv-0x..."`). + * @param strict - When `true`, the input must be AIP-80 formatted. + * Defaults to `false`. + * @throws If the decoded key is not exactly 32 bytes. + */ + constructor(hexInput: HexInput, strict?: boolean) { + super(); + const privateKeyHex = PrivateKeyUtils.parseHexInput(hexInput, PrivateKeyVariants.Secp256k1, strict); + if (privateKeyHex.toUint8Array().length !== Secp256k1PrivateKey.LENGTH) { + throw new Error(`PrivateKey length should be ${Secp256k1PrivateKey.LENGTH}`); + } + this.key = privateKeyHex; + } + + /** + * Generates a random secp256k1 private key. + * + * @returns A new randomly generated `Secp256k1PrivateKey`. + * + * @example + * ```ts + * const key = Secp256k1PrivateKey.generate(); + * ``` + */ + static generate(): Secp256k1PrivateKey { + const hexInput = secp256k1.utils.randomSecretKey(); + return new Secp256k1PrivateKey(hexInput, false); + } + + /** + * Derives a secp256k1 private key from a BIP-39 mnemonic and a BIP-44 + * derivation path. + * + * @param path - A BIP-44 derivation path, e.g. `"m/44'/637'/0'/0/0"`. + * @param mnemonics - A BIP-39 mnemonic phrase. + * @returns The derived `Secp256k1PrivateKey`. + * @throws If the derivation path is not a valid BIP-44 path. + * + * @example + * ```ts + * const key = Secp256k1PrivateKey.fromDerivationPath( + * "m/44'/637'/0'/0/0", + * "your twelve word mnemonic phrase here ...", + * ); + * ``` + */ + static fromDerivationPath(path: string, mnemonics: string): Secp256k1PrivateKey { + if (!isValidBIP44Path(path)) { + throw new Error(`Invalid derivation path ${path}`); + } + return Secp256k1PrivateKey.fromDerivationPathInner(path, mnemonicToSeed(mnemonics)); + } + + private static fromDerivationPathInner(path: string, seed: Uint8Array): Secp256k1PrivateKey { + const hdKey = HDKey.fromMasterSeed(seed).derive(path); + const { privateKey } = hdKey; + if (privateKey === null) { + throw new Error("Invalid key"); + } + // Copy the private key before wiping HDKey internals + const keyCopy = privateKey.slice(); + // Zero HDKey internals to prevent key material lingering in memory + if (hdKey.privateKey) hdKey.privateKey.fill(0); + if (hdKey.chainCode) hdKey.chainCode.fill(0); + return new Secp256k1PrivateKey(keyCopy, false); + } + + private ensureNotCleared(): void { + if (this.cleared) { + throw new Error("Private key has been cleared from memory and can no longer be used"); + } + } + + /** + * Overwrites the private key material in memory with random and zero bytes, + * then marks the key as cleared. + * + * After calling this method any further use of the key will throw. + * + * **Note:** If the key was constructed from a hex string, the original string + * cannot be zeroed (JavaScript strings are immutable). For maximum security, + * construct keys from `Uint8Array` sources when possible and ensure the + * original string reference is not retained. + */ + clear(): void { + if (!this.cleared) { + const keyBytes = this.key.toUint8Array(); + crypto.getRandomValues(keyBytes); + keyBytes.fill(0xff); + crypto.getRandomValues(keyBytes); + keyBytes.fill(0); + this.cleared = true; + } + } + + /** + * Returns whether the key has been cleared from memory. + * + * @returns `true` if {@link clear} has been called, `false` otherwise. + */ + isCleared(): boolean { + return this.cleared; + } + + /** + * Signs a message and returns a {@link Secp256k1Signature}. + * + * The message is SHA3-256 hashed before signing. The signature is + * normalised to low-S form as required by the Aptos chain. + * + * @param message - The message to sign, as raw bytes, a hex string, or a + * plain UTF-8 string. + * @returns The resulting {@link Secp256k1Signature}. + * @throws If the key has been cleared. + */ + sign(message: HexInput): Secp256k1Signature { + this.ensureNotCleared(); + const messageToSign = convertSigningMessage(message); + const messageBytes = Hex.fromHexInput(messageToSign); + const messageHashBytes = sha3_256(messageBytes.toUint8Array()); + const signatureBytes = secp256k1.sign(messageHashBytes, this.key.toUint8Array(), { lowS: true, prehash: false }); + return new Secp256k1Signature(signatureBytes); + } + + /** + * Derives and returns the secp256k1 public key corresponding to this private key. + * + * @returns The associated {@link Secp256k1PublicKey} in uncompressed form. + * @throws If the key has been cleared. + */ + publicKey(): Secp256k1PublicKey { + this.ensureNotCleared(); + const bytes = secp256k1.getPublicKey(this.key.toUint8Array(), false); + return new Secp256k1PublicKey(bytes); + } + + /** + * Returns the raw 32-byte private key material. + * + * @returns The private key as a `Uint8Array`. + * @throws If the key has been cleared. + */ + toUint8Array(): Uint8Array { + this.ensureNotCleared(); + return this.key.toUint8Array(); + } + + /** + * Returns the private key as an AIP-80 formatted string (equivalent to + * {@link toAIP80String}). + * + * @returns An AIP-80 string, e.g. `"secp256k1-priv-0x..."`. + * @throws If the key has been cleared. + */ + toString(): string { + this.ensureNotCleared(); + return this.toAIP80String(); + } + + /** + * Returns the private key as a plain hex string (without AIP-80 prefix). + * + * @returns A `0x`-prefixed hex string. + * @throws If the key has been cleared. + */ + toHexString(): string { + this.ensureNotCleared(); + return this.key.toString(); + } + + /** + * Returns the private key as an AIP-80 compliant string. + * + * @returns A string of the form `"secp256k1-priv-0x"`. + * @throws If the key has been cleared. + */ + toAIP80String(): string { + this.ensureNotCleared(); + return PrivateKeyUtils.formatPrivateKey(this.key.toString(), PrivateKeyVariants.Secp256k1); + } + + /** + * BCS-serialises the private key by writing its bytes with a length prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.toUint8Array()); + } + + /** + * Deserialises a `Secp256k1PrivateKey` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `Secp256k1PrivateKey`. + */ + static deserialize(deserializer: Deserializer): Secp256k1PrivateKey { + const bytes = deserializer.deserializeBytes(); + return new Secp256k1PrivateKey(bytes, false); + } +} + +/** + * Represents a secp256k1 ECDSA signature (64 bytes, DER-encoded compact form). + * + * @example + * ```ts + * const sig = privateKey.sign("0xdeadbeef"); + * console.log(sig.toString()); // "0x..." + * ``` + */ +export class Secp256k1Signature extends Signature { + /** The expected byte length of a secp256k1 signature (compact form). */ + static readonly LENGTH = 64; + + private readonly data: Hex; + + /** + * Creates a `Secp256k1Signature` from raw bytes or a hex string. + * + * @param hexInput - A 64-byte signature in compact form as a hex string or `Uint8Array`. + * @throws If the input is not exactly 64 bytes. + */ + constructor(hexInput: HexInput) { + super(); + const data = Hex.fromHexInput(hexInput); + if (data.toUint8Array().length !== Secp256k1Signature.LENGTH) { + throw new Error( + `Signature length should be ${Secp256k1Signature.LENGTH}, received ${data.toUint8Array().length}`, + ); + } + this.data = data; + } + + /** + * Returns the raw 64-byte signature. + * + * @returns The signature as a `Uint8Array`. + */ + toUint8Array(): Uint8Array { + return this.data.toUint8Array(); + } + + /** + * BCS-serialises the signature by writing its bytes with a length prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.data.toUint8Array()); + } + + /** + * Deserialises a `Secp256k1Signature` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `Secp256k1Signature`. + */ + static deserialize(deserializer: Deserializer): Secp256k1Signature { + const hex = deserializer.deserializeBytes(); + return new Secp256k1Signature(hex); + } +} diff --git a/v10/src/crypto/secp256r1.ts b/v10/src/crypto/secp256r1.ts new file mode 100644 index 000000000..eb3cf66d1 --- /dev/null +++ b/v10/src/crypto/secp256r1.ts @@ -0,0 +1,447 @@ +import { p256 } from "@noble/curves/nist.js"; +import { sha3_256 } from "@noble/hashes/sha3.js"; +import type { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; +import type { HexInput } from "../hex/index.js"; +import { Hex } from "../hex/index.js"; +import { type PrivateKey, PrivateKeyUtils } from "./private-key.js"; +import { PublicKey, type VerifySignatureArgs } from "./public-key.js"; +import { Signature } from "./signature.js"; +import { PrivateKeyVariants } from "./types.js"; +import { convertSigningMessage } from "./utils.js"; + +/** + * Represents a secp256r1 (NIST P-256) public key (65-byte uncompressed or + * 33-byte compressed). + * + * Internally the key is always stored in uncompressed form (65 bytes). + * Compressed keys provided to the constructor are expanded automatically. + * + * @example + * ```ts + * const privateKey = Secp256r1PrivateKey.generate(); + * const publicKey = privateKey.publicKey(); + * const isValid = publicKey.verifySignature({ message: "0xdeadbeef", signature }); + * ``` + */ +export class Secp256r1PublicKey extends PublicKey { + /** The expected byte length of an uncompressed secp256r1 public key. */ + static readonly LENGTH: number = 65; + /** The expected byte length of a compressed secp256r1 public key. */ + static readonly COMPRESSED_LENGTH: number = 33; + + private readonly key: Hex; + + /** + * Creates a `Secp256r1PublicKey` from raw bytes or a hex string. + * + * Both uncompressed (65-byte) and compressed (33-byte) keys are accepted. + * Compressed keys are expanded to uncompressed form before storage. + * + * @param hexInput - The public key as a hex string or `Uint8Array`. + * @throws If the input is neither 33 nor 65 bytes. + */ + constructor(hexInput: HexInput) { + super(); + const hex = Hex.fromHexInput(hexInput); + const keyLength = hex.toUint8Array().length; + if (keyLength !== Secp256r1PublicKey.LENGTH && keyLength !== Secp256r1PublicKey.COMPRESSED_LENGTH) { + throw new Error( + `PublicKey length should be ${Secp256r1PublicKey.LENGTH} or ${Secp256r1PublicKey.COMPRESSED_LENGTH}, received ${keyLength}`, + ); + } + if (keyLength === Secp256r1PublicKey.COMPRESSED_LENGTH) { + const point = p256.Point.fromBytes(hex.toUint8Array()); + this.key = Hex.fromHexInput(point.toBytes(false)); + } else { + // Validate uncompressed key is a valid curve point + p256.Point.fromBytes(hex.toUint8Array()); + this.key = hex; + } + } + + /** + * Verifies that `signature` was produced by signing `message` with the + * corresponding private key. + * + * The message is hashed with SHA3-256 before verification. + * + * @param args - Object containing `message` and `signature`. + * @returns `true` if the signature is valid, `false` otherwise. + */ + verifySignature(args: VerifySignatureArgs): boolean { + const { message, signature } = args; + const messageToVerify = convertSigningMessage(message); + const msgHex = Hex.fromHexInput(messageToVerify).toUint8Array(); + const sha3Message = sha3_256(msgHex); + const rawSignature = signature.toUint8Array(); + return p256.verify(rawSignature, sha3Message, this.toUint8Array(), { prehash: false, lowS: true }); + } + + /** + * Returns the uncompressed 65-byte public key. + * + * @returns The public key as a `Uint8Array`. + */ + toUint8Array(): Uint8Array { + return this.key.toUint8Array(); + } + + /** + * BCS-serialises the public key by writing its bytes with a length prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.key.toUint8Array()); + } + + /** + * Deserialises a `Secp256r1PublicKey` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `Secp256r1PublicKey`. + */ + static deserialize(deserializer: Deserializer): Secp256r1PublicKey { + const bytes = deserializer.deserializeBytes(); + return new Secp256r1PublicKey(bytes); + } +} + +/** + * Represents a secp256r1 (NIST P-256) ECDSA private key (32 bytes). + * + * Supports key generation, signing, and raw byte export. + * + * @example + * ```ts + * const privateKey = Secp256r1PrivateKey.generate(); + * const signature = privateKey.sign("0xdeadbeef"); + * ``` + */ +export class Secp256r1PrivateKey extends Serializable implements PrivateKey { + /** The expected byte length of a secp256r1 private key. */ + static readonly LENGTH: number = 32; + + private key: Hex; + private cleared: boolean = false; + + /** + * Creates a `Secp256r1PrivateKey` from raw bytes or a hex/AIP-80 string. + * + * @param hexInput - A 32-byte private key as raw bytes, a hex string, or an + * AIP-80 string (`"secp256r1-priv-0x..."`). + * @param strict - When `true`, the input must be AIP-80 formatted. + * Defaults to `false`. + * @throws If the decoded key is not exactly 32 bytes. + */ + constructor(hexInput: HexInput, strict?: boolean) { + super(); + const privateKeyHex = PrivateKeyUtils.parseHexInput(hexInput, PrivateKeyVariants.Secp256r1, strict); + const keyLength = privateKeyHex.toUint8Array().length; + if (keyLength !== Secp256r1PrivateKey.LENGTH) { + throw new Error(`PrivateKey length should be ${Secp256r1PrivateKey.LENGTH}, received ${keyLength}`); + } + this.key = privateKeyHex; + } + + /** + * Generates a random secp256r1 private key. + * + * @returns A new randomly generated `Secp256r1PrivateKey`. + * + * @example + * ```ts + * const key = Secp256r1PrivateKey.generate(); + * ``` + */ + static generate(): Secp256r1PrivateKey { + const hexInput = p256.utils.randomSecretKey(); + return new Secp256r1PrivateKey(hexInput); + } + + private ensureNotCleared(): void { + if (this.cleared) { + throw new Error("Private key has been cleared from memory and can no longer be used"); + } + } + + /** + * Overwrites the private key material in memory with random and zero bytes, + * then marks the key as cleared. + * + * After calling this method any further use of the key will throw. + * + * **Note:** If the key was constructed from a hex string, the original string + * cannot be zeroed (JavaScript strings are immutable). For maximum security, + * construct keys from `Uint8Array` sources when possible and ensure the + * original string reference is not retained. + */ + clear(): void { + if (!this.cleared) { + const keyBytes = this.key.toUint8Array(); + crypto.getRandomValues(keyBytes); + keyBytes.fill(0xff); + crypto.getRandomValues(keyBytes); + keyBytes.fill(0); + this.cleared = true; + } + } + + /** + * Returns whether the key has been cleared from memory. + * + * @returns `true` if {@link clear} has been called, `false` otherwise. + */ + isCleared(): boolean { + return this.cleared; + } + + /** + * Signs a message and returns a {@link Secp256r1Signature}. + * + * The message is SHA3-256 hashed before signing. The signature is + * normalised to low-S form. + * + * @param message - The message to sign, as raw bytes or a hex string. + * @returns The resulting {@link Secp256r1Signature}. + * @throws If the key has been cleared. + */ + sign(message: HexInput): Secp256r1Signature { + this.ensureNotCleared(); + const messageToSign = convertSigningMessage(message); + const msgHex = Hex.fromHexInput(messageToSign); + const sha3Message = sha3_256(msgHex.toUint8Array()); + const signatureBytes = p256.sign(sha3Message, this.key.toUint8Array(), { prehash: false, lowS: true }); + return new Secp256r1Signature(signatureBytes); + } + + /** + * Derives and returns the secp256r1 public key corresponding to this private key. + * + * @returns The associated {@link Secp256r1PublicKey} in uncompressed form. + * @throws If the key has been cleared. + */ + publicKey(): Secp256r1PublicKey { + this.ensureNotCleared(); + const bytes = p256.getPublicKey(this.key.toUint8Array(), false); + return new Secp256r1PublicKey(bytes); + } + + /** + * Returns the raw 32-byte private key material. + * + * @returns The private key as a `Uint8Array`. + * @throws If the key has been cleared. + */ + toUint8Array(): Uint8Array { + this.ensureNotCleared(); + return this.key.toUint8Array(); + } + + /** + * Returns the private key as an AIP-80 compliant string. + * + * @returns A string of the form `"secp256r1-priv-0x"`. + * @throws If the key has been cleared. + */ + toString(): string { + this.ensureNotCleared(); + return PrivateKeyUtils.formatPrivateKey(this.key.toString(), PrivateKeyVariants.Secp256r1); + } + + /** + * Returns the private key as a plain hex string (without AIP-80 prefix). + * + * @returns A `0x`-prefixed hex string. + * @throws If the key has been cleared. + */ + toHexString(): string { + this.ensureNotCleared(); + return this.key.toString(); + } + + /** + * BCS-serialises the private key by writing its bytes with a length prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.toUint8Array()); + } + + /** + * Deserialises a `Secp256r1PrivateKey` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `Secp256r1PrivateKey`. + */ + static deserialize(deserializer: Deserializer): Secp256r1PrivateKey { + const bytes = deserializer.deserializeBytes(); + return new Secp256r1PrivateKey(bytes); + } +} + +/** + * Represents a WebAuthn assertion signature, consisting of the ECDSA + * signature, authenticator data, and client data JSON. + * + * Used for passkey-based authentication on Aptos. + */ +export class WebAuthnSignature extends Signature { + /** The raw ECDSA signature bytes. */ + readonly signature: Hex; + /** The authenticator data from the WebAuthn assertion response. */ + readonly authenticatorData: Hex; + /** The client data JSON from the WebAuthn assertion response. */ + readonly clientDataJSON: Hex; + + /** + * Creates a `WebAuthnSignature`. + * + * @param signature - The raw ECDSA signature bytes. + * @param authenticatorData - The authenticator data from the WebAuthn response. + * @param clientDataJSON - The client data JSON from the WebAuthn response. + */ + constructor(signature: HexInput, authenticatorData: HexInput, clientDataJSON: HexInput) { + super(); + this.signature = Hex.fromHexInput(signature); + this.authenticatorData = Hex.fromHexInput(authenticatorData); + this.clientDataJSON = Hex.fromHexInput(clientDataJSON); + } + + /** + * Returns the raw ECDSA signature bytes. + * + * @returns The signature bytes as a `Uint8Array`. + */ + toUint8Array() { + return this.signature.toUint8Array(); + } + + /** + * BCS-serialises the WebAuthn signature, including the variant tag, + * raw signature, authenticator data, and client data JSON. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer) { + serializer.serializeU32AsUleb128(0); + serializer.serializeBytes(this.signature.toUint8Array()); + serializer.serializeBytes(this.authenticatorData.toUint8Array()); + serializer.serializeBytes(this.clientDataJSON.toUint8Array()); + } + + /** + * Deserialises a `WebAuthnSignature` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `WebAuthnSignature`. + * @throws If the variant id is not `0`. + */ + /** Maximum expected size for an ECDSA-P256 signature (DER or compact). */ + static readonly MAX_SIGNATURE_LENGTH = 128; + /** Maximum expected size for authenticator data (2 KB). */ + static readonly MAX_AUTHENTICATOR_DATA_LENGTH = 2048; + /** Maximum expected size for client data JSON (4 KB). */ + static readonly MAX_CLIENT_DATA_JSON_LENGTH = 4096; + + static deserialize(deserializer: Deserializer) { + const id = deserializer.deserializeUleb128AsU32(); + if (id !== 0) { + throw new Error(`Invalid id for WebAuthnSignature: ${id}`); + } + const signature = deserializer.deserializeBytes(); + if (signature.length > WebAuthnSignature.MAX_SIGNATURE_LENGTH) { + throw new Error( + `WebAuthn signature length ${signature.length} exceeds maximum ${WebAuthnSignature.MAX_SIGNATURE_LENGTH}`, + ); + } + const authenticatorData = deserializer.deserializeBytes(); + if (authenticatorData.length > WebAuthnSignature.MAX_AUTHENTICATOR_DATA_LENGTH) { + throw new Error( + `WebAuthn authenticatorData length ${authenticatorData.length} exceeds maximum ${WebAuthnSignature.MAX_AUTHENTICATOR_DATA_LENGTH}`, + ); + } + const clientDataJSON = deserializer.deserializeBytes(); + if (clientDataJSON.length > WebAuthnSignature.MAX_CLIENT_DATA_JSON_LENGTH) { + throw new Error( + `WebAuthn clientDataJSON length ${clientDataJSON.length} exceeds maximum ${WebAuthnSignature.MAX_CLIENT_DATA_JSON_LENGTH}`, + ); + } + return new WebAuthnSignature(signature, authenticatorData, clientDataJSON); + } +} + +/** + * Represents a secp256r1 ECDSA signature (64 bytes, compact form). + * + * The constructor normalises the signature to low-S form if necessary. + * + * @example + * ```ts + * const sig = privateKey.sign("0xdeadbeef"); + * console.log(sig.toString()); // "0x..." + * ``` + */ +export class Secp256r1Signature extends Signature { + /** The expected byte length of a secp256r1 signature (compact form). */ + static readonly LENGTH = 64; + + private readonly data: Hex; + + /** + * Creates a `Secp256r1Signature` from raw bytes or a hex string. + * + * If the signature has a high-S value it is normalised to the equivalent + * low-S form to comply with Aptos chain requirements. + * + * @param hexInput - A 64-byte signature in compact form as a hex string or `Uint8Array`. + * @throws If the input is not exactly 64 bytes. + */ + constructor(hexInput: HexInput) { + super(); + const hex = Hex.fromHexInput(hexInput); + const signatureLength = hex.toUint8Array().length; + if (signatureLength !== Secp256r1Signature.LENGTH) { + throw new Error(`Signature length should be ${Secp256r1Signature.LENGTH}, received ${signatureLength}`); + } + const sig = p256.Signature.fromBytes(hex.toUint8Array()); + if (sig.hasHighS()) { + // biome-ignore lint/suspicious/noExplicitAny: Point.Fn.ORDER is not in the TypeScript types + const n = (p256.Point as any).Fn.ORDER as bigint; + this.data = Hex.fromHexInput(new p256.Signature(sig.r, n - sig.s).toBytes()); + } else { + this.data = hex; + } + } + + /** + * Returns the raw 64-byte signature (low-S normalised). + * + * @returns The signature as a `Uint8Array`. + */ + toUint8Array(): Uint8Array { + return this.data.toUint8Array(); + } + + /** + * BCS-serialises the signature by writing its bytes with a length prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.data.toUint8Array()); + } + + /** + * Deserialises a `Secp256r1Signature` from a BCS stream. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `Secp256r1Signature`. + */ + static deserialize(deserializer: Deserializer): Secp256r1Signature { + const hex = deserializer.deserializeBytes(); + return new Secp256r1Signature(hex); + } +} diff --git a/v10/src/crypto/signature.ts b/v10/src/crypto/signature.ts new file mode 100644 index 000000000..ac4f4f305 --- /dev/null +++ b/v10/src/crypto/signature.ts @@ -0,0 +1,31 @@ +import { Serializable } from "../bcs/serializer.js"; +import { Hex } from "../hex/index.js"; + +/** + * Abstract base class for all cryptographic signatures in the SDK. + * + * Concrete subclasses (e.g. {@link Ed25519Signature}, {@link Secp256k1Signature}) + * must implement `serialize` from {@link Serializable}. The base class provides + * default `toUint8Array` and `toString` implementations that delegate to BCS + * serialization. + */ +export abstract class Signature extends Serializable { + /** + * Returns the raw byte representation of the signature. + * + * @returns The signature as a `Uint8Array`. + */ + toUint8Array(): Uint8Array { + return this.bcsToBytes(); + } + + /** + * Returns a hex-encoded string representation of the signature. + * + * @returns A `0x`-prefixed hex string of the signature bytes. + */ + toString(): string { + const bytes = this.toUint8Array(); + return Hex.fromHexInput(bytes).toString(); + } +} diff --git a/v10/src/crypto/single-key.ts b/v10/src/crypto/single-key.ts new file mode 100644 index 000000000..791faadc1 --- /dev/null +++ b/v10/src/crypto/single-key.ts @@ -0,0 +1,288 @@ +import type { Deserializer } from "../bcs/deserializer.js"; +import type { Serializer } from "../bcs/serializer.js"; +import { Ed25519PublicKey, Ed25519Signature } from "./ed25519.js"; +import { AccountPublicKey, createAuthKey, type PublicKey, type VerifySignatureArgs } from "./public-key.js"; +import { Secp256k1PublicKey, Secp256k1Signature } from "./secp256k1.js"; +import { Secp256r1PublicKey, WebAuthnSignature } from "./secp256r1.js"; +import { Signature } from "./signature.js"; +import { AnyPublicKeyVariant, AnySignatureVariant, SigningScheme } from "./types.js"; + +/** Lazy-loaded constructor interface for public key types, to break circular deps. */ +interface LazyPublicKeyClass { + new (...args: never[]): PublicKey; + deserialize(d: Deserializer): PublicKey; +} + +/** Lazy-loaded constructor interface for signature types, to break circular deps. */ +interface LazySignatureClass { + new (...args: never[]): Signature; + deserialize(d: Deserializer): Signature; +} + +// Forward-declared types to avoid circular dependencies. +// Keyless/FederatedKeyless are imported lazily at deserialization time. +let _KeylessPublicKey: LazyPublicKeyClass | undefined; +let _FederatedKeylessPublicKey: LazyPublicKeyClass | undefined; +let _KeylessSignature: LazySignatureClass | undefined; + +/** + * Registers the Keyless and FederatedKeyless public-key and signature types + * with the `AnyPublicKey` / `AnySignature` deserialisers. + * + * This function must be called once during module initialisation (it is called + * automatically when the `crypto` index module is imported) to break the + * circular dependency between `single-key.ts` and `keyless.ts`. + * + * @param keylessPubKey - The `KeylessPublicKey` class. + * @param federatedKeylessPubKey - The `FederatedKeylessPublicKey` class. + * @param keylessSig - The `KeylessSignature` class. + */ +export function registerKeylessTypes( + keylessPubKey: LazyPublicKeyClass, + federatedKeylessPubKey: LazyPublicKeyClass, + keylessSig: LazySignatureClass, +) { + _KeylessPublicKey = keylessPubKey; + _FederatedKeylessPublicKey = federatedKeylessPubKey; + _KeylessSignature = keylessSig; +} + +/** + * Union type of the private key classes that can be used with `SingleKey` + * accounts. + */ +export type PrivateKeyInput = import("./ed25519.js").Ed25519PrivateKey | import("./secp256k1.js").Secp256k1PrivateKey; + +/** + * A type-tagged wrapper around any concrete {@link PublicKey}, used with the + * `SingleKey` authenticator on Aptos. + * + * `AnyPublicKey` is serialised as a ULEB128 variant index followed by the + * inner public key bytes, allowing the on-chain verifier to dispatch to the + * correct algorithm. + * + * @example + * ```ts + * const anyPubKey = new AnyPublicKey(new Ed25519PublicKey(keyBytes)); + * const authKey = anyPubKey.authKey(); + * ``` + */ +export class AnyPublicKey extends AccountPublicKey { + /** The underlying concrete public key. */ + public readonly publicKey: PublicKey; + /** The variant discriminant identifying the key algorithm. */ + public readonly variant: AnyPublicKeyVariant; + + /** + * Creates an `AnyPublicKey` wrapping the given concrete public key. + * + * The variant is inferred automatically from the key's runtime type. + * + * @param publicKey - The concrete public key to wrap. + * @throws If the public key type is not one of the supported variants + * (Ed25519, Secp256k1, Secp256r1, Keyless, FederatedKeyless). + */ + constructor(publicKey: PublicKey) { + super(); + this.publicKey = publicKey; + if (publicKey instanceof Ed25519PublicKey) { + this.variant = AnyPublicKeyVariant.Ed25519; + } else if (publicKey instanceof Secp256k1PublicKey) { + this.variant = AnyPublicKeyVariant.Secp256k1; + } else if (publicKey instanceof Secp256r1PublicKey) { + this.variant = AnyPublicKeyVariant.Secp256r1; + } else if (_KeylessPublicKey && publicKey instanceof _KeylessPublicKey) { + this.variant = AnyPublicKeyVariant.Keyless; + } else if (_FederatedKeylessPublicKey && publicKey instanceof _FederatedKeylessPublicKey) { + this.variant = AnyPublicKeyVariant.FederatedKeyless; + } else { + throw new Error("Unsupported public key type"); + } + } + + /** + * Verifies that `signature` was produced by the corresponding private key + * signing `message`. + * + * If the inner key is a Keyless key, verification is asynchronous and this + * synchronous method throws — use `verifySignatureAsync` instead. + * + * @param args - Object containing `message` and `signature`. + * @returns `true` if the signature is valid, `false` otherwise. + * @throws If the inner key is a Keyless key. + */ + verifySignature(args: VerifySignatureArgs): boolean { + const { message, signature } = args; + if (_KeylessPublicKey && this.publicKey instanceof _KeylessPublicKey) { + throw new Error("Use verifySignatureAsync to verify Keyless signatures"); + } + if (signature instanceof AnySignature) { + return this.publicKey.verifySignature({ message, signature: signature.signature }); + } + return this.publicKey.verifySignature({ message, signature }); + } + + /** + * Derives the on-chain authentication key using the `SingleKey` signing + * scheme. + * + * @returns The `AccountAddress` representing the authentication key. + */ + authKey(): unknown { + return createAuthKey(SigningScheme.SingleKey, this.bcsToBytes()); + } + + /** + * Returns the BCS-serialised bytes of this `AnyPublicKey`. + * + * @returns The serialised public key as a `Uint8Array`. + */ + toUint8Array(): Uint8Array { + return this.bcsToBytes(); + } + + /** + * BCS-serialises the `AnyPublicKey` by writing the ULEB128 variant index + * followed by the inner public key bytes. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(this.variant); + this.publicKey.serialize(serializer); + } + + /** + * Deserialises an `AnyPublicKey` from a BCS stream. + * + * The variant index determines which concrete public-key type to deserialise. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `AnyPublicKey`. + * @throws If the variant index is not recognised, or if the required Keyless + * types have not been registered via {@link registerKeylessTypes}. + */ + static deserialize(deserializer: Deserializer): AnyPublicKey { + const variantIndex = deserializer.deserializeUleb128AsU32(); + let publicKey: PublicKey; + switch (variantIndex) { + case AnyPublicKeyVariant.Ed25519: + publicKey = Ed25519PublicKey.deserialize(deserializer); + break; + case AnyPublicKeyVariant.Secp256k1: + publicKey = Secp256k1PublicKey.deserialize(deserializer); + break; + case AnyPublicKeyVariant.Secp256r1: + publicKey = Secp256r1PublicKey.deserialize(deserializer); + break; + case AnyPublicKeyVariant.Keyless: + if (!_KeylessPublicKey) throw new Error("KeylessPublicKey not registered"); + publicKey = _KeylessPublicKey.deserialize(deserializer); + break; + case AnyPublicKeyVariant.FederatedKeyless: + if (!_FederatedKeylessPublicKey) throw new Error("FederatedKeylessPublicKey not registered"); + publicKey = _FederatedKeylessPublicKey.deserialize(deserializer); + break; + default: + throw new Error(`Unknown variant index for AnyPublicKey: ${variantIndex}`); + } + return new AnyPublicKey(publicKey); + } +} + +/** + * A type-tagged wrapper around any concrete {@link Signature}, used with the + * `SingleKey` (and `MultiKey`) authenticator on Aptos. + * + * `AnySignature` is serialised as a ULEB128 variant index followed by the + * inner signature bytes, allowing the on-chain verifier to dispatch to the + * correct algorithm. + * + * @example + * ```ts + * const anySig = new AnySignature(ed25519Sig); + * ``` + */ +export class AnySignature extends Signature { + /** The underlying concrete signature. */ + public readonly signature: Signature; + private readonly variant: AnySignatureVariant; + + /** + * Creates an `AnySignature` wrapping the given concrete signature. + * + * The variant is inferred automatically from the signature's runtime type. + * + * @param signature - The concrete signature to wrap. + * @throws If the signature type is not one of the supported variants + * (Ed25519, Secp256k1, WebAuthn, Keyless). + */ + constructor(signature: Signature) { + super(); + this.signature = signature; + if (signature instanceof Ed25519Signature) { + this.variant = AnySignatureVariant.Ed25519; + } else if (signature instanceof Secp256k1Signature) { + this.variant = AnySignatureVariant.Secp256k1; + } else if (signature instanceof WebAuthnSignature) { + this.variant = AnySignatureVariant.WebAuthn; + } else if (_KeylessSignature && signature instanceof _KeylessSignature) { + this.variant = AnySignatureVariant.Keyless; + } else { + throw new Error("Unsupported signature type"); + } + } + + /** + * Returns the BCS-serialised bytes of this `AnySignature`. + * + * @returns The serialised signature as a `Uint8Array`. + */ + toUint8Array(): Uint8Array { + return this.bcsToBytes(); + } + + /** + * BCS-serialises the `AnySignature` by writing the ULEB128 variant index + * followed by the inner signature bytes. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(this.variant); + this.signature.serialize(serializer); + } + + /** + * Deserialises an `AnySignature` from a BCS stream. + * + * The variant index determines which concrete signature type to deserialise. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `AnySignature`. + * @throws If the variant index is not recognised, or if the Keyless signature + * type has not been registered via {@link registerKeylessTypes}. + */ + static deserialize(deserializer: Deserializer): AnySignature { + const variantIndex = deserializer.deserializeUleb128AsU32(); + let signature: Signature; + switch (variantIndex) { + case AnySignatureVariant.Ed25519: + signature = Ed25519Signature.deserialize(deserializer); + break; + case AnySignatureVariant.Secp256k1: + signature = Secp256k1Signature.deserialize(deserializer); + break; + case AnySignatureVariant.WebAuthn: + signature = WebAuthnSignature.deserialize(deserializer); + break; + case AnySignatureVariant.Keyless: + if (!_KeylessSignature) throw new Error("KeylessSignature not registered"); + signature = _KeylessSignature.deserialize(deserializer); + break; + default: + throw new Error(`Unknown variant index for AnySignature: ${variantIndex}`); + } + return new AnySignature(signature); + } +} diff --git a/v10/src/crypto/types.ts b/v10/src/crypto/types.ts new file mode 100644 index 000000000..83ef159d3 --- /dev/null +++ b/v10/src/crypto/types.ts @@ -0,0 +1,137 @@ +// Crypto-related enums and types + +/** + * Variants of private key types supported by the SDK. + */ +export enum PrivateKeyVariants { + Ed25519 = "ed25519", + Secp256k1 = "secp256k1", + Secp256r1 = "secp256r1", +} + +/** + * Numeric discriminants for the `AnyPublicKey` enum on-chain. + * Each variant corresponds to a supported public key algorithm. + */ +export enum AnyPublicKeyVariant { + Ed25519 = 0, + Secp256k1 = 1, + Secp256r1 = 2, + Keyless = 3, + FederatedKeyless = 4, + SlhDsaSha2_128s = 5, +} + +/** + * Converts an {@link AnyPublicKeyVariant} enum value to its canonical string name. + * + * @param variant - The public key variant to convert. + * @returns A lowercase string identifier for the variant (e.g. `"ed25519"`, `"keyless"`). + * @throws If the variant is not recognized. + * + * @example + * ```ts + * anyPublicKeyVariantToString(AnyPublicKeyVariant.Ed25519); // "ed25519" + * anyPublicKeyVariantToString(AnyPublicKeyVariant.Keyless); // "keyless" + * ``` + */ +export function anyPublicKeyVariantToString(variant: AnyPublicKeyVariant): string { + switch (variant) { + case AnyPublicKeyVariant.Ed25519: + return "ed25519"; + case AnyPublicKeyVariant.Secp256k1: + return "secp256k1"; + case AnyPublicKeyVariant.Secp256r1: + return "secp256r1"; + case AnyPublicKeyVariant.Keyless: + return "keyless"; + case AnyPublicKeyVariant.FederatedKeyless: + return "federated_keyless"; + case AnyPublicKeyVariant.SlhDsaSha2_128s: + return "slh_dsa_sha2_128s"; + default: + throw new Error("Unknown public key variant"); + } +} + +/** + * Numeric discriminants for the `AnySignature` enum on-chain. + * Each variant identifies the algorithm used to produce the signature. + */ +export enum AnySignatureVariant { + Ed25519 = 0, + Secp256k1 = 1, + WebAuthn = 2, + Keyless = 3, + SlhDsaSha2_128s = 4, +} + +/** + * Numeric discriminants for the ephemeral public key type used in + * Keyless authentication. + */ +export enum EphemeralPublicKeyVariant { + Ed25519 = 0, +} + +/** + * Numeric discriminants for the ephemeral signature type used in + * Keyless authentication. + */ +export enum EphemeralSignatureVariant { + Ed25519 = 0, +} + +/** + * Numeric discriminants for the ephemeral certificate type embedded + * inside a {@link KeylessSignature}. + */ +export enum EphemeralCertificateVariant { + ZkProof = 0, +} + +/** + * Numeric discriminants for the zero-knowledge proof variant used + * in Keyless authentication. + */ +export enum ZkpVariant { + Groth16 = 0, +} + +/** + * On-chain signing scheme discriminants. The value is serialised into the + * authentication key derivation path to identify which key scheme is in use. + */ +export enum SigningScheme { + Ed25519 = 0, + MultiEd25519 = 1, + SingleKey = 2, + MultiKey = 3, +} + +/** + * Input-facing signing scheme selector used when constructing accounts or + * specifying the desired key algorithm. + */ +export enum SigningSchemeInput { + Ed25519 = 0, + Secp256k1Ecdsa = 2, +} + +/** + * Scheme discriminants used when deriving special-purpose addresses (object + * addresses, resource-account addresses, etc.). + */ +export enum DeriveScheme { + DeriveAuid = 251, + DeriveObjectAddressFromObject = 252, + DeriveObjectAddressFromGuid = 253, + DeriveObjectAddressFromSeed = 254, + DeriveResourceAccountAddress = 255, +} + +/** + * Union of {@link SigningScheme} and {@link DeriveScheme} values that can + * appear as the scheme byte in an authentication key. + */ +export type AuthenticationKeyScheme = SigningScheme | DeriveScheme; diff --git a/v10/src/crypto/utils.ts b/v10/src/crypto/utils.ts new file mode 100644 index 000000000..84a8d26a1 --- /dev/null +++ b/v10/src/crypto/utils.ts @@ -0,0 +1,34 @@ +import type { HexInput } from "../hex/index.js"; + +const HEX_REGEX = /^(?:0x)?[0-9a-fA-F]+$/; +const TEXT_ENCODER = new TextEncoder(); + +/** + * Normalises a signing message so that it is always in a form that can be + * converted to bytes for cryptographic operations. + * + * If `message` is a plain string that is **not** valid hex it is encoded as + * UTF-8 bytes. If it is a valid hex string or already a `Uint8Array` / + * `ArrayBuffer` it is returned unchanged. + * + * @param message - The message to normalise, either as raw bytes or a string. + * @returns The message in a form accepted by `Hex.fromHexInput`. + * + * @example + * ```ts + * // Plain text → UTF-8 bytes + * convertSigningMessage("hello"); // Uint8Array([104, 101, 108, 108, 111]) + * + * // Valid hex string → returned as-is + * convertSigningMessage("0xdeadbeef"); // "0xdeadbeef" + * ``` + */ +export const convertSigningMessage = (message: HexInput): HexInput => { + if (typeof message === "string") { + if (message.length >= 2 && message.length % 2 === 0 && HEX_REGEX.test(message)) { + return message; + } + return TEXT_ENCODER.encode(message); + } + return message; +}; diff --git a/v10/src/hex/errors.ts b/v10/src/hex/errors.ts new file mode 100644 index 000000000..ef5525743 --- /dev/null +++ b/v10/src/hex/errors.ts @@ -0,0 +1,53 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * Error thrown when parsing fails (e.g. in `fromString` / `fromHexString`). + * + * @typeParam T - An enum type describing the possible failure reasons. + * + * @example + * ```typescript + * throw new ParsingError("Invalid hex", HexInvalidReason.INVALID_HEX_CHARS); + * ``` + */ +export class ParsingError extends Error { + /** + * Programmatic reason why parsing failed. + * Should be a value from an enum such as `HexInvalidReason`. + */ + public invalidReason: T; + + /** + * @param message - Human-readable description of the failure. + * @param invalidReason - Machine-readable reason code for the failure. + */ + constructor(message: string, invalidReason: T) { + super(message); + this.invalidReason = invalidReason; + } +} + +/** + * The result type returned by non-throwing validation functions such as `Hex.isValid()`. + * When `valid` is `false`, `invalidReason` and `invalidReasonMessage` provide details. + * + * @typeParam T - An enum type describing the possible failure reasons. + * + * @example + * ```typescript + * const result: ParsingResult = Hex.isValid("0xgg"); + * if (!result.valid) { + * console.log(result.invalidReason); // HexInvalidReason.INVALID_HEX_CHARS + * console.log(result.invalidReasonMessage); // descriptive message + * } + * ``` + */ +export type ParsingResult = { + /** Whether the input was successfully parsed. */ + valid: boolean; + /** Machine-readable reason for the failure (only set when `valid` is `false`). */ + invalidReason?: T; + /** Human-readable description of the failure (only set when `valid` is `false`). */ + invalidReasonMessage?: string; +}; diff --git a/v10/src/hex/hex.ts b/v10/src/hex/hex.ts new file mode 100644 index 000000000..4aac298ba --- /dev/null +++ b/v10/src/hex/hex.ts @@ -0,0 +1,249 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js"; +import { ParsingError, type ParsingResult } from "./errors.js"; + +/** + * A value that can be interpreted as hex data: either a hex-encoded string + * (with or without a `0x` prefix) or a raw `Uint8Array`. + * + * Most public APIs accept `HexInput` and convert it internally, so you rarely + * need to construct a {@link Hex} instance directly. + */ +export type HexInput = string | Uint8Array; + +/** + * Programmatic reasons why a hex string fails validation. + * Returned inside a {@link ParsingResult} by {@link Hex.isValid}. + */ +export enum HexInvalidReason { + /** The string (after stripping `0x`) is empty. */ + TOO_SHORT = "too_short", + /** The string has an odd number of hex characters. */ + INVALID_LENGTH = "invalid_length", + /** The string contains characters that are not valid hex digits. */ + INVALID_HEX_CHARS = "invalid_hex_chars", +} + +/** + * A helper class for working with hex-encoded binary data. + * + * Wraps a `Uint8Array` and provides conversions to/from hex strings. + * Accepts input with or without a `0x` prefix. + * + * NOTE: Do not use this class for Aptos account addresses — use `AccountAddress` instead. + * When accepting hex data as input, prefer `HexInput` and the static helper methods + * ({@link Hex.hexInputToUint8Array}, {@link Hex.hexInputToString}) for zero-allocation paths. + * + * @example + * ```typescript + * const hex = Hex.fromHexInput("0xdeadbeef"); + * console.log(hex.toString()); // "0xdeadbeef" + * console.log(hex.toStringWithoutPrefix()); // "deadbeef" + * console.log(hex.toUint8Array()); // Uint8Array([0xde, 0xad, 0xbe, 0xef]) + * + * // Validate without throwing + * const result = Hex.isValid("0xgg"); + * console.log(result.valid); // false + * console.log(result.invalidReason); // HexInvalidReason.INVALID_HEX_CHARS + * ``` + */ +export class Hex { + private readonly data: Uint8Array; + + /** + * Constructs a `Hex` instance from raw bytes. + * Prefer the static factory methods ({@link Hex.fromHexInput}, {@link Hex.fromHexString}) + * when starting from a string. + * @param data - The raw bytes to wrap. + */ + constructor(data: Uint8Array) { + this.data = data; + } + + // ── Output ── + + /** + * Returns the underlying raw bytes. + * @returns The `Uint8Array` wrapped by this instance. + */ + toUint8Array(): Uint8Array { + return this.data; + } + + /** + * Returns the hex-encoded bytes as a lowercase string **without** a `0x` prefix. + * @returns A lowercase hex string, e.g. `"deadbeef"`. + */ + toStringWithoutPrefix(): string { + return bytesToHex(this.data); + } + + /** + * Returns the hex-encoded bytes as a lowercase `0x`-prefixed string. + * @returns A lowercase hex string, e.g. `"0xdeadbeef"`. + */ + toString(): string { + return `0x${this.toStringWithoutPrefix()}`; + } + + // ── Input ── + + /** + * Parses a hex string (with or without `0x` prefix) into a `Hex` instance. + * @param str - The hex string to parse. + * @returns A new `Hex` instance. + * @throws {ParsingError} If the string is empty, has an odd length, or contains invalid characters. + * + * @example + * ```typescript + * const hex = Hex.fromHexString("deadbeef"); + * const hex2 = Hex.fromHexString("0xdeadbeef"); + * ``` + */ + static fromHexString(str: string): Hex { + let input = str; + + if (input.startsWith("0x")) { + input = input.slice(2); + } + + if (input.length === 0) { + throw new ParsingError( + "Hex string is too short, must be at least 1 char long, excluding the optional leading 0x.", + HexInvalidReason.TOO_SHORT, + ); + } + + if (input.length % 2 !== 0) { + throw new ParsingError("Hex string must be an even number of hex characters.", HexInvalidReason.INVALID_LENGTH); + } + + try { + return new Hex(hexToBytes(input)); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + throw new ParsingError( + `Hex string contains invalid hex characters: ${message}`, + HexInvalidReason.INVALID_HEX_CHARS, + ); + } + } + + /** + * Creates a `Hex` instance from a {@link HexInput} (hex string or `Uint8Array`). + * If `hexInput` is already a `Uint8Array`, it is wrapped without copying. + * @param hexInput - A hex string or raw bytes. + * @returns A new `Hex` instance. + * @throws {ParsingError} If `hexInput` is a string that fails to parse. + * + * @example + * ```typescript + * const a = Hex.fromHexInput("0xabcd"); + * const b = Hex.fromHexInput(new Uint8Array([0xab, 0xcd])); + * ``` + */ + static fromHexInput(hexInput: HexInput): Hex { + if (hexInput instanceof Uint8Array) return new Hex(hexInput); + return Hex.fromHexString(hexInput); + } + + /** + * Converts a {@link HexInput} directly to a `Uint8Array`. + * A convenience shorthand for `Hex.fromHexInput(hexInput).toUint8Array()`. + * @param hexInput - A hex string or raw bytes. + * @returns The decoded bytes. + * @throws {ParsingError} If `hexInput` is a string that fails to parse. + */ + static hexInputToUint8Array(hexInput: HexInput): Uint8Array { + if (hexInput instanceof Uint8Array) return hexInput; + return Hex.fromHexString(hexInput).toUint8Array(); + } + + /** + * Converts a {@link HexInput} to a `0x`-prefixed hex string. + * A convenience shorthand for `Hex.fromHexInput(hexInput).toString()`. + * @param hexInput - A hex string or raw bytes. + * @returns A `0x`-prefixed lowercase hex string. + */ + static hexInputToString(hexInput: HexInput): string { + return Hex.fromHexInput(hexInput).toString(); + } + + /** + * Converts a {@link HexInput} to a hex string without a `0x` prefix. + * A convenience shorthand for `Hex.fromHexInput(hexInput).toStringWithoutPrefix()`. + * @param hexInput - A hex string or raw bytes. + * @returns A lowercase hex string without a leading `0x`. + */ + static hexInputToStringWithoutPrefix(hexInput: HexInput): string { + return Hex.fromHexInput(hexInput).toStringWithoutPrefix(); + } + + // ── Validation ── + + /** + * Validates a hex string without throwing. + * @param str - The string to validate. + * @returns A {@link ParsingResult} describing whether the string is valid, + * and if not, the reason why. + * + * @example + * ```typescript + * const result = Hex.isValid("0xgg"); + * if (!result.valid) { + * console.log(result.invalidReason); // "invalid_hex_chars" + * } + * ``` + */ + static isValid(str: string): ParsingResult { + try { + Hex.fromHexString(str); + return { valid: true }; + } catch (error: unknown) { + if (error instanceof ParsingError) { + return { + valid: false, + invalidReason: error.invalidReason as HexInvalidReason, + invalidReasonMessage: error.message, + }; + } + return { valid: false, invalidReasonMessage: String(error) }; + } + } + + /** + * Returns `true` if this `Hex` instance contains the same bytes as `other`. + * @param other - The `Hex` instance to compare against. + * @returns Whether the two instances have identical byte content. + */ + /** + * Constant-time comparison to avoid timing side-channels when comparing + * secret or security-sensitive data. + */ + equals(other: Hex): boolean { + if (this.data.length !== other.data.length) return false; + let result = 0; + for (let i = 0; i < this.data.length; i++) { + result |= this.data[i] ^ other.data[i]; + } + return result === 0; + } +} + +/** + * Decodes a hex string to a UTF-8 ASCII string. + * Useful for reading on-chain string data stored as hex-encoded bytes. + * + * @param hex - A hex string (with or without `0x` prefix) or raw bytes. + * @returns The decoded ASCII/UTF-8 string. + * + * @example + * ```typescript + * hexToAsciiString("0x68656c6c6f"); // "hello" + * ``` + */ +const TEXT_DECODER = new TextDecoder(); + +export const hexToAsciiString = (hex: string): string => TEXT_DECODER.decode(Hex.fromHexInput(hex).toUint8Array()); diff --git a/v10/src/hex/index.ts b/v10/src/hex/index.ts new file mode 100644 index 000000000..c2dc6924f --- /dev/null +++ b/v10/src/hex/index.ts @@ -0,0 +1,17 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * @module hex + * + * Hex encoding / decoding utilities for the Aptos SDK. + * + * Key exports: + * - {@link Hex} — helper class for hex data (wraps `Uint8Array`, converts to/from strings) + * - {@link HexInput} — union type `string | Uint8Array` accepted by most SDK APIs + * - {@link HexInvalidReason} — enum of reasons why a hex string may fail validation + * - {@link hexToAsciiString} — decode a hex-encoded string to UTF-8 + * - {@link ParsingError} / {@link ParsingResult} — error and result types for validation + */ +export { ParsingError, type ParsingResult } from "./errors.js"; +export { Hex, type HexInput, HexInvalidReason, hexToAsciiString } from "./hex.js"; diff --git a/v10/src/index.ts b/v10/src/index.ts new file mode 100644 index 000000000..db61d9f20 --- /dev/null +++ b/v10/src/index.ts @@ -0,0 +1,20 @@ +// Aptos TypeScript SDK v10 +// ESM-only, tree-shakeable, function-first + +// Layer 4: Account +export * from "./account/index.js"; +// Layer 6: API +export * from "./api/index.js"; +// Layer 0: Primitives +export * from "./bcs/index.js"; +// Layer 5: Client +export * from "./client/index.js"; + +// Layer 2: Core +export * from "./core/index.js"; +// Layer 1: Crypto +export * from "./crypto/index.js"; +export * from "./hex/index.js"; +// Layer 3: Transactions +export * from "./transactions/index.js"; +export { VERSION } from "./version.js"; diff --git a/v10/src/transactions/authenticator.ts b/v10/src/transactions/authenticator.ts new file mode 100644 index 000000000..21ff00fbe --- /dev/null +++ b/v10/src/transactions/authenticator.ts @@ -0,0 +1,1015 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; +import { AccountAddress } from "../core/account-address.js"; +import { Ed25519PublicKey, Ed25519Signature } from "../crypto/ed25519.js"; +import { MultiEd25519PublicKey, MultiEd25519Signature } from "../crypto/multi-ed25519.js"; +import { MultiKey, MultiKeySignature } from "../crypto/multi-key.js"; +import { AnyPublicKey, AnySignature } from "../crypto/single-key.js"; +import type { HexInput } from "../hex/hex.js"; +import { Hex } from "../hex/hex.js"; +import type { MoveFunctionId } from "./types.js"; +import { + AASigningDataVariant, + AbstractAuthenticationDataVariant, + AccountAuthenticatorVariant, + TransactionAuthenticatorVariant, +} from "./types.js"; + +// ── Helper functions ── + +function getFunctionParts(functionArg: MoveFunctionId) { + const funcNameParts = functionArg.split("::"); + if (funcNameParts.length !== 3) { + throw new Error(`Invalid function ${functionArg}`); + } + return { + moduleAddress: funcNameParts[0], + moduleName: funcNameParts[1], + functionName: funcNameParts[2], + }; +} + +function isValidFunctionInfo(functionInfo: string): boolean { + const parts = functionInfo.split("::"); + return parts.length === 3 && AccountAddress.isValid({ input: parts[0] }).valid; +} + +// ── AccountAuthenticator ── + +/** + * Abstract base class for per-account authenticators. + * + * An `AccountAuthenticator` proves that a specific account authorized the transaction. + * The concrete subclass depends on the key scheme used by the account: + * - {@link AccountAuthenticatorEd25519} – legacy single Ed25519 key. + * - {@link AccountAuthenticatorMultiEd25519} – legacy k-of-n Ed25519 multi-key. + * - {@link AccountAuthenticatorSingleKey} – modern single-key (supports Ed25519, Secp256k1, Keyless, etc.). + * - {@link AccountAuthenticatorMultiKey} – modern k-of-n multi-key. + * - {@link AccountAuthenticatorNoAccountAuthenticator} – placeholder when no authentication is required. + * - {@link AccountAuthenticatorAbstraction} – account abstraction with a custom auth function. + * + * The variant is encoded as a ULEB128-prefixed discriminant during BCS serialization. + */ +export abstract class AccountAuthenticator extends Serializable { + abstract serialize(serializer: Serializer): void; + + /** + * Deserializes an `AccountAuthenticator` from BCS bytes, dispatching to the correct + * concrete subclass based on the ULEB128 variant prefix. + * + * @param deserializer - The BCS deserializer to read from. + * @returns The deserialized authenticator instance. + * @throws Error if the variant index is unknown. + */ + static deserialize(deserializer: Deserializer): AccountAuthenticator { + const index = deserializer.deserializeUleb128AsU32(); + switch (index) { + case AccountAuthenticatorVariant.Ed25519: + return AccountAuthenticatorEd25519.load(deserializer); + case AccountAuthenticatorVariant.MultiEd25519: + return AccountAuthenticatorMultiEd25519.load(deserializer); + case AccountAuthenticatorVariant.SingleKey: + return AccountAuthenticatorSingleKey.load(deserializer); + case AccountAuthenticatorVariant.MultiKey: + return AccountAuthenticatorMultiKey.load(deserializer); + case AccountAuthenticatorVariant.NoAccountAuthenticator: + return AccountAuthenticatorNoAccountAuthenticator.load(deserializer); + case AccountAuthenticatorVariant.Abstraction: + return AccountAuthenticatorAbstraction.load(deserializer); + default: + throw new Error(`Unknown variant index for AccountAuthenticator: ${index}`); + } + } + + /** + * Returns `true` when this authenticator is an {@link AccountAuthenticatorEd25519}. + * + * Acts as a type-narrowing predicate. + * + * @returns `true` if this is an Ed25519 account authenticator. + */ + isEd25519(): this is AccountAuthenticatorEd25519 { + return this instanceof AccountAuthenticatorEd25519; + } + + /** + * Returns `true` when this authenticator is an {@link AccountAuthenticatorMultiEd25519}. + * + * Acts as a type-narrowing predicate. + * + * @returns `true` if this is a MultiEd25519 account authenticator. + */ + isMultiEd25519(): this is AccountAuthenticatorMultiEd25519 { + return this instanceof AccountAuthenticatorMultiEd25519; + } + + /** + * Returns `true` when this authenticator is an {@link AccountAuthenticatorSingleKey}. + * + * Acts as a type-narrowing predicate. + * + * @returns `true` if this is a SingleKey account authenticator. + */ + isSingleKey(): this is AccountAuthenticatorSingleKey { + return this instanceof AccountAuthenticatorSingleKey; + } + + /** + * Returns `true` when this authenticator is an {@link AccountAuthenticatorMultiKey}. + * + * Acts as a type-narrowing predicate. + * + * @returns `true` if this is a MultiKey account authenticator. + */ + isMultiKey(): this is AccountAuthenticatorMultiKey { + return this instanceof AccountAuthenticatorMultiKey; + } +} + +// ── AccountAuthenticatorEd25519 ── + +/** + * Per-account authenticator for legacy Ed25519 single-key accounts. + * + * Encapsulates an Ed25519 public key and the corresponding signature over the transaction + * signing message. + * + * @example + * ```typescript + * const authenticator = new AccountAuthenticatorEd25519(publicKey, signature); + * ``` + */ +export class AccountAuthenticatorEd25519 extends AccountAuthenticator { + /** The Ed25519 public key of the signing account. */ + public readonly public_key: Ed25519PublicKey; + + /** The Ed25519 signature over the transaction signing message. */ + public readonly signature: Ed25519Signature; + + /** + * Creates a new `AccountAuthenticatorEd25519`. + * + * @param public_key - The signer's Ed25519 public key. + * @param signature - The Ed25519 signature over the signing message. + */ + constructor(public_key: Ed25519PublicKey, signature: Ed25519Signature) { + super(); + this.public_key = public_key; + this.signature = signature; + } + + /** + * Serializes this authenticator with its variant prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(AccountAuthenticatorVariant.Ed25519); + this.public_key.serialize(serializer); + this.signature.serialize(serializer); + } + + /** + * Deserializes an `AccountAuthenticatorEd25519` from BCS bytes (after the variant prefix + * has already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `AccountAuthenticatorEd25519` instance. + */ + static load(deserializer: Deserializer): AccountAuthenticatorEd25519 { + const public_key = Ed25519PublicKey.deserialize(deserializer); + const signature = Ed25519Signature.deserialize(deserializer); + return new AccountAuthenticatorEd25519(public_key, signature); + } +} + +// ── AccountAuthenticatorMultiEd25519 ── + +/** + * Per-account authenticator for legacy k-of-n MultiEd25519 accounts. + * + * Encapsulates a `MultiEd25519PublicKey` (which describes the threshold and all participant + * keys) and the corresponding aggregate signature. + * + * @example + * ```typescript + * const authenticator = new AccountAuthenticatorMultiEd25519(multiPublicKey, multiSignature); + * ``` + */ +export class AccountAuthenticatorMultiEd25519 extends AccountAuthenticator { + /** The multi-party Ed25519 public key descriptor. */ + public readonly public_key: MultiEd25519PublicKey; + + /** The aggregate Ed25519 signature. */ + public readonly signature: MultiEd25519Signature; + + /** + * Creates a new `AccountAuthenticatorMultiEd25519`. + * + * @param public_key - The multi-party Ed25519 public key descriptor. + * @param signature - The aggregate Ed25519 signature. + */ + constructor(public_key: MultiEd25519PublicKey, signature: MultiEd25519Signature) { + super(); + this.public_key = public_key; + this.signature = signature; + } + + /** + * Serializes this authenticator with its variant prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(AccountAuthenticatorVariant.MultiEd25519); + this.public_key.serialize(serializer); + this.signature.serialize(serializer); + } + + /** + * Deserializes an `AccountAuthenticatorMultiEd25519` from BCS bytes (after the variant + * prefix has already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `AccountAuthenticatorMultiEd25519` instance. + */ + static load(deserializer: Deserializer): AccountAuthenticatorMultiEd25519 { + const public_key = MultiEd25519PublicKey.deserialize(deserializer); + const signature = MultiEd25519Signature.deserialize(deserializer); + return new AccountAuthenticatorMultiEd25519(public_key, signature); + } +} + +// ── AccountAuthenticatorSingleKey ── + +/** + * Per-account authenticator for modern single-key accounts. + * + * Uses {@link AnyPublicKey} and {@link AnySignature} wrappers that support multiple key + * schemes (Ed25519, Secp256k1, Keyless, etc.) under a single authenticator type. + * + * @example + * ```typescript + * const authenticator = new AccountAuthenticatorSingleKey(anyPublicKey, anySignature); + * ``` + */ +export class AccountAuthenticatorSingleKey extends AccountAuthenticator { + /** The scheme-agnostic public key of the signing account. */ + public readonly public_key: AnyPublicKey; + + /** The scheme-agnostic signature over the transaction signing message. */ + public readonly signature: AnySignature; + + /** + * Creates a new `AccountAuthenticatorSingleKey`. + * + * @param public_key - The scheme-agnostic public key. + * @param signature - The scheme-agnostic signature. + */ + constructor(public_key: AnyPublicKey, signature: AnySignature) { + super(); + this.public_key = public_key; + this.signature = signature; + } + + /** + * Serializes this authenticator with its variant prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(AccountAuthenticatorVariant.SingleKey); + this.public_key.serialize(serializer); + this.signature.serialize(serializer); + } + + /** + * Deserializes an `AccountAuthenticatorSingleKey` from BCS bytes (after the variant + * prefix has already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `AccountAuthenticatorSingleKey` instance. + */ + static load(deserializer: Deserializer): AccountAuthenticatorSingleKey { + const public_key = AnyPublicKey.deserialize(deserializer); + const signature = AnySignature.deserialize(deserializer); + return new AccountAuthenticatorSingleKey(public_key, signature); + } +} + +// ── AccountAuthenticatorMultiKey ── + +/** + * Per-account authenticator for modern k-of-n multi-key accounts. + * + * Uses {@link MultiKey} (which holds multiple {@link AnyPublicKey} instances and a threshold) + * and {@link MultiKeySignature} (which holds the collected signatures). + * + * @example + * ```typescript + * const authenticator = new AccountAuthenticatorMultiKey(multiKey, multiKeySignature); + * ``` + */ +export class AccountAuthenticatorMultiKey extends AccountAuthenticator { + /** The multi-key descriptor with threshold and participant public keys. */ + public readonly public_keys: MultiKey; + + /** The collected signatures from the threshold number of participants. */ + public readonly signatures: MultiKeySignature; + + /** + * Creates a new `AccountAuthenticatorMultiKey`. + * + * @param public_keys - The multi-key descriptor. + * @param signatures - The collected threshold signatures. + */ + constructor(public_keys: MultiKey, signatures: MultiKeySignature) { + super(); + this.public_keys = public_keys; + this.signatures = signatures; + } + + /** + * Serializes this authenticator with its variant prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(AccountAuthenticatorVariant.MultiKey); + this.public_keys.serialize(serializer); + this.signatures.serialize(serializer); + } + + /** + * Deserializes an `AccountAuthenticatorMultiKey` from BCS bytes (after the variant prefix + * has already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `AccountAuthenticatorMultiKey` instance. + */ + static load(deserializer: Deserializer): AccountAuthenticatorMultiKey { + const public_keys = MultiKey.deserialize(deserializer); + const signatures = MultiKeySignature.deserialize(deserializer); + return new AccountAuthenticatorMultiKey(public_keys, signatures); + } +} + +// ── AccountAuthenticatorNoAccountAuthenticator ── + +/** + * A placeholder per-account authenticator that carries no cryptographic proof. + * + * Used in fee-payer transactions before the fee payer has signed, acting as a sentinel + * value that the fee payer's authenticator slot is intentionally empty. + */ +export class AccountAuthenticatorNoAccountAuthenticator extends AccountAuthenticator { + /** + * Serializes this authenticator with its variant prefix (no additional bytes). + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(AccountAuthenticatorVariant.NoAccountAuthenticator); + } + + /** + * Deserializes an `AccountAuthenticatorNoAccountAuthenticator` from BCS bytes (after the + * variant prefix has already been consumed). + * + * @param _deserializer - The BCS deserializer (not read from, included for interface consistency). + * @returns A new `AccountAuthenticatorNoAccountAuthenticator` instance. + */ + static load(_deserializer: Deserializer): AccountAuthenticatorNoAccountAuthenticator { + return new AccountAuthenticatorNoAccountAuthenticator(); + } +} + +// ── AccountAuthenticatorAbstraction ── + +/** + * Per-account authenticator for account abstraction (AA) accounts. + * + * Account abstraction allows on-chain Move functions to define custom authentication logic. + * The authenticator captures: + * - The Move function (`functionInfo`) that will verify the signature. + * - A digest of the original signing message. + * - The raw bytes produced by the custom signing logic. + * - Optionally, an `accountIdentity` byte string used by derivable AA accounts. + * + * @example + * ```typescript + * const authenticator = new AccountAuthenticatorAbstraction( + * "0x1::permissioned_signer::authenticate", + * signingMessageDigest, + * abstractionSignatureBytes, + * ); + * ``` + */ +export class AccountAuthenticatorAbstraction extends AccountAuthenticator { + /** + * The fully-qualified Move function identifier in `
::::` format. + * + * This function is invoked on-chain to verify the `abstractionSignature`. + */ + public readonly functionInfo: string; + + /** A hex-encoded digest of the original signing message. */ + public readonly signingMessageDigest: Hex; + + /** The raw signature bytes produced by the custom abstraction signing logic. */ + public readonly abstractionSignature: Uint8Array; + + /** + * An optional byte string that identifies the abstract account. + * + * When present the authenticator uses the `DerivableV1` abstract authentication data + * variant, which allows the account identity to be derived from the public key material. + */ + public readonly accountIdentity?: Uint8Array; + + /** + * Creates a new `AccountAuthenticatorAbstraction`. + * + * @param functionInfo - Fully-qualified Move function identifier used to verify the signature. + * @param signingMessageDigest - Hex-encoded digest of the original signing message. + * @param abstractionSignature - Raw bytes from the custom signing logic. + * @param accountIdentity - Optional account identity bytes (enables `DerivableV1` variant). + * @throws Error if `functionInfo` is not a valid `
::::` string. + */ + constructor( + functionInfo: string, + signingMessageDigest: HexInput, + abstractionSignature: Uint8Array, + accountIdentity?: Uint8Array, + ) { + super(); + if (!isValidFunctionInfo(functionInfo)) { + throw new Error(`Invalid function info ${functionInfo} passed into AccountAuthenticatorAbstraction`); + } + this.functionInfo = functionInfo; + this.abstractionSignature = abstractionSignature; + this.signingMessageDigest = Hex.fromHexInput(signingMessageDigest); + this.accountIdentity = accountIdentity; + } + + /** + * Serializes this authenticator with its variant prefix and all fields. + * + * The serialization format differs depending on whether `accountIdentity` is present: + * - Without `accountIdentity`: uses the `V1` abstract authentication data variant. + * - With `accountIdentity`: uses the `DerivableV1` variant and appends the identity bytes. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(AccountAuthenticatorVariant.Abstraction); + const { moduleAddress, moduleName, functionName } = getFunctionParts(this.functionInfo as MoveFunctionId); + AccountAddress.fromString(moduleAddress).serialize(serializer); + serializer.serializeStr(moduleName); + serializer.serializeStr(functionName); + if (this.accountIdentity) { + serializer.serializeU32AsUleb128(AbstractAuthenticationDataVariant.DerivableV1); + } else { + serializer.serializeU32AsUleb128(AbstractAuthenticationDataVariant.V1); + } + serializer.serializeBytes(this.signingMessageDigest.toUint8Array()); + serializer.serializeBytes(this.abstractionSignature); + if (this.accountIdentity) { + serializer.serializeBytes(this.accountIdentity); + } + } + + /** + * Deserializes an `AccountAuthenticatorAbstraction` from BCS bytes (after the variant + * prefix has already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `AccountAuthenticatorAbstraction` instance. + * @throws Error if the abstract authentication data variant is unknown. + */ + static load(deserializer: Deserializer): AccountAuthenticatorAbstraction { + const moduleAddress = AccountAddress.deserialize(deserializer); + const moduleName = deserializer.deserializeStr(); + const functionName = deserializer.deserializeStr(); + const variant = deserializer.deserializeUleb128AsU32(); + const signingMessageDigest = deserializer.deserializeBytes(); + + if (variant === AbstractAuthenticationDataVariant.V1) { + const abstractionSignature = deserializer.deserializeBytes(); + return new AccountAuthenticatorAbstraction( + `${moduleAddress}::${moduleName}::${functionName}`, + signingMessageDigest, + abstractionSignature, + ); + } + if (variant === AbstractAuthenticationDataVariant.DerivableV1) { + const abstractionSignature = deserializer.deserializeBytes(); + const abstractPublicKey = deserializer.deserializeBytes(); + return new AccountAuthenticatorAbstraction( + `${moduleAddress}::${moduleName}::${functionName}`, + signingMessageDigest, + abstractionSignature, + abstractPublicKey, + ); + } + throw new Error(`Unknown variant index for AccountAuthenticatorAbstraction: ${variant}`); + } +} + +// ── AccountAbstractionMessage ── + +/** + * The BCS-serializable signing message passed to an account abstraction auth function. + * + * When an account uses account abstraction, the Move `authenticate` function receives an + * `AccountAbstractionMessage` rather than the raw transaction signing message. It wraps + * the original signing message bytes together with the fully-qualified Move function info + * of the auth function itself. + * + * @example + * ```typescript + * const aaMessage = new AccountAbstractionMessage( + * originalSigningMessageBytes, + * "0x1::permissioned_signer::authenticate", + * ); + * const digest = sha3_256(aaMessage.bcsToBytes()); + * ``` + */ +export class AccountAbstractionMessage extends Serializable { + /** The original signing message (before account abstraction wrapping). */ + public readonly originalSigningMessage: Hex; + + /** + * The fully-qualified Move function identifier that will verify this message. + * + * Format: `
::::`. + */ + public readonly functionInfo: string; + + /** + * Creates a new `AccountAbstractionMessage`. + * + * @param originalSigningMessage - The raw signing message bytes (hex or Uint8Array). + * @param functionInfo - The fully-qualified Move auth function identifier. + */ + constructor(originalSigningMessage: HexInput, functionInfo: string) { + super(); + this.originalSigningMessage = Hex.fromHexInput(originalSigningMessage); + this.functionInfo = functionInfo; + } + + /** + * Serializes this message with the `V1` signing data variant prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(AASigningDataVariant.V1); + serializer.serializeBytes(this.originalSigningMessage.toUint8Array()); + const { moduleAddress, moduleName, functionName } = getFunctionParts(this.functionInfo as MoveFunctionId); + AccountAddress.fromString(moduleAddress).serialize(serializer); + serializer.serializeStr(moduleName); + serializer.serializeStr(functionName); + } + + /** + * Deserializes an `AccountAbstractionMessage` from BCS bytes. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `AccountAbstractionMessage` instance. + * @throws Error if the signing data variant is not `V1`. + */ + static deserialize(deserializer: Deserializer): AccountAbstractionMessage { + const variant = deserializer.deserializeUleb128AsU32(); + if (variant !== AASigningDataVariant.V1) { + throw new Error(`Unknown variant index for AccountAbstractionMessage: ${variant}`); + } + const originalSigningMessage = deserializer.deserializeBytes(); + const functionInfoModuleAddress = AccountAddress.deserialize(deserializer); + const functionInfoModuleName = deserializer.deserializeStr(); + const functionInfoFunctionName = deserializer.deserializeStr(); + const functionInfo = `${functionInfoModuleAddress}::${functionInfoModuleName}::${functionInfoFunctionName}`; + return new AccountAbstractionMessage(originalSigningMessage, functionInfo); + } +} + +// ── TransactionAuthenticator ── + +/** + * Abstract base class for top-level transaction authenticators. + * + * A `TransactionAuthenticator` proves that all required parties have signed the transaction. + * The concrete subclass depends on the transaction structure: + * - {@link TransactionAuthenticatorEd25519} – single legacy Ed25519 sender. + * - {@link TransactionAuthenticatorMultiEd25519} – single legacy MultiEd25519 sender. + * - {@link TransactionAuthenticatorMultiAgent} – transaction with secondary signers. + * - {@link TransactionAuthenticatorFeePayer} – transaction with a fee payer. + * - {@link TransactionAuthenticatorSingleSender} – single modern-key sender. + * + * The variant is encoded as a ULEB128-prefixed discriminant during BCS serialization. + */ +export abstract class TransactionAuthenticator extends Serializable { + abstract serialize(serializer: Serializer): void; + + /** + * Deserializes a `TransactionAuthenticator` from BCS bytes, dispatching to the correct + * concrete subclass based on the ULEB128 variant prefix. + * + * @param deserializer - The BCS deserializer to read from. + * @returns The deserialized authenticator instance. + * @throws Error if the variant index is unknown. + */ + static deserialize(deserializer: Deserializer): TransactionAuthenticator { + const index = deserializer.deserializeUleb128AsU32(); + switch (index) { + case TransactionAuthenticatorVariant.Ed25519: + return TransactionAuthenticatorEd25519.load(deserializer); + case TransactionAuthenticatorVariant.MultiEd25519: + return TransactionAuthenticatorMultiEd25519.load(deserializer); + case TransactionAuthenticatorVariant.MultiAgent: + return TransactionAuthenticatorMultiAgent.load(deserializer); + case TransactionAuthenticatorVariant.FeePayer: + return TransactionAuthenticatorFeePayer.load(deserializer); + case TransactionAuthenticatorVariant.SingleSender: + return TransactionAuthenticatorSingleSender.load(deserializer); + default: + throw new Error(`Unknown variant index for TransactionAuthenticator: ${index}`); + } + } + + /** + * Returns `true` when this authenticator is a {@link TransactionAuthenticatorEd25519}. + * + * Acts as a type-narrowing predicate. + * + * @returns `true` if this is an Ed25519 transaction authenticator. + */ + isEd25519(): this is TransactionAuthenticatorEd25519 { + return this instanceof TransactionAuthenticatorEd25519; + } + + /** + * Returns `true` when this authenticator is a {@link TransactionAuthenticatorMultiEd25519}. + * + * Acts as a type-narrowing predicate. + * + * @returns `true` if this is a MultiEd25519 transaction authenticator. + */ + isMultiEd25519(): this is TransactionAuthenticatorMultiEd25519 { + return this instanceof TransactionAuthenticatorMultiEd25519; + } + + /** + * Returns `true` when this authenticator is a {@link TransactionAuthenticatorMultiAgent}. + * + * Acts as a type-narrowing predicate. + * + * @returns `true` if this is a multi-agent transaction authenticator. + */ + isMultiAgent(): this is TransactionAuthenticatorMultiAgent { + return this instanceof TransactionAuthenticatorMultiAgent; + } + + /** + * Returns `true` when this authenticator is a {@link TransactionAuthenticatorFeePayer}. + * + * Acts as a type-narrowing predicate. + * + * @returns `true` if this is a fee-payer transaction authenticator. + */ + isFeePayer(): this is TransactionAuthenticatorFeePayer { + return this instanceof TransactionAuthenticatorFeePayer; + } + + /** + * Returns `true` when this authenticator is a {@link TransactionAuthenticatorSingleSender}. + * + * Acts as a type-narrowing predicate. + * + * @returns `true` if this is a single-sender transaction authenticator. + */ + isSingleSender(): this is TransactionAuthenticatorSingleSender { + return this instanceof TransactionAuthenticatorSingleSender; + } +} + +// ── TransactionAuthenticatorEd25519 ── + +/** + * Top-level transaction authenticator for a single legacy Ed25519 sender. + * + * @example + * ```typescript + * const authenticator = new TransactionAuthenticatorEd25519(publicKey, signature); + * ``` + */ +export class TransactionAuthenticatorEd25519 extends TransactionAuthenticator { + /** The Ed25519 public key of the sender. */ + public readonly public_key: Ed25519PublicKey; + + /** The Ed25519 signature over the transaction signing message. */ + public readonly signature: Ed25519Signature; + + /** + * Creates a new `TransactionAuthenticatorEd25519`. + * + * @param public_key - The sender's Ed25519 public key. + * @param signature - The Ed25519 signature over the signing message. + */ + constructor(public_key: Ed25519PublicKey, signature: Ed25519Signature) { + super(); + this.public_key = public_key; + this.signature = signature; + } + + /** + * Serializes this authenticator with its variant prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionAuthenticatorVariant.Ed25519); + this.public_key.serialize(serializer); + this.signature.serialize(serializer); + } + + /** + * Deserializes a `TransactionAuthenticatorEd25519` from BCS bytes (after the variant + * prefix has already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `TransactionAuthenticatorEd25519` instance. + */ + static load(deserializer: Deserializer): TransactionAuthenticatorEd25519 { + const public_key = Ed25519PublicKey.deserialize(deserializer); + const signature = Ed25519Signature.deserialize(deserializer); + return new TransactionAuthenticatorEd25519(public_key, signature); + } +} + +// ── TransactionAuthenticatorMultiEd25519 ── + +/** + * Top-level transaction authenticator for a single legacy MultiEd25519 sender. + * + * @example + * ```typescript + * const authenticator = new TransactionAuthenticatorMultiEd25519(multiPublicKey, multiSignature); + * ``` + */ +export class TransactionAuthenticatorMultiEd25519 extends TransactionAuthenticator { + /** The multi-party Ed25519 public key descriptor. */ + public readonly public_key: MultiEd25519PublicKey; + + /** The aggregate Ed25519 signature. */ + public readonly signature: MultiEd25519Signature; + + /** + * Creates a new `TransactionAuthenticatorMultiEd25519`. + * + * @param public_key - The multi-party Ed25519 public key descriptor. + * @param signature - The aggregate Ed25519 signature. + */ + constructor(public_key: MultiEd25519PublicKey, signature: MultiEd25519Signature) { + super(); + this.public_key = public_key; + this.signature = signature; + } + + /** + * Serializes this authenticator with its variant prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionAuthenticatorVariant.MultiEd25519); + this.public_key.serialize(serializer); + this.signature.serialize(serializer); + } + + /** + * Deserializes a `TransactionAuthenticatorMultiEd25519` from BCS bytes (after the variant + * prefix has already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `TransactionAuthenticatorMultiEd25519` instance. + */ + static load(deserializer: Deserializer): TransactionAuthenticatorMultiEd25519 { + const public_key = MultiEd25519PublicKey.deserialize(deserializer); + const signature = MultiEd25519Signature.deserialize(deserializer); + return new TransactionAuthenticatorMultiEd25519(public_key, signature); + } +} + +// ── TransactionAuthenticatorMultiAgent ── + +/** + * Top-level transaction authenticator for multi-agent transactions. + * + * Carries the primary sender's authenticator as well as the authenticators for each + * secondary signer. The secondary signer addresses are also included to bind each + * authenticator to its corresponding account. + * + * @example + * ```typescript + * const authenticator = new TransactionAuthenticatorMultiAgent( + * senderAuthenticator, + * [secondaryAddress], + * [secondaryAuthenticator], + * ); + * ``` + */ +export class TransactionAuthenticatorMultiAgent extends TransactionAuthenticator { + /** The primary sender's per-account authenticator. */ + public readonly sender: AccountAuthenticator; + + /** Ordered list of secondary signer addresses (matches `secondary_signers`). */ + public readonly secondary_signer_addresses: Array; + + /** Ordered list of secondary signer per-account authenticators. */ + public readonly secondary_signers: Array; + + /** + * Creates a new `TransactionAuthenticatorMultiAgent`. + * + * @param sender - The primary sender's authenticator. + * @param secondary_signer_addresses - Addresses of the secondary signers. + * @param secondary_signers - Per-account authenticators for each secondary signer. + */ + constructor( + sender: AccountAuthenticator, + secondary_signer_addresses: Array, + secondary_signers: Array, + ) { + super(); + this.sender = sender; + this.secondary_signer_addresses = secondary_signer_addresses; + this.secondary_signers = secondary_signers; + } + + /** + * Serializes this authenticator with its variant prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionAuthenticatorVariant.MultiAgent); + this.sender.serialize(serializer); + serializer.serializeVector(this.secondary_signer_addresses); + serializer.serializeVector(this.secondary_signers); + } + + /** + * Deserializes a `TransactionAuthenticatorMultiAgent` from BCS bytes (after the variant + * prefix has already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `TransactionAuthenticatorMultiAgent` instance. + */ + static load(deserializer: Deserializer): TransactionAuthenticatorMultiAgent { + const sender = AccountAuthenticator.deserialize(deserializer); + const secondary_signer_addresses = deserializer.deserializeVector(AccountAddress, 255); + const secondary_signers = deserializer.deserializeVector(AccountAuthenticator, 255); + return new TransactionAuthenticatorMultiAgent(sender, secondary_signer_addresses, secondary_signers); + } +} + +// ── TransactionAuthenticatorFeePayer ── + +/** + * Top-level transaction authenticator for fee-payer (sponsored) transactions. + * + * Carries the primary sender's authenticator, optional secondary signer authenticators, + * and the fee payer's address together with its authenticator. + * + * @example + * ```typescript + * const authenticator = new TransactionAuthenticatorFeePayer( + * senderAuthenticator, + * [], + * [], + * { address: feePayerAddress, authenticator: feePayerAuthenticator }, + * ); + * ``` + */ +export class TransactionAuthenticatorFeePayer extends TransactionAuthenticator { + /** The primary sender's per-account authenticator. */ + public readonly sender: AccountAuthenticator; + + /** Ordered list of secondary signer addresses (matches `secondary_signers`). */ + public readonly secondary_signer_addresses: Array; + + /** Ordered list of secondary signer per-account authenticators. */ + public readonly secondary_signers: Array; + + /** The fee payer account address and its per-account authenticator. */ + public readonly fee_payer: { + address: AccountAddress; + authenticator: AccountAuthenticator; + }; + + /** + * Creates a new `TransactionAuthenticatorFeePayer`. + * + * @param sender - The primary sender's authenticator. + * @param secondary_signer_addresses - Addresses of the secondary signers. + * @param secondary_signers - Per-account authenticators for each secondary signer. + * @param fee_payer - Object containing the fee payer address and authenticator. + */ + constructor( + sender: AccountAuthenticator, + secondary_signer_addresses: Array, + secondary_signers: Array, + fee_payer: { address: AccountAddress; authenticator: AccountAuthenticator }, + ) { + super(); + this.sender = sender; + this.secondary_signer_addresses = secondary_signer_addresses; + this.secondary_signers = secondary_signers; + this.fee_payer = fee_payer; + } + + /** + * Serializes this authenticator with its variant prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionAuthenticatorVariant.FeePayer); + this.sender.serialize(serializer); + serializer.serializeVector(this.secondary_signer_addresses); + serializer.serializeVector(this.secondary_signers); + this.fee_payer.address.serialize(serializer); + this.fee_payer.authenticator.serialize(serializer); + } + + /** + * Deserializes a `TransactionAuthenticatorFeePayer` from BCS bytes (after the variant + * prefix has already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `TransactionAuthenticatorFeePayer` instance. + */ + static load(deserializer: Deserializer): TransactionAuthenticatorFeePayer { + const sender = AccountAuthenticator.deserialize(deserializer); + const secondary_signer_addresses = deserializer.deserializeVector(AccountAddress, 255); + const secondary_signers = deserializer.deserializeVector(AccountAuthenticator, 255); + const address = AccountAddress.deserialize(deserializer); + const authenticator = AccountAuthenticator.deserialize(deserializer); + const fee_payer = { address, authenticator }; + return new TransactionAuthenticatorFeePayer(sender, secondary_signer_addresses, secondary_signers, fee_payer); + } +} + +// ── TransactionAuthenticatorSingleSender ── + +/** + * Top-level transaction authenticator for a single modern-key sender. + * + * Wraps a single {@link AccountAuthenticator} and is used when the transaction has one + * sender with a modern key scheme (e.g. SingleKey or MultiKey). + * + * @example + * ```typescript + * const authenticator = new TransactionAuthenticatorSingleSender(senderAuthenticator); + * ``` + */ +export class TransactionAuthenticatorSingleSender extends TransactionAuthenticator { + /** The sender's per-account authenticator. */ + public readonly sender: AccountAuthenticator; + + /** + * Creates a new `TransactionAuthenticatorSingleSender`. + * + * @param sender - The sender's per-account authenticator. + */ + constructor(sender: AccountAuthenticator) { + super(); + this.sender = sender; + } + + /** + * Serializes this authenticator with its variant prefix. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionAuthenticatorVariant.SingleSender); + this.sender.serialize(serializer); + } + + /** + * Deserializes a `TransactionAuthenticatorSingleSender` from BCS bytes (after the variant + * prefix has already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `TransactionAuthenticatorSingleSender` instance. + */ + static load(deserializer: Deserializer): TransactionAuthenticatorSingleSender { + const sender = AccountAuthenticator.deserialize(deserializer); + return new TransactionAuthenticatorSingleSender(sender); + } +} diff --git a/v10/src/transactions/chain-id.ts b/v10/src/transactions/chain-id.ts new file mode 100644 index 000000000..52f23dc24 --- /dev/null +++ b/v10/src/transactions/chain-id.ts @@ -0,0 +1,52 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; + +/** + * BCS-serializable wrapper around a numeric Aptos chain identifier. + * + * The chain ID is included in every {@link RawTransaction} to prevent replay attacks across + * different Aptos networks (e.g. mainnet vs. testnet). + * + * @example + * ```typescript + * const chainId = new ChainId(1); // mainnet + * const bytes = chainId.bcsToBytes(); + * ``` + */ +export class ChainId extends Serializable { + /** The numeric chain identifier. */ + public readonly chainId: number; + + /** + * Creates a new `ChainId` wrapper. + * + * @param chainId - The numeric identifier of the target Aptos network. + */ + constructor(chainId: number) { + super(); + this.chainId = chainId; + } + + /** + * Serializes the chain ID as a single unsigned 8-bit integer. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU8(this.chainId); + } + + /** + * Deserializes a `ChainId` from BCS bytes. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `ChainId` instance. + */ + static deserialize(deserializer: Deserializer): ChainId { + const chainId = deserializer.deserializeU8(); + return new ChainId(chainId); + } +} diff --git a/v10/src/transactions/index.ts b/v10/src/transactions/index.ts new file mode 100644 index 000000000..3213474c0 --- /dev/null +++ b/v10/src/transactions/index.ts @@ -0,0 +1,33 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * Public API surface for the Aptos transactions module. + * + * Re-exports all transaction-related types, classes, and functions used to build, sign, + * and submit transactions to the Aptos blockchain: + * + * - **Authenticators** – {@link AccountAuthenticator} and {@link TransactionAuthenticator} + * subclasses that prove authorization. + * - **Chain ID** – {@link ChainId} wrapper used inside raw transactions. + * - **Module ID** – {@link ModuleId} used inside entry function descriptors. + * - **Transaction wrappers** – {@link SimpleTransaction} and {@link MultiAgentTransaction}. + * - **Raw transactions** – {@link RawTransaction}, {@link MultiAgentRawTransaction}, and + * {@link FeePayerRawTransaction}. + * - **Signed transaction** – {@link SignedTransaction} combining raw transaction and authenticator. + * - **Signing message utilities** – {@link generateSigningMessageForTransaction} and helpers. + * - **Transaction payloads** – {@link EntryFunction}, {@link Script}, {@link MultiSig}, and + * all payload wrapper types. + * - **Type aliases and enums** – {@link MoveModuleId}, {@link MoveStructId}, + * {@link MoveFunctionId}, {@link AnyRawTransaction}, and all variant enums. + */ +export * from "./authenticator.js"; +export * from "./chain-id.js"; +export * from "./module-id.js"; +export * from "./multi-agent-transaction.js"; +export * from "./raw-transaction.js"; +export * from "./signed-transaction.js"; +export * from "./signing-message.js"; +export * from "./simple-transaction.js"; +export * from "./transaction-payload.js"; +export * from "./types.js"; diff --git a/v10/src/transactions/module-id.ts b/v10/src/transactions/module-id.ts new file mode 100644 index 000000000..b4d1fd4cc --- /dev/null +++ b/v10/src/transactions/module-id.ts @@ -0,0 +1,82 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; +import { AccountAddress } from "../core/account-address.js"; +import { Identifier } from "../core/type-tag.js"; +import type { MoveModuleId } from "./types.js"; + +/** + * BCS-serializable representation of a Move module identifier. + * + * A module is uniquely identified by the combination of the deploying account address and + * the module name. `ModuleId` is used inside {@link EntryFunction} to specify which module + * the function belongs to. + * + * @example + * ```typescript + * const moduleId = ModuleId.fromStr("0x1::coin"); + * ``` + */ +export class ModuleId extends Serializable { + /** The on-chain address that published this module. */ + public readonly address: AccountAddress; + + /** The name of the module within the publishing account. */ + public readonly name: Identifier; + + /** + * Creates a new `ModuleId` from an address and module name. + * + * @param address - The account address that published the module. + * @param name - The module name identifier. + */ + constructor(address: AccountAddress, name: Identifier) { + super(); + this.address = address; + this.name = name; + } + + /** + * Parses a `ModuleId` from a string in `
::` format. + * + * @param moduleId - A fully-qualified module identifier string, e.g. `"0x1::coin"`. + * @returns A new `ModuleId` instance. + * @throws Error if the string does not contain exactly two `::` separated parts. + * + * @example + * ```typescript + * const moduleId = ModuleId.fromStr("0x1::coin"); + * ``` + */ + static fromStr(moduleId: MoveModuleId): ModuleId { + const parts = moduleId.split("::"); + if (parts.length !== 2) { + throw new Error("Invalid module id."); + } + return new ModuleId(AccountAddress.fromString(parts[0]), new Identifier(parts[1])); + } + + /** + * Serializes this `ModuleId` into BCS bytes (address followed by name). + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + this.address.serialize(serializer); + this.name.serialize(serializer); + } + + /** + * Deserializes a `ModuleId` from BCS bytes. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `ModuleId` instance. + */ + static deserialize(deserializer: Deserializer): ModuleId { + const address = AccountAddress.deserialize(deserializer); + const name = Identifier.deserialize(deserializer); + return new ModuleId(address, name); + } +} diff --git a/v10/src/transactions/multi-agent-transaction.ts b/v10/src/transactions/multi-agent-transaction.ts new file mode 100644 index 000000000..9a9cecc36 --- /dev/null +++ b/v10/src/transactions/multi-agent-transaction.ts @@ -0,0 +1,99 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; +import { AccountAddress } from "../core/account-address.js"; +import { RawTransaction } from "./raw-transaction.js"; + +/** + * A high-level wrapper around a transaction that requires multiple signers. + * + * `MultiAgentTransaction` extends the single-sender model by requiring one or more + * secondary signers to co-sign the transaction. It also supports an optional fee payer + * that sponsors the gas costs. + * + * This is the preferred type for cross-account operations such as atomic swaps or actions + * that require authorization from multiple parties. + * + * @example + * ```typescript + * // Multi-agent transaction without a fee payer + * const multiAgentTxn = new MultiAgentTransaction(rawTransaction, [secondaryAddress]); + * + * // Multi-agent transaction with a fee payer + * const sponsoredMultiAgentTxn = new MultiAgentTransaction( + * rawTransaction, + * [secondaryAddress], + * feePayerAddress, + * ); + * ``` + */ +export class MultiAgentTransaction extends Serializable { + /** The underlying unsigned transaction. */ + public rawTransaction: RawTransaction; + + /** + * Address of the fee payer account, if this is a sponsored transaction. + * + * When `undefined` the sender pays their own gas fees. + */ + public feePayerAddress?: AccountAddress | undefined; + + /** Ordered list of secondary signer addresses that must co-sign this transaction. */ + public secondarySignerAddresses: AccountAddress[]; + + /** + * Creates a new `MultiAgentTransaction`. + * + * @param rawTransaction - The core unsigned transaction to wrap. + * @param secondarySignerAddresses - Addresses of the accounts that must co-sign. + * @param feePayerAddress - Optional address of the account that will pay gas fees. + */ + constructor( + rawTransaction: RawTransaction, + secondarySignerAddresses: AccountAddress[], + feePayerAddress?: AccountAddress, + ) { + super(); + this.rawTransaction = rawTransaction; + this.feePayerAddress = feePayerAddress; + this.secondarySignerAddresses = secondarySignerAddresses; + } + + /** + * Serializes this transaction to BCS bytes. + * + * The format is: + * `rawTransaction | secondarySignerAddresses[] | bool(feePayerPresent) [| feePayerAddress]`. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + this.rawTransaction.serialize(serializer); + serializer.serializeVector(this.secondarySignerAddresses); + if (this.feePayerAddress === undefined) { + serializer.serializeBool(false); + } else { + serializer.serializeBool(true); + this.feePayerAddress.serialize(serializer); + } + } + + /** + * Deserializes a `MultiAgentTransaction` from BCS bytes. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `MultiAgentTransaction` instance. + */ + static deserialize(deserializer: Deserializer): MultiAgentTransaction { + const rawTransaction = RawTransaction.deserialize(deserializer); + const secondarySignerAddresses = deserializer.deserializeVector(AccountAddress); + const feePayerPresent = deserializer.deserializeBool(); + let feePayerAddress: AccountAddress | undefined; + if (feePayerPresent) { + feePayerAddress = AccountAddress.deserialize(deserializer); + } + return new MultiAgentTransaction(rawTransaction, secondarySignerAddresses, feePayerAddress); + } +} diff --git a/v10/src/transactions/raw-transaction.ts b/v10/src/transactions/raw-transaction.ts new file mode 100644 index 000000000..ffc4f70f8 --- /dev/null +++ b/v10/src/transactions/raw-transaction.ts @@ -0,0 +1,303 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; +import { AccountAddress } from "../core/account-address.js"; +import { ChainId } from "./chain-id.js"; +import { TransactionPayload } from "./transaction-payload.js"; +import { TransactionVariants } from "./types.js"; + +// ── RawTransaction ── + +/** + * The core unsigned transaction structure that is submitted to the Aptos blockchain. + * + * A `RawTransaction` contains everything the network needs to execute a transaction: the + * sender, a monotonically-increasing sequence number, the payload (entry function, script, + * or multisig), gas limits, an expiry timestamp, and the target chain ID. + * + * To create a signable message from a `RawTransaction`, pass it through + * {@link generateSigningMessageForTransaction}. + * + * @example + * ```typescript + * const rawTxn = new RawTransaction( + * senderAddress, + * sequenceNumber, + * payload, + * maxGasAmount, + * gasUnitPrice, + * expirationTimestampSecs, + * chainId, + * ); + * const bytes = rawTxn.bcsToBytes(); + * ``` + */ +export class RawTransaction extends Serializable { + /** The account address of the transaction sender. */ + public readonly sender: AccountAddress; + + /** + * The sender's current sequence number. + * + * Each transaction must use the next sequential value to prevent replay attacks and + * ensure ordering. + */ + public readonly sequence_number: bigint; + + /** The executable payload of this transaction. */ + public readonly payload: TransactionPayload; + + /** The maximum number of gas units the sender is willing to pay for this transaction. */ + public readonly max_gas_amount: bigint; + + /** The price (in octas) the sender is willing to pay per unit of gas. */ + public readonly gas_unit_price: bigint; + + /** + * Unix timestamp (seconds) after which the transaction is considered expired and will + * no longer be accepted by the network. + */ + public readonly expiration_timestamp_secs: bigint; + + /** Identifies the target Aptos network; prevents cross-chain replay. */ + public readonly chain_id: ChainId; + + /** + * Creates a new `RawTransaction`. + * + * @param sender - The account address of the transaction sender. + * @param sequence_number - The sender's current on-chain sequence number. + * @param payload - The transaction payload to execute. + * @param max_gas_amount - Maximum gas units the sender will pay. + * @param gas_unit_price - Price per gas unit in octas. + * @param expiration_timestamp_secs - Unix timestamp (seconds) when the transaction expires. + * @param chain_id - The target chain's numeric identifier. + */ + constructor( + sender: AccountAddress, + sequence_number: bigint, + payload: TransactionPayload, + max_gas_amount: bigint, + gas_unit_price: bigint, + expiration_timestamp_secs: bigint, + chain_id: ChainId, + ) { + super(); + this.sender = sender; + this.sequence_number = sequence_number; + this.payload = payload; + this.max_gas_amount = max_gas_amount; + this.gas_unit_price = gas_unit_price; + this.expiration_timestamp_secs = expiration_timestamp_secs; + this.chain_id = chain_id; + } + + /** + * Serializes this `RawTransaction` into BCS bytes. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + this.sender.serialize(serializer); + serializer.serializeU64(this.sequence_number); + this.payload.serialize(serializer); + serializer.serializeU64(this.max_gas_amount); + serializer.serializeU64(this.gas_unit_price); + serializer.serializeU64(this.expiration_timestamp_secs); + this.chain_id.serialize(serializer); + } + + /** + * Deserializes a `RawTransaction` from BCS bytes. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `RawTransaction` instance. + */ + static deserialize(deserializer: Deserializer): RawTransaction { + const sender = AccountAddress.deserialize(deserializer); + const sequence_number = deserializer.deserializeU64(); + const payload = TransactionPayload.deserialize(deserializer); + const max_gas_amount = deserializer.deserializeU64(); + const gas_unit_price = deserializer.deserializeU64(); + const expiration_timestamp_secs = deserializer.deserializeU64(); + const chain_id = ChainId.deserialize(deserializer); + return new RawTransaction( + sender, + sequence_number, + payload, + max_gas_amount, + gas_unit_price, + expiration_timestamp_secs, + chain_id, + ); + } +} + +// ── RawTransactionWithData ── + +/** + * Abstract base class for raw transactions that carry additional signer metadata alongside + * the core {@link RawTransaction}. + * + * Two concrete variants exist: + * - {@link MultiAgentRawTransaction} – adds secondary signer addresses. + * - {@link FeePayerRawTransaction} – adds secondary signer addresses and a fee payer address. + * + * The variant is encoded as a ULEB128-prefixed discriminant during BCS serialization. + */ +export abstract class RawTransactionWithData extends Serializable { + abstract serialize(serializer: Serializer): void; + + /** + * Deserializes a `RawTransactionWithData` from BCS bytes, dispatching to the correct + * concrete subclass based on the ULEB128 variant prefix. + * + * @param deserializer - The BCS deserializer to read from. + * @returns Either a {@link MultiAgentRawTransaction} or a {@link FeePayerRawTransaction}. + * @throws Error if the variant index is unknown. + */ + static deserialize(deserializer: Deserializer): RawTransactionWithData { + const index = deserializer.deserializeUleb128AsU32(); + switch (index) { + case TransactionVariants.MultiAgentTransaction: + return MultiAgentRawTransaction.load(deserializer); + case TransactionVariants.FeePayerTransaction: + return FeePayerRawTransaction.load(deserializer); + default: + throw new Error(`Unknown variant index for RawTransactionWithData: ${index}`); + } + } +} + +// ── MultiAgentRawTransaction ── + +/** + * A raw transaction that includes one or more secondary signers in addition to the primary sender. + * + * All secondary signer addresses must be present in the BCS bytes before the transaction is + * signed by any party. This is the low-level representation used when computing the signing + * message; the high-level wrapper is {@link MultiAgentTransaction}. + * + * @example + * ```typescript + * const multiAgentRawTxn = new MultiAgentRawTransaction(rawTxn, [secondaryAddress]); + * const signingMessage = generateSigningMessageForTransaction(multiAgentTxn); + * ``` + */ +export class MultiAgentRawTransaction extends RawTransactionWithData { + /** The underlying unsigned transaction. */ + public readonly raw_txn: RawTransaction; + + /** Ordered list of secondary signer addresses that must co-sign this transaction. */ + public readonly secondary_signer_addresses: Array; + + /** + * Creates a new `MultiAgentRawTransaction`. + * + * @param raw_txn - The core unsigned transaction. + * @param secondary_signer_addresses - Addresses of the required secondary signers. + */ + constructor(raw_txn: RawTransaction, secondary_signer_addresses: Array) { + super(); + this.raw_txn = raw_txn; + this.secondary_signer_addresses = secondary_signer_addresses; + } + + /** + * Serializes this transaction with its variant prefix and secondary signer addresses. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionVariants.MultiAgentTransaction); + this.raw_txn.serialize(serializer); + serializer.serializeVector(this.secondary_signer_addresses); + } + + /** + * Deserializes a `MultiAgentRawTransaction` from BCS bytes (after the variant prefix has + * already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `MultiAgentRawTransaction` instance. + */ + static load(deserializer: Deserializer): MultiAgentRawTransaction { + const rawTxn = RawTransaction.deserialize(deserializer); + const secondarySignerAddresses = deserializer.deserializeVector(AccountAddress, 255); + return new MultiAgentRawTransaction(rawTxn, secondarySignerAddresses); + } +} + +// ── FeePayerRawTransaction ── + +/** + * A raw transaction that designates a separate account to pay the gas fees. + * + * In addition to the primary sender and optional secondary signers, this variant carries + * the address of the fee payer account. The fee payer must also sign the transaction. + * This is the low-level representation; the high-level wrapper is {@link SimpleTransaction} + * or {@link MultiAgentTransaction} with a `feePayerAddress` set. + * + * @example + * ```typescript + * const feePayerRawTxn = new FeePayerRawTransaction(rawTxn, [], feePayerAddress); + * const signingMessage = generateSigningMessageForTransaction(simpleTxn); + * ``` + */ +export class FeePayerRawTransaction extends RawTransactionWithData { + /** The underlying unsigned transaction. */ + public readonly raw_txn: RawTransaction; + + /** Ordered list of secondary signer addresses (may be empty). */ + public readonly secondary_signer_addresses: Array; + + /** The address of the account that will pay gas fees for this transaction. */ + public readonly fee_payer_address: AccountAddress; + + /** + * Creates a new `FeePayerRawTransaction`. + * + * @param raw_txn - The core unsigned transaction. + * @param secondary_signer_addresses - Addresses of any required secondary signers. + * @param fee_payer_address - The address of the fee payer account. + */ + constructor( + raw_txn: RawTransaction, + secondary_signer_addresses: Array, + fee_payer_address: AccountAddress, + ) { + super(); + this.raw_txn = raw_txn; + this.secondary_signer_addresses = secondary_signer_addresses; + this.fee_payer_address = fee_payer_address; + } + + /** + * Serializes this transaction with its variant prefix, secondary signer addresses, and + * fee payer address. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionVariants.FeePayerTransaction); + this.raw_txn.serialize(serializer); + serializer.serializeVector(this.secondary_signer_addresses); + this.fee_payer_address.serialize(serializer); + } + + /** + * Deserializes a `FeePayerRawTransaction` from BCS bytes (after the variant prefix has + * already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `FeePayerRawTransaction` instance. + */ + static load(deserializer: Deserializer): FeePayerRawTransaction { + const rawTxn = RawTransaction.deserialize(deserializer); + const secondarySignerAddresses = deserializer.deserializeVector(AccountAddress, 255); + const feePayerAddress = AccountAddress.deserialize(deserializer); + return new FeePayerRawTransaction(rawTxn, secondarySignerAddresses, feePayerAddress); + } +} diff --git a/v10/src/transactions/signed-transaction.ts b/v10/src/transactions/signed-transaction.ts new file mode 100644 index 000000000..ecc1032da --- /dev/null +++ b/v10/src/transactions/signed-transaction.ts @@ -0,0 +1,63 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; +import { TransactionAuthenticator } from "./authenticator.js"; +import { RawTransaction } from "./raw-transaction.js"; + +/** + * A fully signed transaction ready for submission to the Aptos network. + * + * `SignedTransaction` pairs a {@link RawTransaction} with a {@link TransactionAuthenticator} + * that proves the sender (and any co-signers) have authorized the transaction. This is the + * final form produced by the signing step in the build → sign → submit workflow. + * + * @example + * ```typescript + * const signedTxn = new SignedTransaction(rawTxn, authenticator); + * const bytes = signedTxn.bcsToBytes(); + * // Submit `bytes` to the Aptos REST API + * ``` + */ +export class SignedTransaction extends Serializable { + /** The unsigned transaction that was signed. */ + public readonly raw_txn: RawTransaction; + + /** The cryptographic proof that the transaction was authorized by the sender. */ + public readonly authenticator: TransactionAuthenticator; + + /** + * Creates a new `SignedTransaction`. + * + * @param raw_txn - The unsigned transaction to sign. + * @param authenticator - The authenticator produced by signing the transaction. + */ + constructor(raw_txn: RawTransaction, authenticator: TransactionAuthenticator) { + super(); + this.raw_txn = raw_txn; + this.authenticator = authenticator; + } + + /** + * Serializes this signed transaction to BCS bytes (raw transaction followed by authenticator). + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + this.raw_txn.serialize(serializer); + this.authenticator.serialize(serializer); + } + + /** + * Deserializes a `SignedTransaction` from BCS bytes. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `SignedTransaction` instance. + */ + static deserialize(deserializer: Deserializer): SignedTransaction { + const raw_txn = RawTransaction.deserialize(deserializer); + const authenticator = TransactionAuthenticator.deserialize(deserializer); + return new SignedTransaction(raw_txn, authenticator); + } +} diff --git a/v10/src/transactions/signing-message.ts b/v10/src/transactions/signing-message.ts new file mode 100644 index 000000000..9348c56be --- /dev/null +++ b/v10/src/transactions/signing-message.ts @@ -0,0 +1,122 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { sha3_256 as sha3Hash } from "@noble/hashes/sha3.js"; +import { RAW_TRANSACTION_SALT, RAW_TRANSACTION_WITH_DATA_SALT } from "../core/constants.js"; +import { FeePayerRawTransaction, MultiAgentRawTransaction } from "./raw-transaction.js"; +import type { AnyRawTransaction, AnyRawTransactionInstance } from "./types.js"; + +/** + * Derives the appropriate low-level raw transaction instance from a high-level transaction + * wrapper. + * + * The Aptos protocol requires different BCS-encoded structures depending on whether the + * transaction has secondary signers or a fee payer. This helper inspects the wrapper and + * returns: + * - A {@link FeePayerRawTransaction} when `feePayerAddress` is set. + * - A {@link MultiAgentRawTransaction} when `secondarySignerAddresses` is set. + * - The bare {@link RawTransaction} otherwise. + * + * @param transaction - The high-level transaction wrapper to inspect. + * @returns The corresponding low-level BCS-serializable transaction instance. + * + * @example + * ```typescript + * const rawTxnInstance = deriveTransactionType(simpleTransaction); + * const bytes = rawTxnInstance.bcsToBytes(); + * ``` + */ +export function deriveTransactionType(transaction: AnyRawTransaction): AnyRawTransactionInstance { + if (transaction.feePayerAddress) { + return new FeePayerRawTransaction( + transaction.rawTransaction, + transaction.secondarySignerAddresses ?? [], + transaction.feePayerAddress, + ); + } + if (transaction.secondarySignerAddresses) { + return new MultiAgentRawTransaction(transaction.rawTransaction, transaction.secondarySignerAddresses); + } + return transaction.rawTransaction; +} + +const textEncoder = new TextEncoder(); +const MAX_DOMAIN_SEPARATOR_CACHE = 64; +const domainSeparatorCache = new Map(); + +/** + * Constructs an Aptos-prefixed signing message from arbitrary bytes and a domain separator. + * + * The signing message is: + * ``` + * SHA3-256(domainSeparator) || bytes + * ``` + * + * The SHA3-256 hash of the domain separator acts as a fixed-length domain prefix that + * prevents cross-protocol signature reuse. All Aptos domain separators must begin with + * `"APTOS::"`. + * + * @param bytes - The raw BCS-serialized payload bytes to sign. + * @param domainSeparator - An `"APTOS::"`-prefixed string that uniquely identifies the + * signing context (e.g. `"APTOS::RawTransaction"`). + * @returns The combined byte array `SHA3-256(domainSeparator) || bytes`. + * @throws Error if `domainSeparator` does not start with `"APTOS::"`. + * + * @example + * ```typescript + * const message = generateSigningMessage(rawTxnBytes, "APTOS::RawTransaction"); + * const signature = privateKey.sign(message); + * ``` + */ +export function generateSigningMessage(bytes: Uint8Array, domainSeparator: string): Uint8Array { + if (!domainSeparator.startsWith("APTOS::")) { + throw new Error(`Domain separator needs to start with 'APTOS::'. Provided - ${domainSeparator}`); + } + + let prefix = domainSeparatorCache.get(domainSeparator); + if (prefix === undefined) { + prefix = sha3Hash.create().update(textEncoder.encode(domainSeparator)).digest(); + if (domainSeparatorCache.size >= MAX_DOMAIN_SEPARATOR_CACHE) { + const oldest = domainSeparatorCache.keys().next().value; + if (oldest !== undefined) domainSeparatorCache.delete(oldest); + } + domainSeparatorCache.set(domainSeparator, prefix); + } + + const mergedArray = new Uint8Array(prefix.length + bytes.length); + mergedArray.set(prefix); + mergedArray.set(bytes, prefix.length); + + return mergedArray; +} + +/** + * Generates the signing message for a high-level transaction wrapper. + * + * This is the primary entry point for computing the bytes that must be signed when + * authorizing an Aptos transaction. It: + * 1. Calls {@link deriveTransactionType} to obtain the correct low-level BCS structure. + * 2. Selects the appropriate domain separator: + * - `RAW_TRANSACTION_WITH_DATA_SALT` for fee-payer or multi-agent transactions. + * - `RAW_TRANSACTION_SALT` for plain single-sender transactions. + * 3. Calls {@link generateSigningMessage} with the serialized bytes and chosen separator. + * + * @param transaction - The high-level transaction wrapper to generate a signing message for. + * @returns The signing message bytes that every required signer must sign. + * + * @example + * ```typescript + * const signingMessage = generateSigningMessageForTransaction(simpleTransaction); + * const signature = account.sign(signingMessage); + * ``` + */ +export function generateSigningMessageForTransaction(transaction: AnyRawTransaction): Uint8Array { + const rawTxn = deriveTransactionType(transaction); + if (transaction.feePayerAddress) { + return generateSigningMessage(rawTxn.bcsToBytes(), RAW_TRANSACTION_WITH_DATA_SALT); + } + if (transaction.secondarySignerAddresses) { + return generateSigningMessage(rawTxn.bcsToBytes(), RAW_TRANSACTION_WITH_DATA_SALT); + } + return generateSigningMessage(rawTxn.bcsToBytes(), RAW_TRANSACTION_SALT); +} diff --git a/v10/src/transactions/simple-transaction.ts b/v10/src/transactions/simple-transaction.ts new file mode 100644 index 000000000..6bef98ec6 --- /dev/null +++ b/v10/src/transactions/simple-transaction.ts @@ -0,0 +1,93 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { Deserializer } from "../bcs/deserializer.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; +import { AccountAddress } from "../core/account-address.js"; +import { RawTransaction } from "./raw-transaction.js"; + +/** + * A high-level wrapper around a single-sender transaction, optionally with a fee payer. + * + * `SimpleTransaction` is the most common transaction type used in the SDK. It encapsulates a + * {@link RawTransaction} and, when present, the address of an account that will sponsor the + * gas fees. Use {@link MultiAgentTransaction} when the transaction requires additional + * secondary signers. + * + * The `secondarySignerAddresses` property is always `undefined` on this class and serves as a + * discriminant against `MultiAgentTransaction` in union type checks. + * + * @example + * ```typescript + * // Simple transaction without a fee payer + * const simpleTxn = new SimpleTransaction(rawTransaction); + * + * // Simple transaction with a fee payer (sponsored transaction) + * const sponsoredTxn = new SimpleTransaction(rawTransaction, feePayerAddress); + * ``` + */ +export class SimpleTransaction extends Serializable { + /** The underlying unsigned transaction. */ + public rawTransaction: RawTransaction; + + /** + * Address of the fee payer account, if this is a sponsored transaction. + * + * When `undefined` the sender pays their own gas fees. + */ + public feePayerAddress?: AccountAddress | undefined; + + /** + * Always `undefined` on `SimpleTransaction`. + * + * This property exists solely to discriminate `SimpleTransaction` from + * `MultiAgentTransaction` in union type checks without a runtime `instanceof` test. + */ + // Used for type discrimination vs MultiAgentTransaction + public readonly secondarySignerAddresses: undefined; + + /** + * Creates a new `SimpleTransaction`. + * + * @param rawTransaction - The core unsigned transaction to wrap. + * @param feePayerAddress - Optional address of the account that will pay gas fees. + */ + constructor(rawTransaction: RawTransaction, feePayerAddress?: AccountAddress) { + super(); + this.rawTransaction = rawTransaction; + this.feePayerAddress = feePayerAddress; + } + + /** + * Serializes this transaction to BCS bytes. + * + * The format is: `rawTransaction | bool(feePayerPresent) [| feePayerAddress]`. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + this.rawTransaction.serialize(serializer); + if (this.feePayerAddress === undefined) { + serializer.serializeBool(false); + } else { + serializer.serializeBool(true); + this.feePayerAddress.serialize(serializer); + } + } + + /** + * Deserializes a `SimpleTransaction` from BCS bytes. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `SimpleTransaction` instance. + */ + static deserialize(deserializer: Deserializer): SimpleTransaction { + const rawTransaction = RawTransaction.deserialize(deserializer); + const feePayerPresent = deserializer.deserializeBool(); + let feePayerAddress: AccountAddress | undefined; + if (feePayerPresent) { + feePayerAddress = AccountAddress.deserialize(deserializer); + } + return new SimpleTransaction(rawTransaction, feePayerAddress); + } +} diff --git a/v10/src/transactions/transaction-payload.ts b/v10/src/transactions/transaction-payload.ts new file mode 100644 index 000000000..25fcc8a53 --- /dev/null +++ b/v10/src/transactions/transaction-payload.ts @@ -0,0 +1,939 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { Deserializer } from "../bcs/deserializer.js"; +import { Bool, I8, I16, I32, I64, I128, I256, U8, U16, U32, U64, U128, U256 } from "../bcs/move-primitives.js"; +import { EntryFunctionBytes, MoveVector, Serialized } from "../bcs/move-structs.js"; +import { Serializable, type Serializer } from "../bcs/serializer.js"; +import type { AnyNumber, EntryFunctionArgument, ScriptFunctionArgument, TransactionArgument } from "../bcs/types.js"; +import { ScriptTransactionArgumentVariants } from "../bcs/types.js"; +import { AccountAddress } from "../core/account-address.js"; +import { Identifier, TypeTag } from "../core/type-tag.js"; +import { ModuleId } from "./module-id.js"; +import type { MoveModuleId } from "./types.js"; +import { + TransactionExecutableVariants, + TransactionExtraConfigVariants, + TransactionInnerPayloadVariants, + TransactionPayloadVariants, +} from "./types.js"; + +// ── Script argument deserialization ── + +/** + * Deserializes a single script transaction argument from BCS bytes. + * + * Script arguments are prefixed with a ULEB128-encoded variant tag that identifies the + * concrete Move primitive type. This function reads that tag and dispatches to the + * appropriate type's deserializer. + * + * @param deserializer - The BCS deserializer positioned at the start of a script argument. + * @returns The deserialized `TransactionArgument` (a Move primitive or serialized value). + * @throws Error if the variant index does not correspond to a known script argument type. + */ +export function deserializeFromScriptArgument(deserializer: Deserializer): TransactionArgument { + const index = deserializer.deserializeUleb128AsU32(); + switch (index) { + case ScriptTransactionArgumentVariants.U8: + return U8.deserialize(deserializer); + case ScriptTransactionArgumentVariants.U64: + return U64.deserialize(deserializer); + case ScriptTransactionArgumentVariants.U128: + return U128.deserialize(deserializer); + case ScriptTransactionArgumentVariants.Address: + return AccountAddress.deserialize(deserializer); + case ScriptTransactionArgumentVariants.U8Vector: + return MoveVector.deserialize(deserializer, U8); + case ScriptTransactionArgumentVariants.Bool: + return Bool.deserialize(deserializer); + case ScriptTransactionArgumentVariants.U16: + return U16.deserialize(deserializer); + case ScriptTransactionArgumentVariants.U32: + return U32.deserialize(deserializer); + case ScriptTransactionArgumentVariants.U256: + return U256.deserialize(deserializer); + case ScriptTransactionArgumentVariants.Serialized: + return Serialized.deserialize(deserializer); + case ScriptTransactionArgumentVariants.I8: + return I8.deserialize(deserializer); + case ScriptTransactionArgumentVariants.I16: + return I16.deserialize(deserializer); + case ScriptTransactionArgumentVariants.I32: + return I32.deserialize(deserializer); + case ScriptTransactionArgumentVariants.I64: + return I64.deserialize(deserializer); + case ScriptTransactionArgumentVariants.I128: + return I128.deserialize(deserializer); + case ScriptTransactionArgumentVariants.I256: + return I256.deserialize(deserializer); + default: + throw new Error(`Unknown variant index for ScriptTransactionArgument: ${index}`); + } +} + +// ── TransactionPayload ── + +/** + * Abstract base class for all transaction payloads. + * + * Concrete subclasses represent the different types of actions that can be executed: + * - {@link TransactionPayloadScript} – a compiled Move script with arbitrary logic. + * - {@link TransactionPayloadEntryFunction} – a call to a named public entry function. + * - {@link TransactionPayloadMultiSig} – an action to be executed by a multisig account. + * - {@link TransactionInnerPayload} – an orderless (v2) payload variant. + * + * The variant is encoded as a ULEB128-prefixed discriminant during BCS serialization. + */ +export abstract class TransactionPayload extends Serializable { + abstract serialize(serializer: Serializer): void; + + /** + * Deserializes a `TransactionPayload` from BCS bytes, dispatching to the correct + * concrete subclass based on the ULEB128 variant prefix. + * + * @param deserializer - The BCS deserializer to read from. + * @returns The deserialized payload instance. + * @throws Error if the variant index is unknown. + */ + static deserialize(deserializer: Deserializer): TransactionPayload { + const index = deserializer.deserializeUleb128AsU32(); + switch (index) { + case TransactionPayloadVariants.Script: + return TransactionPayloadScript.load(deserializer); + case TransactionPayloadVariants.EntryFunction: + return TransactionPayloadEntryFunction.load(deserializer); + case TransactionPayloadVariants.Multisig: + return TransactionPayloadMultiSig.load(deserializer); + case TransactionPayloadVariants.Payload: + return TransactionInnerPayload.deserialize(deserializer); + default: + throw new Error(`Unknown variant index for TransactionPayload: ${index}`); + } + } +} + +// ── TransactionPayloadScript ── + +/** + * A transaction payload that executes a compiled Move script. + * + * Scripts allow arbitrary on-chain logic that is not limited to pre-deployed entry + * functions. The compiled bytecode is included directly in the transaction. + * + * @example + * ```typescript + * const payload = new TransactionPayloadScript( + * new Script(bytecode, typeArgs, scriptArgs), + * ); + * ``` + */ +export class TransactionPayloadScript extends TransactionPayload { + /** The compiled Move script to execute. */ + public readonly script: Script; + + /** + * Creates a new `TransactionPayloadScript`. + * + * @param script - The compiled Move script to execute. + */ + constructor(script: Script) { + super(); + this.script = script; + } + + /** + * Serializes this payload with its variant prefix followed by the script. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionPayloadVariants.Script); + this.script.serialize(serializer); + } + + /** + * Deserializes a `TransactionPayloadScript` from BCS bytes (after the variant prefix has + * already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `TransactionPayloadScript` instance. + */ + static load(deserializer: Deserializer): TransactionPayloadScript { + const script = Script.deserialize(deserializer); + return new TransactionPayloadScript(script); + } +} + +// ── TransactionPayloadEntryFunction ── + +/** + * A transaction payload that calls a named public entry function on a deployed Move module. + * + * This is the most commonly used payload type when interacting with smart contracts on + * Aptos. The function is identified by its module and name, and arguments are passed as + * BCS-encoded bytes. + * + * @example + * ```typescript + * const payload = new TransactionPayloadEntryFunction( + * EntryFunction.build("0x1::coin", "transfer", [coinTypeTag], [recipient, amount]), + * ); + * ``` + */ +export class TransactionPayloadEntryFunction extends TransactionPayload { + /** The entry function call to execute. */ + public readonly entryFunction: EntryFunction; + + /** + * Creates a new `TransactionPayloadEntryFunction`. + * + * @param entryFunction - The entry function call descriptor. + */ + constructor(entryFunction: EntryFunction) { + super(); + this.entryFunction = entryFunction; + } + + /** + * Serializes this payload with its variant prefix followed by the entry function. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionPayloadVariants.EntryFunction); + this.entryFunction.serialize(serializer); + } + + /** + * Deserializes a `TransactionPayloadEntryFunction` from BCS bytes (after the variant + * prefix has already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `TransactionPayloadEntryFunction` instance. + */ + static load(deserializer: Deserializer): TransactionPayloadEntryFunction { + const entryFunction = EntryFunction.deserialize(deserializer); + return new TransactionPayloadEntryFunction(entryFunction); + } +} + +// ── TransactionPayloadMultiSig ── + +/** + * A transaction payload that submits a multisig action for execution. + * + * This payload type interacts with on-chain multisig accounts. The action to be + * executed may be stored on-chain (omit `transaction_payload`) or provided inline. + * + * @example + * ```typescript + * const payload = new TransactionPayloadMultiSig( + * new MultiSig(multisigAccountAddress), + * ); + * ``` + */ +export class TransactionPayloadMultiSig extends TransactionPayload { + /** The multisig execution descriptor. */ + public readonly multiSig: MultiSig; + + /** + * Creates a new `TransactionPayloadMultiSig`. + * + * @param multiSig - The multisig execution descriptor. + */ + constructor(multiSig: MultiSig) { + super(); + this.multiSig = multiSig; + } + + /** + * Serializes this payload with its variant prefix followed by the multisig descriptor. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionPayloadVariants.Multisig); + this.multiSig.serialize(serializer); + } + + /** + * Deserializes a `TransactionPayloadMultiSig` from BCS bytes (after the variant prefix + * has already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `TransactionPayloadMultiSig` instance. + */ + static load(deserializer: Deserializer): TransactionPayloadMultiSig { + const value = MultiSig.deserialize(deserializer); + return new TransactionPayloadMultiSig(value); + } +} + +// ── EntryFunction ── + +/** + * Describes a call to a public entry function on a deployed Move module. + * + * An `EntryFunction` captures the fully-qualified function reference (module + name), + * any type arguments (generics), and the BCS-encoded argument values. + * + * Prefer the static {@link EntryFunction.build} factory method over the constructor when + * working with string-based identifiers. + * + * @example + * ```typescript + * const entryFunction = EntryFunction.build( + * "0x1::coin", + * "transfer", + * [new TypeTagStruct(coinStructTag)], + * [recipient, new U64(amount)], + * ); + * ``` + */ +export class EntryFunction { + /** The module that contains the entry function. */ + public readonly module_name: ModuleId; + + /** The name of the entry function within the module. */ + public readonly function_name: Identifier; + + /** Type arguments supplied to the generic function parameters. */ + public readonly type_args: Array; + + /** BCS-encoded argument values for the function parameters. */ + public readonly args: Array; + + /** + * Creates a new `EntryFunction` from already-parsed components. + * + * @param module_name - The module identifier. + * @param function_name - The function name identifier. + * @param type_args - Generic type arguments. + * @param args - BCS-encoded function arguments. + */ + constructor( + module_name: ModuleId, + function_name: Identifier, + type_args: Array, + args: Array, + ) { + this.module_name = module_name; + this.function_name = function_name; + this.type_args = type_args; + this.args = args; + } + + /** + * Creates an `EntryFunction` from string-based identifiers. + * + * This factory method is more ergonomic than the constructor when module and function + * names are known at compile time as string literals. + * + * @param module_id - The module identifier in `
::` format. + * @param function_name - The name of the entry function. + * @param type_args - Generic type arguments. + * @param args - BCS-encodable function arguments. + * @returns A new `EntryFunction` instance. + * + * @example + * ```typescript + * const fn = EntryFunction.build("0x1::coin", "transfer", [typeTag], [recipient, amount]); + * ``` + */ + static build( + module_id: MoveModuleId, + function_name: string, + type_args: Array, + args: Array, + ): EntryFunction { + return new EntryFunction(ModuleId.fromStr(module_id), new Identifier(function_name), type_args, args); + } + + /** + * Serializes this entry function call to BCS bytes. + * + * Each argument is serialized using its `serializeForEntryFunction` method, which + * length-prefixes the raw BCS bytes. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + this.module_name.serialize(serializer); + this.function_name.serialize(serializer); + serializer.serializeVector(this.type_args); + serializer.serializeU32AsUleb128(this.args.length); + this.args.forEach((item: EntryFunctionArgument) => { + item.serializeForEntryFunction(serializer); + }); + } + + /** + * Deserializes an `EntryFunction` from BCS bytes. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `EntryFunction` instance. + */ + static deserialize(deserializer: Deserializer): EntryFunction { + const module_name = ModuleId.deserialize(deserializer); + const function_name = Identifier.deserialize(deserializer); + const type_args = deserializer.deserializeVector(TypeTag); + + const length = deserializer.deserializeUleb128AsU32(); + const args: Array = [] as EntryFunctionBytes[]; + + for (let i = 0; i < length; i += 1) { + const fixedBytesLength = deserializer.deserializeUleb128AsU32(); + const fixedBytes = EntryFunctionBytes.deserialize(deserializer, fixedBytesLength); + args.push(fixedBytes); + } + + return new EntryFunction(module_name, function_name, type_args, args); + } +} + +// ── Script ── + +/** + * A compiled Move script along with its type arguments and runtime arguments. + * + * Scripts are arbitrary on-chain programs that are compiled to Move bytecode and included + * directly in a transaction. Unlike entry functions they are not stored on-chain, which + * makes them suitable for one-off or complex multi-step operations. + * + * @example + * ```typescript + * const script = new Script(compiledBytecode, [typeArg], [new U64(100n)]); + * ``` + */ +export class Script { + /** The compiled Move bytecode of the script. */ + public readonly bytecode: Uint8Array; + + /** Type arguments supplied to the generic script parameters. */ + public readonly type_args: Array; + + /** Runtime arguments passed to the script's `main` function. */ + public readonly args: Array; + + /** + * Creates a new `Script`. + * + * @param bytecode - The compiled Move bytecode. + * @param type_args - Generic type arguments. + * @param args - Runtime arguments for the script. + */ + constructor(bytecode: Uint8Array, type_args: Array, args: Array) { + this.bytecode = bytecode; + this.type_args = type_args; + this.args = args; + } + + /** + * Serializes this script to BCS bytes. + * + * Each argument is serialized using its `serializeForScriptFunction` method. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.bytecode); + serializer.serializeVector(this.type_args); + serializer.serializeU32AsUleb128(this.args.length); + this.args.forEach((item: ScriptFunctionArgument) => { + item.serializeForScriptFunction(serializer); + }); + } + + /** + * Deserializes a `Script` from BCS bytes. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `Script` instance. + */ + static deserialize(deserializer: Deserializer): Script { + const bytecode = deserializer.deserializeBytes(); + const type_args = deserializer.deserializeVector(TypeTag); + const length = deserializer.deserializeUleb128AsU32(); + const args: ScriptFunctionArgument[] = []; + for (let i = 0; i < length; i += 1) { + const scriptArgument = deserializeFromScriptArgument(deserializer); + args.push(scriptArgument); + } + return new Script(bytecode, type_args, args); + } +} + +// ── MultiSig ── + +/** + * Descriptor for an action to be executed through an on-chain multisig account. + * + * A `MultiSig` payload points to the multisig account address that should execute the + * action. The actual payload may be stored on-chain (omit `transaction_payload`) or + * supplied inline for immediate execution. + * + * @example + * ```typescript + * // Execute with an on-chain stored payload + * const multiSig = new MultiSig(multisigAccountAddress); + * + * // Execute with an inline payload + * const multiSig = new MultiSig(multisigAccountAddress, new MultiSigTransactionPayload(entryFn)); + * ``` + */ +export class MultiSig { + /** The on-chain address of the multisig account that will execute the action. */ + public readonly multisig_address: AccountAddress; + + /** + * Optional inline payload to execute. + * + * When `undefined` the multisig account's next queued transaction is executed. + */ + public readonly transaction_payload?: MultiSigTransactionPayload; + + /** + * Creates a new `MultiSig` descriptor. + * + * @param multisig_address - The address of the multisig account. + * @param transaction_payload - Optional inline payload to execute. + */ + constructor(multisig_address: AccountAddress, transaction_payload?: MultiSigTransactionPayload) { + this.multisig_address = multisig_address; + this.transaction_payload = transaction_payload; + } + + /** + * Serializes this multisig descriptor to BCS bytes. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + this.multisig_address.serialize(serializer); + if (this.transaction_payload === undefined) { + serializer.serializeBool(false); + } else { + serializer.serializeBool(true); + this.transaction_payload.serialize(serializer); + } + } + + /** + * Deserializes a `MultiSig` from BCS bytes. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `MultiSig` instance. + */ + static deserialize(deserializer: Deserializer): MultiSig { + const multisig_address = AccountAddress.deserialize(deserializer); + const payloadPresent = deserializer.deserializeBool(); + let transaction_payload: MultiSigTransactionPayload | undefined; + if (payloadPresent) { + transaction_payload = MultiSigTransactionPayload.deserialize(deserializer); + } + return new MultiSig(multisig_address, transaction_payload); + } +} + +// ── MultiSigTransactionPayload ── + +/** + * An inline payload for a multisig account transaction. + * + * Currently only {@link EntryFunction} payloads are supported. This class wraps an + * `EntryFunction` and BCS-serializes it with a variant prefix that the multisig account + * Move module uses to dispatch the call. + * + * @example + * ```typescript + * const payload = new MultiSigTransactionPayload( + * EntryFunction.build("0x1::coin", "transfer", [typeTag], [recipient, amount]), + * ); + * ``` + */ +export class MultiSigTransactionPayload extends Serializable { + /** The entry function call to execute through the multisig account. */ + public readonly transaction_payload: EntryFunction; + + /** + * Creates a new `MultiSigTransactionPayload`. + * + * @param transaction_payload - The entry function call to execute. + */ + constructor(transaction_payload: EntryFunction) { + super(); + this.transaction_payload = transaction_payload; + } + + /** + * Serializes this payload with a variant prefix (always 0 for `EntryFunction`). + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + // Currently only EntryFunction is supported + serializer.serializeU32AsUleb128(0); + this.transaction_payload.serialize(serializer); + } + + /** + * Deserializes a `MultiSigTransactionPayload` from BCS bytes. + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `MultiSigTransactionPayload` instance. + */ + static deserialize(deserializer: Deserializer): MultiSigTransactionPayload { + const variant = deserializer.deserializeUleb128AsU32(); + if (variant !== 0) { + throw new Error(`Unknown MultiSigTransactionPayload variant: ${variant}. Only EntryFunction (0) is supported.`); + } + return new MultiSigTransactionPayload(EntryFunction.deserialize(deserializer)); + } +} + +// ── Orderless transaction types ── + +/** + * Abstract base class for orderless (v2) transaction inner payloads. + * + * Orderless transactions do not use a sequence number for replay protection; instead they + * use a nonce or are identified by their content. The inner payload carries both the + * executable (entry function, script, or empty) and extra configuration. + * + * Concrete subclasses: {@link TransactionInnerPayloadV1}. + */ +export abstract class TransactionInnerPayload extends TransactionPayload { + abstract serialize(serializer: Serializer): void; + + /** + * Deserializes a `TransactionInnerPayload` from BCS bytes, dispatching to the correct + * versioned subclass. + * + * @param deserializer - The BCS deserializer to read from. + * @returns The deserialized inner payload instance. + * @throws Error if the version variant is unknown. + */ + static deserialize(deserializer: Deserializer): TransactionInnerPayload { + const index = deserializer.deserializeUleb128AsU32(); + switch (index) { + case TransactionInnerPayloadVariants.V1: + return TransactionInnerPayloadV1.load(deserializer); + default: + throw new Error(`Unknown variant index for TransactionInnerPayload: ${index}`); + } + } +} + +/** + * Version 1 of the orderless transaction inner payload. + * + * Combines a {@link TransactionExecutable} (what to run) with a + * {@link TransactionExtraConfig} (replay protection nonce and optional multisig address). + * + * @example + * ```typescript + * const payload = new TransactionInnerPayloadV1( + * new TransactionExecutableEntryFunction(entryFunction), + * new TransactionExtraConfigV1(undefined, replayProtectionNonce), + * ); + * ``` + */ +export class TransactionInnerPayloadV1 extends TransactionInnerPayload { + /** The executable portion of the payload. */ + executable: TransactionExecutable; + + /** Extra configuration such as replay-protection nonce and multisig address. */ + extra_config: TransactionExtraConfig; + + /** + * Creates a new `TransactionInnerPayloadV1`. + * + * @param executable - The executable (entry function, script, or empty). + * @param extra_config - Additional configuration for the orderless transaction. + */ + constructor(executable: TransactionExecutable, extra_config: TransactionExtraConfig) { + super(); + this.executable = executable; + this.extra_config = extra_config; + } + + /** + * Serializes this payload with the outer `Payload` variant prefix, the inner `V1` version + * prefix, then the executable and extra config. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionPayloadVariants.Payload); + serializer.serializeU32AsUleb128(TransactionInnerPayloadVariants.V1); + this.executable.serialize(serializer); + this.extra_config.serialize(serializer); + } + + /** + * Deserializes a `TransactionInnerPayloadV1` from BCS bytes (after both the outer and + * inner variant prefixes have already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `TransactionInnerPayloadV1` instance. + */ + static load(deserializer: Deserializer): TransactionInnerPayloadV1 { + const executable = TransactionExecutable.deserialize(deserializer); + const extra_config = TransactionExtraConfig.deserialize(deserializer); + return new TransactionInnerPayloadV1(executable, extra_config); + } +} + +/** + * Abstract base class for the executable portion of an orderless transaction payload. + * + * Concrete subclasses: + * - {@link TransactionExecutableScript} – executes a compiled Move script. + * - {@link TransactionExecutableEntryFunction} – calls a deployed entry function. + * - {@link TransactionExecutableEmpty} – carries no executable content. + */ +export abstract class TransactionExecutable { + abstract serialize(serializer: Serializer): void; + + /** + * Deserializes a `TransactionExecutable` from BCS bytes, dispatching to the correct + * concrete subclass based on the ULEB128 variant prefix. + * + * @param deserializer - The BCS deserializer to read from. + * @returns The deserialized executable instance. + * @throws Error if the variant index is unknown. + */ + static deserialize(deserializer: Deserializer): TransactionExecutable { + const index = deserializer.deserializeUleb128AsU32(); + switch (index) { + case TransactionExecutableVariants.Script: + return TransactionExecutableScript.load(deserializer); + case TransactionExecutableVariants.EntryFunction: + return TransactionExecutableEntryFunction.load(deserializer); + case TransactionExecutableVariants.Empty: + return TransactionExecutableEmpty.load(deserializer); + default: + throw new Error(`Unknown variant index for TransactionExecutable: ${index}`); + } + } +} + +/** + * An orderless transaction executable that runs a compiled Move script. + * + * @example + * ```typescript + * const executable = new TransactionExecutableScript( + * new Script(bytecode, typeArgs, scriptArgs), + * ); + * ``` + */ +export class TransactionExecutableScript extends TransactionExecutable { + /** The compiled Move script to execute. */ + script: Script; + + /** + * Creates a new `TransactionExecutableScript`. + * + * @param script - The compiled Move script to execute. + */ + constructor(script: Script) { + super(); + this.script = script; + } + + /** + * Serializes this executable with its variant prefix followed by the script. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionExecutableVariants.Script); + this.script.serialize(serializer); + } + + /** + * Deserializes a `TransactionExecutableScript` from BCS bytes (after the variant prefix + * has already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `TransactionExecutableScript` instance. + */ + static load(deserializer: Deserializer): TransactionExecutableScript { + const script = Script.deserialize(deserializer); + return new TransactionExecutableScript(script); + } +} + +/** + * An orderless transaction executable that calls a deployed entry function. + * + * @example + * ```typescript + * const executable = new TransactionExecutableEntryFunction( + * EntryFunction.build("0x1::coin", "transfer", [typeTag], [recipient, amount]), + * ); + * ``` + */ +export class TransactionExecutableEntryFunction extends TransactionExecutable { + /** The entry function call descriptor. */ + entryFunction: EntryFunction; + + /** + * Creates a new `TransactionExecutableEntryFunction`. + * + * @param entryFunction - The entry function call to execute. + */ + constructor(entryFunction: EntryFunction) { + super(); + this.entryFunction = entryFunction; + } + + /** + * Serializes this executable with its variant prefix followed by the entry function. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionExecutableVariants.EntryFunction); + this.entryFunction.serialize(serializer); + } + + /** + * Deserializes a `TransactionExecutableEntryFunction` from BCS bytes (after the variant + * prefix has already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `TransactionExecutableEntryFunction` instance. + */ + static load(deserializer: Deserializer): TransactionExecutableEntryFunction { + const entryFunction = EntryFunction.deserialize(deserializer); + return new TransactionExecutableEntryFunction(entryFunction); + } +} + +/** + * An orderless transaction executable that carries no executable content. + * + * Used when the transaction payload consists solely of extra configuration without an + * associated script or entry function call. + */ +export class TransactionExecutableEmpty extends TransactionExecutable { + /** + * Serializes this executable with its variant prefix (no additional bytes). + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionExecutableVariants.Empty); + } + + /** + * Deserializes a `TransactionExecutableEmpty` from BCS bytes (after the variant prefix + * has already been consumed). + * + * @param _ - The BCS deserializer (not read from, included for interface consistency). + * @returns A new `TransactionExecutableEmpty` instance. + */ + static load(_: Deserializer): TransactionExecutableEmpty { + return new TransactionExecutableEmpty(); + } +} + +/** + * Abstract base class for extra configuration attached to orderless transaction payloads. + * + * Concrete subclasses: {@link TransactionExtraConfigV1}. + */ +export abstract class TransactionExtraConfig { + abstract serialize(serializer: Serializer): void; + + /** + * Deserializes a `TransactionExtraConfig` from BCS bytes, dispatching to the correct + * versioned subclass. + * + * @param deserializer - The BCS deserializer to read from. + * @returns The deserialized extra config instance. + * @throws Error if the version variant is unknown. + */ + static deserialize(deserializer: Deserializer): TransactionExtraConfig { + const index = deserializer.deserializeUleb128AsU32(); + switch (index) { + case TransactionExtraConfigVariants.V1: + return TransactionExtraConfigV1.load(deserializer); + default: + throw new Error(`Unknown variant index for TransactionExtraConfig: ${index}`); + } + } +} + +/** + * Version 1 of the orderless transaction extra configuration. + * + * Optionally carries a multisig account address (for multisig orderless transactions) and + * a replay-protection nonce that uniquely identifies the transaction without requiring a + * sequence number. + * + * @example + * ```typescript + * // With a replay protection nonce only + * const extraConfig = new TransactionExtraConfigV1(undefined, 42n); + * + * // With both multisig address and nonce + * const extraConfig = new TransactionExtraConfigV1(multisigAddress, 42n); + * ``` + */ +export class TransactionExtraConfigV1 extends TransactionExtraConfig { + /** + * Optional on-chain multisig account address. + * + * When set, the transaction is executed through the specified multisig account. + */ + multisigAddress?: AccountAddress; + + /** + * Optional replay-protection nonce. + * + * Replaces the sequence number for orderless transactions. Must be unique per sender + * within the nonce's validity window. + */ + replayProtectionNonce?: bigint; + + /** + * Creates a new `TransactionExtraConfigV1`. + * + * @param multisigAddress - Optional address of the multisig account to execute through. + * @param replayProtectionNonce - Optional nonce for replay protection (replaces sequence number). + */ + constructor(multisigAddress?: AccountAddress, replayProtectionNonce?: AnyNumber) { + super(); + this.multisigAddress = multisigAddress; + this.replayProtectionNonce = replayProtectionNonce !== undefined ? BigInt(replayProtectionNonce) : undefined; + } + + /** + * Serializes this extra config with its version prefix and optional fields. + * + * @param serializer - The BCS serializer to write into. + */ + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(TransactionExtraConfigVariants.V1); + serializer.serializeOption(this.multisigAddress); + serializer.serializeOption( + this.replayProtectionNonce !== undefined ? new U64(this.replayProtectionNonce) : undefined, + ); + } + + /** + * Deserializes a `TransactionExtraConfigV1` from BCS bytes (after the version prefix has + * already been consumed). + * + * @param deserializer - The BCS deserializer to read from. + * @returns A new `TransactionExtraConfigV1` instance. + */ + static load(deserializer: Deserializer): TransactionExtraConfigV1 { + const multisigAddress = deserializer.deserializeOption(AccountAddress); + const replayProtectionNonce = deserializer.deserializeOption(U64); + return new TransactionExtraConfigV1(multisigAddress, replayProtectionNonce?.value); + } +} diff --git a/v10/src/transactions/types.ts b/v10/src/transactions/types.ts new file mode 100644 index 000000000..0113f2e70 --- /dev/null +++ b/v10/src/transactions/types.ts @@ -0,0 +1,172 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +// ── Variant enums for transaction payloads ── + +/** + * Discriminant values used to identify the variant of a serialized {@link TransactionPayload}. + * + * These numeric constants are written as ULEB128-encoded prefix bytes during BCS serialization + * and are read back during deserialization to select the correct concrete payload type. + */ +export enum TransactionPayloadVariants { + Script = 0, + // Deprecated: ModuleBundle = 1, + EntryFunction = 2, + Multisig = 3, + Payload = 4, +} + +/** + * Discriminant values for versioned inner payload types used in orderless transactions. + * + * Currently only `V1` is defined. + */ +export enum TransactionInnerPayloadVariants { + V1 = 0, +} + +/** + * Discriminant values for the executable portion of an orderless transaction payload. + * + * Identifies whether the payload executes a {@link Script}, an {@link EntryFunction}, + * or carries no executable content (`Empty`). + */ +export enum TransactionExecutableVariants { + Script = 0, + EntryFunction = 1, + Empty = 2, +} + +/** + * Discriminant values for the extra configuration attached to an orderless transaction payload. + * + * Currently only `V1` is defined. + */ +export enum TransactionExtraConfigVariants { + V1 = 0, +} + +// ── Variant enums for raw transaction with data ── + +/** + * Discriminant values used to identify whether a raw transaction carries additional signer data. + * + * - `MultiAgentTransaction` – the transaction has one or more secondary signers. + * - `FeePayerTransaction` – the transaction has a designated fee payer account. + */ +export enum TransactionVariants { + MultiAgentTransaction = 0, + FeePayerTransaction = 1, +} + +// ── Variant enums for authenticators ── + +/** + * Discriminant values for the top-level transaction authenticator variants. + * + * Written as a ULEB128 prefix during BCS serialization of a {@link TransactionAuthenticator} + * subclass. + */ +export enum TransactionAuthenticatorVariant { + Ed25519 = 0, + MultiEd25519 = 1, + MultiAgent = 2, + FeePayer = 3, + SingleSender = 4, +} + +/** + * Discriminant values for the per-account authenticator variants. + * + * Written as a ULEB128 prefix during BCS serialization of an {@link AccountAuthenticator} + * subclass. + */ +export enum AccountAuthenticatorVariant { + Ed25519 = 0, + MultiEd25519 = 1, + SingleKey = 2, + MultiKey = 3, + NoAccountAuthenticator = 4, + Abstraction = 5, +} + +// ── Variant enums for account abstraction ── + +/** + * Discriminant values for the abstract authentication data format. + * + * - `V1` – standard abstraction where the authenticating function is fully specified. + * - `DerivableV1` – derivable abstraction that additionally carries an account identity payload. + */ +export enum AbstractAuthenticationDataVariant { + V1 = 0, + DerivableV1 = 1, +} + +/** + * Discriminant values for the account-abstraction signing data wrapper. + * + * Currently only `V1` is defined. + */ +export enum AASigningDataVariant { + V1 = 0, +} + +// ── Type aliases used in transaction APIs ── + +/** + * A fully-qualified Move module identifier in the form `
::`. + * + * @example + * ```typescript + * const moduleId: MoveModuleId = "0x1::coin"; + * ``` + */ +export type MoveModuleId = `${string}::${string}`; + +/** + * A fully-qualified Move struct identifier in the form `
::::`. + * + * @example + * ```typescript + * const structId: MoveStructId = "0x1::coin::CoinStore"; + * ``` + */ +export type MoveStructId = `${string}::${string}::${string}`; + +/** + * A fully-qualified Move function identifier in the form `
::::`. + * + * This is a type alias for {@link MoveStructId} because Move function identifiers share the + * same three-part `address::module::name` structure. + * + * @example + * ```typescript + * const functionId: MoveFunctionId = "0x1::coin::transfer"; + * ``` + */ +export type MoveFunctionId = MoveStructId; + +// ── Aggregate transaction types ── + +import type { MultiAgentTransaction } from "./multi-agent-transaction.js"; +import type { FeePayerRawTransaction, MultiAgentRawTransaction, RawTransaction } from "./raw-transaction.js"; +import type { SimpleTransaction } from "./simple-transaction.js"; + +/** + * Union of the two high-level transaction wrappers that the SDK works with. + * + * - {@link SimpleTransaction} – single-sender transaction, optionally with a fee payer. + * - {@link MultiAgentTransaction} – transaction with one or more secondary signers. + */ +export type AnyRawTransaction = SimpleTransaction | MultiAgentTransaction; + +/** + * Union of the low-level BCS-serializable raw transaction types. + * + * - {@link RawTransaction} – plain single-sender transaction bytes. + * - {@link MultiAgentRawTransaction} – bytes with secondary signer addresses appended. + * - {@link FeePayerRawTransaction} – bytes with secondary signer addresses and fee payer address appended. + */ +export type AnyRawTransactionInstance = RawTransaction | MultiAgentRawTransaction | FeePayerRawTransaction; diff --git a/v10/src/version.ts b/v10/src/version.ts new file mode 100644 index 000000000..e5f12d19d --- /dev/null +++ b/v10/src/version.ts @@ -0,0 +1 @@ +export const VERSION = "10.0.0"; diff --git a/v10/tests/e2e/api/api.e2e.test.ts b/v10/tests/e2e/api/api.e2e.test.ts new file mode 100644 index 000000000..3fd0584b9 --- /dev/null +++ b/v10/tests/e2e/api/api.e2e.test.ts @@ -0,0 +1,149 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { beforeAll, describe, expect, it } from "vitest"; +import type { Ed25519Account } from "../../../src/account/ed25519-account.js"; +import { generateAccount } from "../../../src/account/factory.js"; +import { Aptos } from "../../../src/api/index.js"; +import { U64 } from "../../../src/bcs/move-primitives.js"; +import { AccountAddress } from "../../../src/core/account-address.js"; +import { Network } from "../../../src/core/network.js"; + +const aptos = new Aptos({ network: Network.LOCAL }); + +describe("General API E2E", () => { + it("getLedgerInfo returns valid data", async () => { + const info = await aptos.general.getLedgerInfo(); + expect(info.chain_id).toBe(4); + expect(Number(info.ledger_version)).toBeGreaterThanOrEqual(0); + expect(Number(info.block_height)).toBeGreaterThanOrEqual(0); + }); + + it("getChainId returns 4 for local", async () => { + const chainId = await aptos.general.getChainId(); + expect(chainId).toBe(4); + }); + + it("getGasPriceEstimation returns gas estimate", async () => { + const gas = await aptos.general.getGasPriceEstimation(); + expect(gas.gas_estimate).toBeGreaterThan(0); + }); + + it("getBlockByHeight returns a block", async () => { + const block = await aptos.general.getBlockByHeight(1); + expect(block.block_height).toBe("1"); + expect(block.block_hash).toBeDefined(); + }); +}); + +describe("Account API E2E", () => { + let alice: Ed25519Account; + + beforeAll(async () => { + alice = generateAccount() as Ed25519Account; + await aptos.faucet.fund(alice.accountAddress, 100_000_000); + }); + + it("getInfo returns account data", async () => { + const info = await aptos.account.getInfo(alice.accountAddress); + expect(info.sequence_number).toBeDefined(); + expect(info.authentication_key).toBeDefined(); + }); + + it("getModules returns modules for 0x1", async () => { + const modules = await aptos.account.getModules("0x1", { limit: 5 }); + expect(modules.length).toBeGreaterThan(0); + expect(modules[0].bytecode).toBeDefined(); + }); + + it("getModule returns a specific module", async () => { + const mod = await aptos.account.getModule("0x1", "coin"); + expect(mod.abi?.name).toBe("coin"); + }); + + it("getResource works for framework accounts", async () => { + // 0x1 always has the Account resource + const resource = await aptos.account.getResource<{ authentication_key: string; sequence_number: string }>( + "0x1", + "0x1::account::Account", + ); + expect(resource.sequence_number).toBeDefined(); + expect(resource.authentication_key).toBeDefined(); + }); +}); + +describe("Transaction API E2E", () => { + let alice: Ed25519Account; + let bob: Ed25519Account; + + beforeAll(async () => { + alice = generateAccount() as Ed25519Account; + bob = generateAccount() as Ed25519Account; + await aptos.faucet.fund(alice.accountAddress, 100_000_000); + }); + + it("builds, signs, submits, and waits for a transaction", async () => { + const tx = await aptos.transaction.buildSimple(alice.accountAddress, { + function: "0x1::aptos_account::transfer", + typeArguments: [], + functionArguments: [AccountAddress.from(bob.accountAddress), new U64(1_000)], + }); + + const pending = await aptos.transaction.signAndSubmit(alice, tx); + expect(pending.hash).toBeDefined(); + + const committed = await aptos.transaction.waitForTransaction(pending.hash, { checkSuccess: true }); + expect(committed).toBeDefined(); + expect("success" in committed && committed.success).toBe(true); + }); + + it("getByHash returns the transaction", async () => { + // First submit a transaction + const tx = await aptos.transaction.buildSimple(alice.accountAddress, { + function: "0x1::aptos_account::transfer", + typeArguments: [], + functionArguments: [AccountAddress.from(bob.accountAddress), new U64(500)], + }); + const pending = await aptos.transaction.signAndSubmit(alice, tx); + await aptos.transaction.waitForTransaction(pending.hash); + + const fetched = await aptos.transaction.getByHash(pending.hash); + expect(fetched.hash).toBe(pending.hash); + }); + + it("getAccountTransactions returns history", async () => { + const txns = await aptos.account.getTransactions(alice.accountAddress, { limit: 10 }); + expect(txns.length).toBeGreaterThan(0); + }); +}); + +describe("View Function E2E", () => { + let alice: Ed25519Account; + + beforeAll(async () => { + alice = generateAccount() as Ed25519Account; + await aptos.faucet.fund(alice.accountAddress, 100_000_000); + }); + + it("calls a view function", async () => { + const result = await aptos.general.view({ + function: "0x1::coin::balance", + type_arguments: ["0x1::aptos_coin::AptosCoin"], + arguments: [alice.accountAddress.toString()], + }); + expect(Number(result[0])).toBeGreaterThan(0); + }); +}); + +describe("Faucet E2E", () => { + it("funds a new account", async () => { + const account = generateAccount() as Ed25519Account; + const txn = await aptos.faucet.fund(account.accountAddress, 50_000_000); + expect(txn.hash).toBeDefined(); + expect(txn.success).toBe(true); + + // Verify account exists by fetching its info + const info = await aptos.account.getInfo(account.accountAddress); + expect(info.sequence_number).toBe("0"); + }); +}); diff --git a/v10/tests/e2e/compat/compat.e2e.test.ts b/v10/tests/e2e/compat/compat.e2e.test.ts new file mode 100644 index 000000000..26086980e --- /dev/null +++ b/v10/tests/e2e/compat/compat.e2e.test.ts @@ -0,0 +1,170 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +// Tests that verify v6-style API calling conventions work through the compat layer. + +import { beforeAll, describe, expect, it } from "vitest"; +import type { Ed25519Account } from "../../../src/account/ed25519-account.js"; +import { generateAccount } from "../../../src/account/factory.js"; +import { AptosConfig } from "../../../src/api/config.js"; +import { U64 } from "../../../src/bcs/move-primitives.js"; +import { Aptos } from "../../../src/compat/aptos.js"; +import { AccountAddress } from "../../../src/core/account-address.js"; +import { Network } from "../../../src/core/network.js"; + +// v6-style: new Aptos(new AptosConfig({ network })) +const config = new AptosConfig({ network: Network.LOCAL }); +const aptos = new Aptos(config); + +describe("Compat: General API (v6 style)", () => { + it("getLedgerInfo()", async () => { + const info = await aptos.getLedgerInfo(); + expect(info.chain_id).toBe(4); + expect(Number(info.ledger_version)).toBeGreaterThanOrEqual(0); + }); + + it("getChainId()", async () => { + const chainId = await aptos.getChainId(); + expect(chainId).toBe(4); + }); + + it("getGasPriceEstimation()", async () => { + const gas = await aptos.getGasPriceEstimation(); + expect(gas.gas_estimate).toBeGreaterThan(0); + }); + + it("getBlockByHeight({ blockHeight })", async () => { + const block = await aptos.getBlockByHeight({ blockHeight: 1 }); + expect(block.block_height).toBe("1"); + expect(block.block_hash).toBeDefined(); + }); + + it("view({ payload })", async () => { + const alice = generateAccount() as Ed25519Account; + await aptos.fundAccount({ accountAddress: alice.accountAddress, amount: 100_000_000 }); + + const result = await aptos.view({ + payload: { + function: "0x1::coin::balance", + type_arguments: ["0x1::aptos_coin::AptosCoin"], + arguments: [alice.accountAddress.toString()], + }, + }); + expect(Number(result[0])).toBeGreaterThan(0); + }); +}); + +describe("Compat: Account API (v6 style)", () => { + let alice: Ed25519Account; + + beforeAll(async () => { + alice = generateAccount() as Ed25519Account; + await aptos.fundAccount({ accountAddress: alice.accountAddress, amount: 100_000_000 }); + }); + + it("getAccountInfo({ accountAddress })", async () => { + const info = await aptos.getAccountInfo({ accountAddress: alice.accountAddress }); + expect(info.sequence_number).toBeDefined(); + expect(info.authentication_key).toBeDefined(); + }); + + it("getAccountModules({ accountAddress, options })", async () => { + const modules = await aptos.getAccountModules({ accountAddress: "0x1", options: { limit: 5 } }); + expect(modules.length).toBeGreaterThan(0); + expect(modules[0].bytecode).toBeDefined(); + }); + + it("getAccountModule({ accountAddress, moduleName })", async () => { + const mod = await aptos.getAccountModule({ accountAddress: "0x1", moduleName: "coin" }); + expect(mod.abi?.name).toBe("coin"); + }); + + it("getAccountResource({ accountAddress, resourceType })", async () => { + const resource = await aptos.getAccountResource<{ authentication_key: string; sequence_number: string }>({ + accountAddress: "0x1", + resourceType: "0x1::account::Account", + }); + expect(resource.sequence_number).toBeDefined(); + expect(resource.authentication_key).toBeDefined(); + }); +}); + +describe("Compat: Transaction API (v6 style)", () => { + let alice: Ed25519Account; + let bob: Ed25519Account; + + beforeAll(async () => { + alice = generateAccount() as Ed25519Account; + bob = generateAccount() as Ed25519Account; + await aptos.fundAccount({ accountAddress: alice.accountAddress, amount: 100_000_000 }); + }); + + it("transaction.build.simple({ sender, data }) — v6 nested pattern", async () => { + const tx = await aptos.transaction.build.simple({ + sender: alice.accountAddress, + data: { + function: "0x1::aptos_account::transfer", + typeArguments: [], + functionArguments: [AccountAddress.from(bob.accountAddress), new U64(1_000)], + }, + }); + + // v6-style sign and submit + const pending = await aptos.signAndSubmitTransaction({ signer: alice, transaction: tx }); + expect(pending.hash).toBeDefined(); + + const committed = await aptos.waitForTransaction({ + transactionHash: pending.hash, + options: { checkSuccess: true }, + }); + expect(committed).toBeDefined(); + expect("success" in committed && committed.success).toBe(true); + }); + + it("getTransactionByHash({ transactionHash })", async () => { + const tx = await aptos.transaction.build.simple({ + sender: alice.accountAddress, + data: { + function: "0x1::aptos_account::transfer", + typeArguments: [], + functionArguments: [AccountAddress.from(bob.accountAddress), new U64(500)], + }, + }); + const pending = await aptos.signAndSubmitTransaction({ signer: alice, transaction: tx }); + await aptos.waitForTransaction({ transactionHash: pending.hash }); + + const fetched = await aptos.getTransactionByHash({ transactionHash: pending.hash }); + expect(fetched.hash).toBe(pending.hash); + }); + + it("getAccountTransactions({ accountAddress })", async () => { + const txns = await aptos.getAccountTransactions({ accountAddress: alice.accountAddress, options: { limit: 10 } }); + expect(txns.length).toBeGreaterThan(0); + }); +}); + +describe("Compat: Faucet API (v6 style)", () => { + it("fundAccount({ accountAddress, amount })", async () => { + const account = generateAccount() as Ed25519Account; + const txn = await aptos.fundAccount({ accountAddress: account.accountAddress, amount: 50_000_000 }); + expect(txn.hash).toBeDefined(); + expect(txn.success).toBe(true); + + const info = await aptos.getAccountInfo({ accountAddress: account.accountAddress }); + expect(info.sequence_number).toBe("0"); + }); +}); + +describe("Compat: v10 namespaced API still works", () => { + it("aptos.general.getLedgerInfo() — v10 native access", async () => { + const info = await aptos.general.getLedgerInfo(); + expect(info.chain_id).toBe(4); + }); + + it("aptos.account.getInfo() — v10 native access", async () => { + const alice = generateAccount() as Ed25519Account; + await aptos.fundAccount({ accountAddress: alice.accountAddress, amount: 100_000_000 }); + const info = await aptos.account.getInfo(alice.accountAddress); + expect(info.sequence_number).toBeDefined(); + }); +}); diff --git a/v10/tests/e2e/local-node.ts b/v10/tests/e2e/local-node.ts new file mode 100644 index 000000000..c5fd4342b --- /dev/null +++ b/v10/tests/e2e/local-node.ts @@ -0,0 +1,71 @@ +// Simple local testnet runner for v10 E2E tests +// Starts `aptos node run-localnet` and waits for readiness + +import { type ChildProcess, spawn } from "node:child_process"; + +const READINESS_URL = "http://127.0.0.1:8070/"; +const MAX_WAIT_SEC = 75; + +let process: ChildProcess | null = null; + +export async function startLocalNode(): Promise { + // Check if already running + if (await isNodeUp()) { + console.log("[v10 e2e] Local node already running"); + return; + } + + console.log("[v10 e2e] Starting local node..."); + process = spawn("npx", ["aptos", "node", "run-localnet", "--force-restart", "--assume-yes"], { + env: { ...globalThis.process.env, ENABLE_KEYLESS_DEFAULT: "1" }, + stdio: "pipe", + }); + + process.stdout?.on("data", (data) => { + const str = data.toString(); + if (str.includes("Setup is complete")) { + console.log("[v10 e2e] Local node setup complete"); + } + }); + + process.stderr?.on("data", (data) => { + // Suppress most stderr noise, only log errors + const str = data.toString(); + if (str.includes("error") || str.includes("Error")) { + console.error("[v10 e2e] Node error:", str.trim()); + } + }); + + // Wait for readiness + const start = Date.now(); + while (Date.now() - start < MAX_WAIT_SEC * 1000) { + if (await isNodeUp()) { + console.log("[v10 e2e] Local node is ready"); + return; + } + await new Promise((r) => setTimeout(r, 1000)); + } + + throw new Error(`Local node failed to start within ${MAX_WAIT_SEC}s`); +} + +export async function stopLocalNode(): Promise { + if (process?.pid) { + console.log("[v10 e2e] Stopping local node..."); + try { + process.kill("SIGTERM"); + } catch { + // Process may already be dead + } + process = null; + } +} + +async function isNodeUp(): Promise { + try { + const response = await fetch(READINESS_URL); + return response.status === 200; + } catch { + return false; + } +} diff --git a/v10/tests/e2e/setup.ts b/v10/tests/e2e/setup.ts new file mode 100644 index 000000000..01d043102 --- /dev/null +++ b/v10/tests/e2e/setup.ts @@ -0,0 +1,10 @@ +// Global setup/teardown for E2E tests +import { startLocalNode, stopLocalNode } from "./local-node.js"; + +export async function setup() { + await startLocalNode(); +} + +export async function teardown() { + await stopLocalNode(); +} diff --git a/v10/tests/unit/account/account.test.ts b/v10/tests/unit/account/account.test.ts new file mode 100644 index 000000000..102ef6306 --- /dev/null +++ b/v10/tests/unit/account/account.test.ts @@ -0,0 +1,267 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it, test } from "vitest"; +import { Ed25519Account } from "../../../src/account/ed25519-account.js"; +import { + accountFromDerivationPath, + accountFromPrivateKey, + authKey, + generateAccount, +} from "../../../src/account/factory.js"; +import { MultiEd25519Account } from "../../../src/account/multi-ed25519-account.js"; +import { MultiKeyAccount } from "../../../src/account/multi-key-account.js"; +import { SingleKeyAccount } from "../../../src/account/single-key-account.js"; +// Import from core barrel to trigger auth key factory registration +import { AccountAddress, AuthenticationKey } from "../../../src/core/index.js"; +import { Ed25519PrivateKey, Ed25519PublicKey } from "../../../src/crypto/ed25519.js"; +import { MultiEd25519PublicKey } from "../../../src/crypto/multi-ed25519.js"; +import { MultiKey } from "../../../src/crypto/multi-key.js"; +import { Secp256k1PrivateKey, Secp256k1PublicKey } from "../../../src/crypto/secp256k1.js"; +import { AnyPublicKey } from "../../../src/crypto/single-key.js"; +import { SigningScheme, SigningSchemeInput } from "../../../src/crypto/types.js"; + +// ── Test fixtures ── + +const ed25519 = { + privateKey: "ed25519-priv-0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5", + publicKey: "0xde19e5d1880cac87d57484ce9ed2e84cf0f9599f12e7cc3a52e4e7657a763f2c", + address: "0x978c213990c4833df71548df7ce49d54c759d6b6d932de22b24d56060b7af2aa", + messageEncoded: "68656c6c6f20776f726c64", + stringMessage: "hello world", + signatureHex: + "0x9e653d56a09247570bb174a389e85b9226abd5c403ea6c504b386626a145158cd4efd66fc5e071c0e19538a96a05ddbda24d3c51e1e6a9dacc6bb1ce775cce07", +}; + +const singleSignerED25519 = { + publicKey: "0xe425451a5dc888ac871976c3c724dec6118910e7d11d344b4b07a22cd94e8c2e", + privateKey: "ed25519-priv-0xf508cbef4e0fe463204aab724a90791c9a9dbe60a53b4978bbddbc712b55f2fd", + address: "0x5bdf77d5bf826c8c04273d4e7323f7bc4a85ee7ee34b37bd7458b7aed3639dd3", + messageEncoded: "68656c6c6f20776f726c64", + signatureHex: + "0xc6f50f4e0cb1961f6f7b28be1a1d80e3ece240dfbb7bd8a8b03cc26bfd144fc176295d7c322c5bf3d9669d2ad49d8bdbfe77254b4a6393d8c49da04b40cee600", +}; + +const secp256k1TestObject = { + privateKey: "secp256k1-priv-0xd107155adf816a0a94c6db3c9489c13ad8a1eda7ada2e558ba3bfa47c020347e", + publicKey: + "0x04acdd16651b839c24665b7e2033b55225f384554949fef46c397b5275f37f6ee95554d70fb5d9f93c5831ebf695c7206e7477ce708f03ae9bb2862dc6c9e033ea", + address: "0x5792c985bc96f436270bd2a3c692210b09c7febb8889345ceefdbae4bacfe498", + messageEncoded: "68656c6c6f20776f726c64", + stringMessage: "hello world", + signatureHex: + "0xd0d634e843b61339473b028105930ace022980708b2855954b977da09df84a770c0b68c29c8ca1b5409a5085b0ec263be80e433c83fcf6debb82f3447e71edca", +}; + +const walletFixture = { + address: "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", + mnemonic: "shoot island position soft burden budget tooth cruel issue economy destroy above", + path: "m/44'/637'/0'/0'/0'", +}; + +const ed25519WalletTestObject = { + address: "0x28b829b524d7c24aa7fd8916573c814df766dae542f724e1cf8914536232c346", + mnemonic: "shoot island position soft burden budget tooth cruel issue economy destroy above", + path: "m/44'/637'/0'/0'/0'", +}; + +const secp256k1WalletTestObject = { + address: "0x4b4aa8759fcef40ba49e999409eb73a98252f44f6612a4de2b23bad5c37b15a6", + mnemonic: "shoot island position soft burden budget tooth cruel issue economy destroy above", + path: "m/44'/637'/0'/0/0", +}; + +// ── Tests ── + +describe("generateAccount", () => { + it("creates a legacy Ed25519 account by default", () => { + const account = generateAccount(); + expect(account).toBeInstanceOf(Ed25519Account); + expect(account.publicKey).toBeInstanceOf(Ed25519PublicKey); + expect(account.signingScheme).toEqual(SigningScheme.Ed25519); + }); + + it("creates a SingleKey Ed25519 account when legacy=false", () => { + const account = generateAccount({ scheme: SigningSchemeInput.Ed25519, legacy: false }); + expect(account).toBeInstanceOf(SingleKeyAccount); + expect(account.publicKey).toBeInstanceOf(AnyPublicKey); + expect(account.signingScheme).toEqual(SigningScheme.SingleKey); + }); + + it("creates a SingleKey Secp256k1 account", () => { + const account = generateAccount({ scheme: SigningSchemeInput.Secp256k1Ecdsa }); + expect(account).toBeInstanceOf(SingleKeyAccount); + expect(account.publicKey).toBeInstanceOf(AnyPublicKey); + expect(account.signingScheme).toEqual(SigningScheme.SingleKey); + }); +}); + +describe("accountFromPrivateKey", () => { + it("derives correct legacy Ed25519 account", () => { + const privateKey = new Ed25519PrivateKey(ed25519.privateKey); + const account = accountFromPrivateKey({ privateKey }); + expect(account).toBeInstanceOf(Ed25519Account); + expect(account.publicKey).toBeInstanceOf(Ed25519PublicKey); + expect(account.publicKey.toString()).toEqual(new Ed25519PublicKey(ed25519.publicKey).toString()); + expect(account.accountAddress.toString()).toEqual(ed25519.address); + }); + + it("derives correct legacy Ed25519 account with explicit address", () => { + const privateKey = new Ed25519PrivateKey(ed25519.privateKey); + const address = AccountAddress.from(ed25519.address); + const account = accountFromPrivateKey({ privateKey, address, legacy: true }); + expect(account).toBeInstanceOf(Ed25519Account); + expect(account.accountAddress.toString()).toEqual(ed25519.address); + }); + + it("derives correct SingleKey Ed25519 account when legacy=false", () => { + const privateKey = new Ed25519PrivateKey(singleSignerED25519.privateKey); + const account = accountFromPrivateKey({ privateKey, legacy: false }); + expect(account).toBeInstanceOf(SingleKeyAccount); + expect(account.publicKey).toBeInstanceOf(AnyPublicKey); + expect(account.accountAddress.toString()).toEqual(singleSignerED25519.address); + }); + + it("derives correct SingleKey Secp256k1 account", () => { + const privateKey = new Secp256k1PrivateKey(secp256k1TestObject.privateKey); + const account = accountFromPrivateKey({ privateKey }); + expect(account).toBeInstanceOf(SingleKeyAccount); + expect(account.publicKey).toBeInstanceOf(AnyPublicKey); + expect((account.publicKey as AnyPublicKey).publicKey).toBeInstanceOf(Secp256k1PublicKey); + expect(account.accountAddress.toString()).toEqual(secp256k1TestObject.address); + }); +}); + +describe("accountFromDerivationPath", () => { + it("derives legacy Ed25519 account from mnemonic", () => { + const account = accountFromDerivationPath({ + path: walletFixture.path, + mnemonic: walletFixture.mnemonic, + scheme: SigningSchemeInput.Ed25519, + }); + expect(account.accountAddress.toString()).toEqual(walletFixture.address); + }); + + it("derives SingleKey Ed25519 account from mnemonic", () => { + const account = accountFromDerivationPath({ + path: ed25519WalletTestObject.path, + mnemonic: ed25519WalletTestObject.mnemonic, + scheme: SigningSchemeInput.Ed25519, + legacy: false, + }); + expect(account.accountAddress.toString()).toEqual(ed25519WalletTestObject.address); + }); + + it("derives SingleKey Secp256k1 account from mnemonic", () => { + const account = accountFromDerivationPath({ + path: secp256k1WalletTestObject.path, + mnemonic: secp256k1WalletTestObject.mnemonic, + scheme: SigningSchemeInput.Secp256k1Ecdsa, + }); + expect(account.accountAddress.toString()).toEqual(secp256k1WalletTestObject.address); + }); +}); + +describe("sign and verify", () => { + it("signs and verifies with legacy Ed25519", () => { + const privateKey = new Ed25519PrivateKey(ed25519.privateKey); + const account = accountFromPrivateKey({ privateKey, legacy: true }) as Ed25519Account; + const signature = account.sign(ed25519.messageEncoded); + expect(signature.toString()).toEqual(ed25519.signatureHex); + expect(account.verifySignature({ message: ed25519.messageEncoded, signature })).toBe(true); + }); + + it("signs and verifies with SingleKey Ed25519", () => { + const privateKey = new Ed25519PrivateKey(singleSignerED25519.privateKey); + const account = accountFromPrivateKey({ privateKey, legacy: false }) as SingleKeyAccount; + const signature = account.sign(singleSignerED25519.messageEncoded); + expect(signature.signature.toString()).toEqual(singleSignerED25519.signatureHex); + expect(account.verifySignature({ message: singleSignerED25519.messageEncoded, signature })).toBe(true); + }); + + it("signs and verifies with SingleKey Secp256k1", () => { + const privateKey = new Secp256k1PrivateKey(secp256k1TestObject.privateKey); + const account = accountFromPrivateKey({ privateKey }) as SingleKeyAccount; + const signature = account.sign(secp256k1TestObject.messageEncoded); + expect(signature.signature.toString()).toEqual(secp256k1TestObject.signatureHex); + expect(account.verifySignature({ message: secp256k1TestObject.messageEncoded, signature })).toBe(true); + }); + + describe("MultiKey", () => { + const signer1 = generateAccount({ scheme: SigningSchemeInput.Ed25519, legacy: false }) as SingleKeyAccount; + const signer2 = generateAccount({ scheme: SigningSchemeInput.Secp256k1Ecdsa }) as SingleKeyAccount; + const signer3 = generateAccount({ scheme: SigningSchemeInput.Ed25519, legacy: false }) as SingleKeyAccount; + + const multiKey = new MultiKey({ + publicKeys: [signer1.publicKey, signer2.publicKey, signer3.publicKey], + signaturesRequired: 2, + }); + + it("signs and verifies with 2-of-3 MultiKey", () => { + const account = new MultiKeyAccount({ + multiKey, + signers: [signer1, signer2], + }); + const message = "test message"; + const signature = account.sign(message); + expect(account.verifySignature({ message, signature })).toBe(true); + }); + + test("throws on insufficient signers", () => { + expect(() => new MultiKeyAccount({ multiKey, signers: [signer1] })).toThrow(); + }); + + test("throws on too many signers", () => { + expect(() => new MultiKeyAccount({ multiKey, signers: [signer1, signer2, signer3] })).toThrow(); + }); + }); + + describe("MultiEd25519", () => { + const pk1 = Ed25519PrivateKey.generate(); + const pk2 = Ed25519PrivateKey.generate(); + const pk3 = Ed25519PrivateKey.generate(); + + const multiKey = new MultiEd25519PublicKey({ + publicKeys: [pk1.publicKey(), pk2.publicKey(), pk3.publicKey()], + threshold: 2, + }); + + it("signs and verifies with 2-of-3 MultiEd25519", () => { + const account = new MultiEd25519Account({ + publicKey: multiKey, + signers: [pk1, pk3], + }); + const message = "test message"; + const signature = account.sign(message); + expect(account.verifySignature({ message, signature })).toBe(true); + }); + + it("signs and verifies with misordered signers", () => { + const account = new MultiEd25519Account({ + publicKey: multiKey, + signers: [pk3, pk2], + }); + const message = "test message"; + const signature = account.sign(message); + expect(account.verifySignature({ message, signature })).toBe(true); + }); + + test("throws on insufficient signers", () => { + expect(() => new MultiEd25519Account({ publicKey: multiKey, signers: [pk1] })).toThrow(); + }); + }); +}); + +describe("authKey", () => { + it("derives correct address from Ed25519 public key", () => { + const publicKey = new Ed25519PublicKey(ed25519.publicKey); + const key = authKey({ publicKey }); + expect(key.derivedAddress().toString()).toBe(ed25519.address); + }); + + it("derives correct address from AuthenticationKey.fromPublicKey", () => { + const publicKey = new Ed25519PublicKey(ed25519.publicKey); + const key = AuthenticationKey.fromPublicKey({ publicKey }); + expect(key.derivedAddress().toString()).toBe(ed25519.address); + }); +}); diff --git a/v10/tests/unit/account/ephemeral-key-pair.test.ts b/v10/tests/unit/account/ephemeral-key-pair.test.ts new file mode 100644 index 000000000..856c2ea56 --- /dev/null +++ b/v10/tests/unit/account/ephemeral-key-pair.test.ts @@ -0,0 +1,98 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; +import { EphemeralKeyPair } from "../../../src/account/ephemeral-key-pair.js"; +import { Ed25519PrivateKey } from "../../../src/crypto/ed25519.js"; + +const PRIVATE_KEY_HEX = "ed25519-priv-0x1111111111111111111111111111111111111111111111111111111111111111"; + +describe("EphemeralKeyPair", () => { + it("generates a new key pair", () => { + const ekp = EphemeralKeyPair.generate(); + expect(ekp.getPublicKey()).toBeDefined(); + expect(ekp.nonce).toBeDefined(); + expect(ekp.expiryDateSecs).toBeGreaterThan(0); + expect(ekp.blinder.length).toBe(31); + }); + + it("creates from private key with custom expiry", () => { + const privateKey = new Ed25519PrivateKey(PRIVATE_KEY_HEX); + const ekp = new EphemeralKeyPair({ + privateKey, + expiryDateSecs: 9876543210, + blinder: new Uint8Array(31), + }); + expect(ekp.expiryDateSecs).toBe(9876543210); + expect(ekp.isExpired()).toBe(false); + }); + + it("detects expired key pair", () => { + const privateKey = new Ed25519PrivateKey(PRIVATE_KEY_HEX); + const ekp = new EphemeralKeyPair({ + privateKey, + expiryDateSecs: 10, // long in the past + blinder: new Uint8Array(31), + }); + expect(ekp.isExpired()).toBe(true); + }); + + it("serializes and deserializes", () => { + const privateKey = new Ed25519PrivateKey(PRIVATE_KEY_HEX); + const ekp = new EphemeralKeyPair({ + privateKey, + expiryDateSecs: 9876543210, + blinder: new Uint8Array(31), + }); + + const bytes = ekp.bcsToBytes(); + const restored = EphemeralKeyPair.fromBytes(bytes); + + expect(restored.nonce).toEqual(ekp.nonce); + expect(restored.expiryDateSecs).toEqual(ekp.expiryDateSecs); + expect(restored.blinder).toEqual(ekp.blinder); + }); + + it("signs data when not expired", () => { + const privateKey = new Ed25519PrivateKey(PRIVATE_KEY_HEX); + const ekp = new EphemeralKeyPair({ + privateKey, + expiryDateSecs: 9876543210, + blinder: new Uint8Array(31), + }); + const sig = ekp.sign("68656c6c6f"); + expect(sig).toBeDefined(); + }); + + it("throws when signing with expired key pair", () => { + const privateKey = new Ed25519PrivateKey(PRIVATE_KEY_HEX); + const ekp = new EphemeralKeyPair({ + privateKey, + expiryDateSecs: 10, + blinder: new Uint8Array(31), + }); + expect(() => ekp.sign("68656c6c6f")).toThrow("expired"); + }); + + it("clears key material", () => { + const ekp = EphemeralKeyPair.generate(); + expect(ekp.isCleared()).toBe(false); + ekp.clear(); + expect(ekp.isCleared()).toBe(true); + expect(() => ekp.sign("68656c6c6f")).toThrow("cleared"); + }); + + it("nonce is deterministic for same inputs", () => { + const privateKey = new Ed25519PrivateKey(PRIVATE_KEY_HEX); + const blinder = new Uint8Array(31); + + const ekp1 = new EphemeralKeyPair({ privateKey, expiryDateSecs: 9876543210, blinder }); + const ekp2 = new EphemeralKeyPair({ + privateKey: new Ed25519PrivateKey(PRIVATE_KEY_HEX), + expiryDateSecs: 9876543210, + blinder: new Uint8Array(31), + }); + + expect(ekp1.nonce).toEqual(ekp2.nonce); + }); +}); diff --git a/v10/tests/unit/api/api.test.ts b/v10/tests/unit/api/api.test.ts new file mode 100644 index 000000000..e1cfd3682 --- /dev/null +++ b/v10/tests/unit/api/api.test.ts @@ -0,0 +1,293 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { Aptos, AptosConfig } from "../../../src/api/index.js"; +import type { AccountData, GasEstimation, LedgerInfo, TransactionResponse } from "../../../src/api/types.js"; +import { RoleType } from "../../../src/api/types.js"; +import { Network } from "../../../src/core/network.js"; + +// Mock @aptos-labs/aptos-client — all HTTP requests flow through this +vi.mock("@aptos-labs/aptos-client", () => ({ + jsonRequest: vi.fn(), + bcsRequest: vi.fn(), +})); + +import { jsonRequest } from "@aptos-labs/aptos-client"; + +const mockClient = vi.mocked(jsonRequest); + +/** Build a mock AptosClientResponse for JSON calls. */ +function mockJsonResponse(data: unknown, status = 200, headers: Record = {}) { + return { status, statusText: status === 200 ? "OK" : "Error", data, headers }; +} + +describe("Aptos facade", () => { + it("creates with default config", () => { + const aptos = new Aptos(); + expect(aptos.config.network).toBe(Network.DEVNET); + }); + + it("creates with AptosSettings", () => { + const aptos = new Aptos({ network: Network.TESTNET }); + expect(aptos.config.network).toBe(Network.TESTNET); + }); + + it("creates with AptosConfig instance", () => { + const config = new AptosConfig({ network: Network.LOCAL }); + const aptos = new Aptos(config); + expect(aptos.config.network).toBe(Network.LOCAL); + }); + + it("has all namespace sub-objects", () => { + const aptos = new Aptos(); + expect(aptos.general).toBeDefined(); + expect(aptos.account).toBeDefined(); + expect(aptos.transaction).toBeDefined(); + expect(aptos.coin).toBeDefined(); + expect(aptos.faucet).toBeDefined(); + expect(aptos.table).toBeDefined(); + }); +}); + +describe("General API", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("getLedgerInfo", async () => { + const ledgerInfo: LedgerInfo = { + chain_id: 4, + epoch: "1", + ledger_version: "100", + oldest_ledger_version: "0", + ledger_timestamp: "1000", + node_role: RoleType.FULL_NODE, + oldest_block_height: "0", + block_height: "50", + }; + mockClient.mockResolvedValueOnce(mockJsonResponse(ledgerInfo)); + + const aptos = new Aptos({ network: Network.LOCAL }); + const result = await aptos.general.getLedgerInfo(); + expect(result.chain_id).toBe(4); + expect(result.ledger_version).toBe("100"); + }); + + it("getChainId", async () => { + mockClient.mockResolvedValueOnce( + mockJsonResponse({ + chain_id: 4, + epoch: "1", + ledger_version: "0", + oldest_ledger_version: "0", + ledger_timestamp: "0", + node_role: "full_node", + oldest_block_height: "0", + block_height: "0", + }), + ); + + const aptos = new Aptos({ network: Network.LOCAL }); + const chainId = await aptos.general.getChainId(); + expect(chainId).toBe(4); + }); + + it("getGasPriceEstimation", async () => { + const gasEstimation: GasEstimation = { + gas_estimate: 100, + deprioritized_gas_estimate: 50, + prioritized_gas_estimate: 200, + }; + mockClient.mockResolvedValueOnce(mockJsonResponse(gasEstimation)); + + const aptos = new Aptos({ network: Network.LOCAL }); + const result = await aptos.general.getGasPriceEstimation(); + expect(result.gas_estimate).toBe(100); + }); + + it("getBlockByHeight", async () => { + mockClient.mockResolvedValueOnce( + mockJsonResponse({ + block_height: "10", + block_hash: "0xabc", + block_timestamp: "1000", + first_version: "1", + last_version: "10", + }), + ); + + const aptos = new Aptos({ network: Network.LOCAL }); + const block = await aptos.general.getBlockByHeight(10); + expect(block.block_height).toBe("10"); + }); + + it("view function", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse(["100"])); + + const aptos = new Aptos({ network: Network.LOCAL }); + const result = await aptos.general.view({ + function: "0x1::coin::balance", + type_arguments: ["0x1::aptos_coin::AptosCoin"], + arguments: ["0x1"], + }); + expect(result).toEqual(["100"]); + }); +}); + +describe("Account API", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("getInfo", async () => { + const accountData: AccountData = { + sequence_number: "42", + authentication_key: "0x1234", + }; + mockClient.mockResolvedValueOnce(mockJsonResponse(accountData)); + + const aptos = new Aptos({ network: Network.LOCAL }); + const result = await aptos.account.getInfo("0x1"); + expect(result.sequence_number).toBe("42"); + }); + + it("getModule", async () => { + mockClient.mockResolvedValueOnce( + mockJsonResponse({ + bytecode: "0x01", + abi: { address: "0x1", name: "coin", friends: [], exposed_functions: [], structs: [] }, + }), + ); + + const aptos = new Aptos({ network: Network.LOCAL }); + const mod = await aptos.account.getModule("0x1", "coin"); + expect(mod.abi?.name).toBe("coin"); + }); + + it("getResource", async () => { + mockClient.mockResolvedValueOnce( + mockJsonResponse({ type: "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", data: { coin: { value: "100" } } }), + ); + + const aptos = new Aptos({ network: Network.LOCAL }); + const resource = await aptos.account.getResource<{ coin: { value: string } }>( + "0x1", + "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>" as `${string}::${string}::${string}`, + ); + expect(resource.coin.value).toBe("100"); + }); +}); + +describe("Transaction API", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("getByHash", async () => { + mockClient.mockResolvedValueOnce( + mockJsonResponse({ + type: "user_transaction", + version: "1", + hash: "0xabc", + state_change_hash: "0x", + event_root_hash: "0x", + gas_used: "100", + success: true, + vm_status: "Executed successfully", + accumulator_root_hash: "0x", + changes: [], + sender: "0x1", + sequence_number: "0", + max_gas_amount: "200000", + gas_unit_price: "100", + expiration_timestamp_secs: "100", + payload: {}, + events: [], + timestamp: "1000", + }), + ); + + const aptos = new Aptos({ network: Network.LOCAL }); + const txn = await aptos.transaction.getByHash("0xabc"); + expect(txn.hash).toBe("0xabc"); + }); + + it("getByVersion", async () => { + mockClient.mockResolvedValueOnce( + mockJsonResponse({ + type: "user_transaction", + version: "42", + hash: "0xdef", + state_change_hash: "0x", + event_root_hash: "0x", + gas_used: "100", + success: true, + vm_status: "Executed successfully", + accumulator_root_hash: "0x", + changes: [], + sender: "0x1", + sequence_number: "0", + max_gas_amount: "200000", + gas_unit_price: "100", + expiration_timestamp_secs: "100", + payload: {}, + events: [], + timestamp: "1000", + }), + ); + + const aptos = new Aptos({ network: Network.LOCAL }); + const txn = await aptos.transaction.getByVersion(42); + expect((txn as TransactionResponse & { version?: string }).version).toBe("42"); + }); + + it("buildSimple creates a SimpleTransaction", async () => { + // Mock 3 parallel requests: ledgerInfo, gasEstimation, accountSequenceNumber + mockClient.mockResolvedValueOnce( + mockJsonResponse({ + chain_id: 4, + epoch: "1", + ledger_version: "100", + oldest_ledger_version: "0", + ledger_timestamp: "0", + node_role: "full_node", + oldest_block_height: "0", + block_height: "50", + }), + ); + mockClient.mockResolvedValueOnce( + mockJsonResponse({ gas_estimate: 100, deprioritized_gas_estimate: 50, prioritized_gas_estimate: 200 }), + ); + mockClient.mockResolvedValueOnce(mockJsonResponse({ sequence_number: "5", authentication_key: "0x1234" })); + + const aptos = new Aptos({ network: Network.LOCAL }); + const tx = await aptos.transaction.buildSimple("0x1", { + function: "0x1::aptos_account::transfer", + typeArguments: [], + functionArguments: [], + }); + + expect(tx.rawTransaction).toBeDefined(); + expect(tx.rawTransaction.sender.toString()).toBe("0x1"); + expect(tx.rawTransaction.chain_id.chainId).toBe(4); + }); +}); + +describe("Table API", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("getItem", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse({ value: "42" })); + + const aptos = new Aptos({ network: Network.LOCAL }); + const item = await aptos.table.getItem<{ value: string }>("0xhandle", { + key_type: "address", + value_type: "u64", + key: "0x1", + }); + expect(item.value).toBe("42"); + }); +}); diff --git a/v10/tests/unit/bcs/deserializer.test.ts b/v10/tests/unit/bcs/deserializer.test.ts new file mode 100644 index 000000000..6a2b35268 --- /dev/null +++ b/v10/tests/unit/bcs/deserializer.test.ts @@ -0,0 +1,280 @@ +import { describe, expect, test } from "vitest"; +import { Deserializer } from "../../../src/bcs/deserializer.js"; +import { Serializable, Serializer } from "../../../src/bcs/serializer.js"; + +describe("Deserializer", () => { + // ── Boolean ── + + test("deserializes true", () => { + const d = new Deserializer(new Uint8Array([0x01])); + expect(d.deserializeBool()).toBe(true); + }); + + test("deserializes false", () => { + const d = new Deserializer(new Uint8Array([0x00])); + expect(d.deserializeBool()).toBe(false); + }); + + test("throws on invalid boolean", () => { + const d = new Deserializer(new Uint8Array([0x02])); + expect(() => d.deserializeBool()).toThrow("Invalid boolean value"); + }); + + // ── Unsigned integers ── + + test("deserializes U8", () => { + const d = new Deserializer(new Uint8Array([0xff])); + expect(d.deserializeU8()).toBe(255); + }); + + test("deserializes U16", () => { + const d = new Deserializer(new Uint8Array([0x34, 0x12])); + expect(d.deserializeU16()).toBe(0x1234); + }); + + test("deserializes U32", () => { + const d = new Deserializer(new Uint8Array([0x78, 0x56, 0x34, 0x12])); + expect(d.deserializeU32()).toBe(0x12345678); + }); + + test("deserializes U64", () => { + const d = new Deserializer(new Uint8Array([0x00, 0xef, 0xcd, 0xab, 0x78, 0x56, 0x34, 0x12])); + expect(d.deserializeU64()).toBe(BigInt("1311768467750121216")); + }); + + test("deserializes max U64", () => { + const d = new Deserializer(new Uint8Array(8).fill(0xff)); + expect(d.deserializeU64()).toBe(18446744073709551615n); + }); + + test("deserializes U128", () => { + const bytes = new Uint8Array(16); + bytes[0] = 1; + const d = new Deserializer(bytes); + expect(d.deserializeU128()).toBe(1n); + }); + + test("deserializes U256", () => { + const bytes = new Uint8Array(32); + bytes[0] = 1; + const d = new Deserializer(bytes); + expect(d.deserializeU256()).toBe(1n); + }); + + // ── Signed integers ── + + test("deserializes I8 negative", () => { + const d = new Deserializer(new Uint8Array([0xff])); + expect(d.deserializeI8()).toBe(-1); + }); + + test("deserializes I8 positive", () => { + const d = new Deserializer(new Uint8Array([0x7f])); + expect(d.deserializeI8()).toBe(127); + }); + + test("deserializes I16 negative", () => { + const d = new Deserializer(new Uint8Array([0xff, 0xff])); + expect(d.deserializeI16()).toBe(-1); + }); + + test("deserializes I32 negative", () => { + const d = new Deserializer(new Uint8Array([0xff, 0xff, 0xff, 0xff])); + expect(d.deserializeI32()).toBe(-1); + }); + + test("deserializes I64 negative", () => { + const d = new Deserializer(new Uint8Array(8).fill(0xff)); + expect(d.deserializeI64()).toBe(-1n); + }); + + test("deserializes I64 positive", () => { + const bytes = new Uint8Array(8); + bytes[0] = 1; + const d = new Deserializer(bytes); + expect(d.deserializeI64()).toBe(1n); + }); + + test("deserializes I128 negative", () => { + const d = new Deserializer(new Uint8Array(16).fill(0xff)); + expect(d.deserializeI128()).toBe(-1n); + }); + + test("deserializes I256 negative", () => { + const d = new Deserializer(new Uint8Array(32).fill(0xff)); + expect(d.deserializeI256()).toBe(-1n); + }); + + // ── String / Bytes ── + + test("deserializes string", () => { + const d = new Deserializer(new Uint8Array([8, 49, 50, 51, 52, 97, 98, 99, 100])); + expect(d.deserializeStr()).toBe("1234abcd"); + }); + + test("deserializes empty string", () => { + const d = new Deserializer(new Uint8Array([0])); + expect(d.deserializeStr()).toBe(""); + }); + + test("deserializes bytes", () => { + const d = new Deserializer(new Uint8Array([3, 1, 2, 3])); + expect(d.deserializeBytes()).toEqual(new Uint8Array([1, 2, 3])); + }); + + test("deserializes fixed bytes", () => { + const d = new Deserializer(new Uint8Array([1, 2, 3])); + expect(d.deserializeFixedBytes(3)).toEqual(new Uint8Array([1, 2, 3])); + }); + + test("deserialized bytes are a copy", () => { + const d = new Deserializer(new Uint8Array([3, 1, 2, 3])); + const bytes = d.deserializeBytes(); + bytes[0] = 99; // modifying should not affect deserializer + const d2 = new Deserializer(new Uint8Array([3, 1, 2, 3])); + expect(d2.deserializeBytes()).toEqual(new Uint8Array([1, 2, 3])); + }); + + // ── ULEB128 ── + + test("deserializes ULEB128 single byte", () => { + const d = new Deserializer(new Uint8Array([0x00])); + expect(d.deserializeUleb128AsU32()).toBe(0); + }); + + test("deserializes ULEB128 multi-byte", () => { + const d = new Deserializer(new Uint8Array([0x80, 0x01])); + expect(d.deserializeUleb128AsU32()).toBe(128); + }); + + // ── Composable ── + + test("deserializes vector", () => { + class SimpleU8 extends Serializable { + constructor(public val: number) { + super(); + } + serialize(s: Serializer): void { + s.serializeU8(this.val); + } + static deserialize(d: Deserializer): SimpleU8 { + return new SimpleU8(d.deserializeU8()); + } + } + const d = new Deserializer(new Uint8Array([3, 1, 2, 3])); + const vec = d.deserializeVector(SimpleU8); + expect(vec.map((v) => v.val)).toEqual([1, 2, 3]); + }); + + test("deserializeOption with value", () => { + const d = new Deserializer(new Uint8Array([1, 5, 104, 101, 108, 108, 111])); + expect(d.deserializeOption("string")).toBe("hello"); + }); + + test("deserializeOption without value", () => { + const d = new Deserializer(new Uint8Array([0])); + expect(d.deserializeOption("string")).toBeUndefined(); + }); + + test("deserializeOption bytes", () => { + const d = new Deserializer(new Uint8Array([1, 3, 1, 2, 3])); + expect(d.deserializeOption("bytes")).toEqual(new Uint8Array([1, 2, 3])); + }); + + test("deserializeOption fixedBytes", () => { + const d = new Deserializer(new Uint8Array([1, 1, 2, 3])); + expect(d.deserializeOption("fixedBytes", 3)).toEqual(new Uint8Array([1, 2, 3])); + }); + + test("deserializeOption fixedBytes throws without length", () => { + const d = new Deserializer(new Uint8Array([1, 1, 2, 3])); + // @ts-expect-error testing missing length + expect(() => d.deserializeOption("fixedBytes")).toThrow("Fixed bytes length not provided"); + }); + + // ── Buffer management ── + + test("remaining() tracks position", () => { + const d = new Deserializer(new Uint8Array([1, 2, 3])); + expect(d.remaining()).toBe(3); + d.deserializeU8(); + expect(d.remaining()).toBe(2); + }); + + test("assertFinished passes when buffer is consumed", () => { + const d = new Deserializer(new Uint8Array([1])); + d.deserializeU8(); + expect(() => d.assertFinished()).not.toThrow(); + }); + + test("assertFinished throws when buffer has remaining bytes", () => { + const d = new Deserializer(new Uint8Array([1, 2])); + d.deserializeU8(); + expect(() => d.assertFinished()).toThrow("Buffer has remaining bytes"); + }); + + test("throws when reading past end of buffer", () => { + const d = new Deserializer(new Uint8Array([1])); + d.deserializeU8(); + expect(() => d.deserializeU8()).toThrow("Reached to the end of buffer"); + }); + + test("fromHex creates from hex string", () => { + const d = Deserializer.fromHex("0x01ff"); + expect(d.deserializeU8()).toBe(1); + expect(d.deserializeU8()).toBe(255); + }); +}); + +describe("Serializer/Deserializer round-trip", () => { + test("round-trips all unsigned types", () => { + const s = new Serializer(); + s.serializeU8(42); + s.serializeU16(1234); + s.serializeU32(123456789); + s.serializeU64(9876543210n); + s.serializeU128(99999999999999999999n); + s.serializeU256(1n); + + const d = new Deserializer(s.toUint8Array()); + expect(d.deserializeU8()).toBe(42); + expect(d.deserializeU16()).toBe(1234); + expect(d.deserializeU32()).toBe(123456789); + expect(d.deserializeU64()).toBe(9876543210n); + expect(d.deserializeU128()).toBe(99999999999999999999n); + expect(d.deserializeU256()).toBe(1n); + d.assertFinished(); + }); + + test("round-trips all signed types", () => { + const s = new Serializer(); + s.serializeI8(-42); + s.serializeI16(-1234); + s.serializeI32(-123456789); + s.serializeI64(-9876543210n); + s.serializeI128(-99999999999999999999n); + s.serializeI256(-1n); + + const d = new Deserializer(s.toUint8Array()); + expect(d.deserializeI8()).toBe(-42); + expect(d.deserializeI16()).toBe(-1234); + expect(d.deserializeI32()).toBe(-123456789); + expect(d.deserializeI64()).toBe(-9876543210n); + expect(d.deserializeI128()).toBe(-99999999999999999999n); + expect(d.deserializeI256()).toBe(-1n); + d.assertFinished(); + }); + + test("round-trips string and bytes", () => { + const s = new Serializer(); + s.serializeStr("Hello, World!"); + s.serializeBytes(new Uint8Array([1, 2, 3, 4, 5])); + s.serializeBool(true); + + const d = new Deserializer(s.toUint8Array()); + expect(d.deserializeStr()).toBe("Hello, World!"); + expect(d.deserializeBytes()).toEqual(new Uint8Array([1, 2, 3, 4, 5])); + expect(d.deserializeBool()).toBe(true); + d.assertFinished(); + }); +}); diff --git a/v10/tests/unit/bcs/move-types.test.ts b/v10/tests/unit/bcs/move-types.test.ts new file mode 100644 index 000000000..9fdde5290 --- /dev/null +++ b/v10/tests/unit/bcs/move-types.test.ts @@ -0,0 +1,380 @@ +import { describe, expect, test } from "vitest"; +import { Deserializer } from "../../../src/bcs/deserializer.js"; +import { + Bool, + I8, + I16, + I32, + I64, + I128, + I256, + U8, + U16, + U32, + U64, + U128, + U256, +} from "../../../src/bcs/move-primitives.js"; +import { + EntryFunctionBytes, + FixedBytes, + MoveOption, + MoveString, + MoveVector, + Serialized, +} from "../../../src/bcs/move-structs.js"; +import { Serializer } from "../../../src/bcs/serializer.js"; + +// ── Move Primitives ── + +describe("Bool", () => { + test("stores value", () => { + expect(new Bool(true).value).toBe(true); + expect(new Bool(false).value).toBe(false); + }); + + test("serializes and deserializes", () => { + const b = new Bool(true); + const bytes = b.bcsToBytes(); + expect(bytes).toEqual(new Uint8Array([0x01])); + expect(Bool.deserialize(new Deserializer(bytes)).value).toBe(true); + }); + + test("throws on non-boolean", () => { + // @ts-expect-error testing invalid input + expect(() => new Bool(1)).toThrow(); + }); +}); + +describe("Unsigned integers", () => { + test("U8 round-trip", () => { + const val = new U8(255); + expect(val.value).toBe(255); + const d = new Deserializer(val.bcsToBytes()); + expect(U8.deserialize(d).value).toBe(255); + }); + + test("U16 round-trip", () => { + const val = new U16(65535); + const d = new Deserializer(val.bcsToBytes()); + expect(U16.deserialize(d).value).toBe(65535); + }); + + test("U32 round-trip", () => { + const val = new U32(4294967295); + const d = new Deserializer(val.bcsToBytes()); + expect(U32.deserialize(d).value).toBe(4294967295); + }); + + test("U64 round-trip", () => { + const val = new U64(18446744073709551615n); + const d = new Deserializer(val.bcsToBytes()); + expect(U64.deserialize(d).value).toBe(18446744073709551615n); + }); + + test("U64 accepts number input", () => { + const val = new U64(100); + expect(val.value).toBe(100n); + }); + + test("U128 round-trip", () => { + const val = new U128(340282366920938463463374607431768211455n); + const d = new Deserializer(val.bcsToBytes()); + expect(U128.deserialize(d).value).toBe(340282366920938463463374607431768211455n); + }); + + test("U256 round-trip", () => { + const val = new U256(1n); + const d = new Deserializer(val.bcsToBytes()); + expect(U256.deserialize(d).value).toBe(1n); + }); + + test("U8 throws on out of range", () => { + expect(() => new U8(256)).toThrow(); + expect(() => new U8(-1)).toThrow(); + }); + + test("U64 throws on out of range", () => { + expect(() => new U64(-1)).toThrow(); + }); +}); + +describe("Signed integers", () => { + test("I8 round-trip", () => { + const val = new I8(-128); + const d = new Deserializer(val.bcsToBytes()); + expect(I8.deserialize(d).value).toBe(-128); + }); + + test("I16 round-trip", () => { + const val = new I16(-32768); + const d = new Deserializer(val.bcsToBytes()); + expect(I16.deserialize(d).value).toBe(-32768); + }); + + test("I32 round-trip", () => { + const val = new I32(-2147483648); + const d = new Deserializer(val.bcsToBytes()); + expect(I32.deserialize(d).value).toBe(-2147483648); + }); + + test("I64 round-trip", () => { + const val = new I64(-9223372036854775808n); + const d = new Deserializer(val.bcsToBytes()); + expect(I64.deserialize(d).value).toBe(-9223372036854775808n); + }); + + test("I128 round-trip", () => { + const val = new I128(-1n); + const d = new Deserializer(val.bcsToBytes()); + expect(I128.deserialize(d).value).toBe(-1n); + }); + + test("I256 round-trip", () => { + const val = new I256(-1n); + const d = new Deserializer(val.bcsToBytes()); + expect(I256.deserialize(d).value).toBe(-1n); + }); + + test("I8 throws on out of range", () => { + expect(() => new I8(128)).toThrow(); + expect(() => new I8(-129)).toThrow(); + }); +}); + +describe("serializeForEntryFunction", () => { + test("U8 serializes as bytes with length prefix", () => { + const s = new Serializer(); + new U8(42).serializeForEntryFunction(s); + // Length prefix (1) + value (42) + expect(s.toUint8Array()).toEqual(new Uint8Array([1, 42])); + }); + + test("Bool serializes as bytes with length prefix", () => { + const s = new Serializer(); + new Bool(true).serializeForEntryFunction(s); + expect(s.toUint8Array()).toEqual(new Uint8Array([1, 1])); + }); +}); + +describe("serializeForScriptFunction", () => { + test("U8 serializes with variant tag", () => { + const s = new Serializer(); + new U8(42).serializeForScriptFunction(s); + // Variant tag (0 = U8) + value (42) + expect(s.toUint8Array()).toEqual(new Uint8Array([0, 42])); + }); + + test("Bool serializes with variant tag", () => { + const s = new Serializer(); + new Bool(true).serializeForScriptFunction(s); + // Variant tag (5 = Bool) + value (1) + expect(s.toUint8Array()).toEqual(new Uint8Array([5, 1])); + }); +}); + +// ── Move Structs ── + +describe("MoveString", () => { + test("round-trip", () => { + const str = new MoveString("hello world"); + const d = new Deserializer(str.bcsToBytes()); + expect(MoveString.deserialize(d).value).toBe("hello world"); + }); + + test("bcsToHex returns hex representation", () => { + const str = new MoveString("hi"); + expect(str.bcsToHex().toString()).toMatch(/^0x/); + }); +}); + +describe("MoveVector", () => { + test("U8 factory from number array", () => { + const vec = MoveVector.U8([1, 2, 3]); + expect(vec.values.length).toBe(3); + expect(vec.values[0].value).toBe(1); + }); + + test("U8 factory from Uint8Array", () => { + const vec = MoveVector.U8(new Uint8Array([4, 5, 6])); + expect(vec.values.length).toBe(3); + }); + + test("U8 factory from hex string", () => { + const vec = MoveVector.U8("0x0102"); + expect(vec.values.length).toBe(2); + expect(vec.values[0].value).toBe(1); + expect(vec.values[1].value).toBe(2); + }); + + test("U8 factory from empty array", () => { + const vec = MoveVector.U8([]); + expect(vec.values.length).toBe(0); + }); + + test("Bool factory", () => { + const vec = MoveVector.Bool([true, false, true]); + expect(vec.values.length).toBe(3); + expect(vec.values[1].value).toBe(false); + }); + + test("U64 factory", () => { + const vec = MoveVector.U64([1n, 2n, 3n]); + expect(vec.values.length).toBe(3); + }); + + test("I8 factory", () => { + const vec = MoveVector.I8([-1, 0, 127]); + expect(vec.values[0].value).toBe(-1); + }); + + test("MoveString factory", () => { + const vec = MoveVector.MoveString(["hello", "world"]); + expect(vec.values.length).toBe(2); + }); + + test("serializes and deserializes", () => { + const vec = MoveVector.U8([10, 20, 30]); + const bytes = vec.bcsToBytes(); + const d = new Deserializer(bytes); + const result = MoveVector.deserialize(d, U8); + expect(result.values.length).toBe(3); + expect(result.values[0].value).toBe(10); + expect(result.values[2].value).toBe(30); + }); + + test("serializeForEntryFunction wraps with length prefix", () => { + const s = new Serializer(); + const vec = MoveVector.U8([1, 2]); + vec.serializeForEntryFunction(s); + // Length of BCS bytes + the BCS vector data + const result = s.toUint8Array(); + expect(result[0]).toBe(3); // BCS vector [2, 1, 2] is 3 bytes + }); +}); + +describe("MoveOption", () => { + test("U8 with value", () => { + const opt = MoveOption.U8(42); + expect(opt.isSome()).toBe(true); + expect(opt.unwrap().value).toBe(42); + expect(opt.value?.value).toBe(42); + }); + + test("U8 without value", () => { + const opt = MoveOption.U8(undefined); + expect(opt.isSome()).toBe(false); + expect(opt.value).toBeUndefined(); + }); + + test("U8 with null", () => { + const opt = MoveOption.U8(null); + expect(opt.isSome()).toBe(false); + }); + + test("unwrap throws when empty", () => { + const opt = MoveOption.U8(undefined); + expect(() => opt.unwrap()).toThrow("Called unwrap on a MoveOption with no value"); + }); + + test("Bool factory", () => { + const opt = MoveOption.Bool(true); + expect(opt.isSome()).toBe(true); + expect(opt.unwrap().value).toBe(true); + }); + + test("MoveString factory", () => { + const opt = MoveOption.MoveString("hello"); + expect(opt.isSome()).toBe(true); + expect(opt.unwrap().value).toBe("hello"); + }); + + test("serializes some value", () => { + const opt = MoveOption.U8(5); + const bytes = opt.bcsToBytes(); + // MoveOption serializes as MoveVector: [length=1, value] + expect(bytes).toEqual(new Uint8Array([1, 5])); + }); + + test("serializes none value", () => { + const opt = MoveOption.U8(undefined); + const bytes = opt.bcsToBytes(); + // MoveOption serializes as MoveVector: [length=0] + expect(bytes).toEqual(new Uint8Array([0])); + }); + + test("round-trip with deserialization", () => { + const opt = MoveOption.U64(12345n); + const bytes = opt.bcsToBytes(); + const d = new Deserializer(bytes); + const result = MoveOption.deserialize(d, U64); + expect(result.isSome()).toBe(true); + expect(result.unwrap().value).toBe(12345n); + }); + + test("signed integer factories", () => { + expect(MoveOption.I8(-1)?.isSome()).toBe(true); + expect(MoveOption.I16(-1)?.isSome()).toBe(true); + expect(MoveOption.I32(-1)?.isSome()).toBe(true); + expect(MoveOption.I64(-1n)?.isSome()).toBe(true); + expect(MoveOption.I128(-1n)?.isSome()).toBe(true); + expect(MoveOption.I256(-1n)?.isSome()).toBe(true); + }); +}); + +describe("Serialized", () => { + test("serializes bytes with length prefix", () => { + const s = new Serialized(new Uint8Array([1, 2, 3])); + const bytes = s.bcsToBytes(); + expect(bytes).toEqual(new Uint8Array([3, 1, 2, 3])); + }); + + test("round-trip", () => { + const s = new Serialized(new Uint8Array([10, 20])); + const d = new Deserializer(s.bcsToBytes()); + const result = Serialized.deserialize(d); + expect(result.value).toEqual(new Uint8Array([10, 20])); + }); + + test("accepts hex string", () => { + const s = new Serialized("0x0102"); + expect(s.value).toEqual(new Uint8Array([1, 2])); + }); +}); + +describe("FixedBytes", () => { + test("serializes without length prefix", () => { + const fb = new FixedBytes(new Uint8Array([1, 2, 3])); + expect(fb.bcsToBytes()).toEqual(new Uint8Array([1, 2, 3])); + }); + + test("accepts hex string", () => { + const fb = new FixedBytes("0x0102"); + expect(fb.value).toEqual(new Uint8Array([1, 2])); + }); + + test("round-trip", () => { + const fb = new FixedBytes(new Uint8Array([10, 20, 30])); + const d = new Deserializer(fb.bcsToBytes()); + const result = FixedBytes.deserialize(d, 3); + expect(result.value).toEqual(new Uint8Array([10, 20, 30])); + }); +}); + +describe("EntryFunctionBytes", () => { + test("deserialize and re-serialize", () => { + // Simulate creating EntryFunctionBytes via deserialization + const d = new Deserializer(new Uint8Array([0xab, 0xcd])); + const efb = EntryFunctionBytes.deserialize(d, 2); + expect(efb.value.value).toEqual(new Uint8Array([0xab, 0xcd])); + }); + + test("serializeForEntryFunction adds length prefix", () => { + const d = new Deserializer(new Uint8Array([0x01, 0x02])); + const efb = EntryFunctionBytes.deserialize(d, 2); + const s = new Serializer(); + efb.serializeForEntryFunction(s); + // Length prefix (2) + the 2 bytes + expect(s.toUint8Array()).toEqual(new Uint8Array([2, 0x01, 0x02])); + }); +}); diff --git a/v10/tests/unit/bcs/serializer.test.ts b/v10/tests/unit/bcs/serializer.test.ts new file mode 100644 index 000000000..d1e1c8d67 --- /dev/null +++ b/v10/tests/unit/bcs/serializer.test.ts @@ -0,0 +1,248 @@ +import { beforeEach, describe, expect, test } from "vitest"; +import { + ensureBoolean, + outOfRangeErrorMessage, + Serializable, + Serializer, + validateNumberInRange, +} from "../../../src/bcs/serializer.js"; + +describe("Serializer", () => { + let serializer: Serializer; + + beforeEach(() => { + serializer = new Serializer(); + }); + + test("throws on zero-length buffer", () => { + expect(() => new Serializer(0)).toThrow("Length needs to be greater than 0"); + }); + + // ── Boolean ── + + test("serializes true", () => { + serializer.serializeBool(true); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([0x01])); + }); + + test("serializes false", () => { + serializer.serializeBool(false); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([0x00])); + }); + + test("throws on non-boolean", () => { + // @ts-expect-error testing invalid input + expect(() => serializer.serializeBool(123)).toThrow("is not a boolean value"); + }); + + // ── Unsigned integers ── + + test("serializes U8", () => { + serializer.serializeU8(255); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([0xff])); + }); + + test("throws on U8 out of range", () => { + expect(() => serializer.serializeU8(256)).toThrow(); + expect(() => serializer.serializeU8(-1)).toThrow(); + }); + + test("serializes U16 little-endian", () => { + serializer.serializeU16(0x1234); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([0x34, 0x12])); + }); + + test("serializes U32 little-endian", () => { + serializer.serializeU32(0x12345678); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([0x78, 0x56, 0x34, 0x12])); + }); + + test("serializes U64", () => { + serializer.serializeU64(BigInt("1311768467750121216")); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([0x00, 0xef, 0xcd, 0xab, 0x78, 0x56, 0x34, 0x12])); + }); + + test("serializes max U64", () => { + serializer.serializeU64(18446744073709551615n); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])); + }); + + test("serializes U128", () => { + serializer.serializeU128(1n); + const expected = new Uint8Array(16); + expected[0] = 1; + expect(serializer.toUint8Array()).toEqual(expected); + }); + + test("serializes U256", () => { + serializer.serializeU256(1n); + const expected = new Uint8Array(32); + expected[0] = 1; + expect(serializer.toUint8Array()).toEqual(expected); + }); + + // ── Signed integers ── + + test("serializes I8", () => { + serializer.serializeI8(-1); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([0xff])); + }); + + test("serializes I16", () => { + serializer.serializeI16(-1); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([0xff, 0xff])); + }); + + test("serializes I32", () => { + serializer.serializeI32(-1); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([0xff, 0xff, 0xff, 0xff])); + }); + + test("serializes I64 positive", () => { + serializer.serializeI64(1n); + const expected = new Uint8Array(8); + expected[0] = 1; + expect(serializer.toUint8Array()).toEqual(expected); + }); + + test("serializes I64 negative", () => { + serializer.serializeI64(-1n); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])); + }); + + test("serializes I128 negative", () => { + serializer.serializeI128(-1n); + expect(serializer.toUint8Array()).toEqual(new Uint8Array(16).fill(0xff)); + }); + + test("serializes I256 negative", () => { + serializer.serializeI256(-1n); + expect(serializer.toUint8Array()).toEqual(new Uint8Array(32).fill(0xff)); + }); + + test("throws on I8 out of range", () => { + expect(() => serializer.serializeI8(128)).toThrow(); + expect(() => serializer.serializeI8(-129)).toThrow(); + }); + + // ── String / Bytes ── + + test("serializes string with length prefix", () => { + serializer.serializeStr("1234abcd"); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([8, 49, 50, 51, 52, 97, 98, 99, 100])); + }); + + test("serializes empty string", () => { + serializer.serializeStr(""); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([0])); + }); + + test("serializes bytes with length prefix", () => { + serializer.serializeBytes(new Uint8Array([1, 2, 3])); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([3, 1, 2, 3])); + }); + + test("serializes fixed bytes without length prefix", () => { + serializer.serializeFixedBytes(new Uint8Array([1, 2, 3])); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([1, 2, 3])); + }); + + // ── ULEB128 ── + + test("serializes ULEB128", () => { + serializer.serializeU32AsUleb128(0); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([0])); + }); + + test("serializes ULEB128 multi-byte", () => { + serializer.serializeU32AsUleb128(128); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([0x80, 0x01])); + }); + + // ── Composable ── + + test("serializes vector", () => { + class SimpleU8 extends Serializable { + constructor(public val: number) { + super(); + } + serialize(s: Serializer): void { + s.serializeU8(this.val); + } + } + serializer.serializeVector([new SimpleU8(1), new SimpleU8(2), new SimpleU8(3)]); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([3, 1, 2, 3])); + }); + + test("serializeAsBytes wraps with length prefix", () => { + class SimpleU16 extends Serializable { + constructor(public val: number) { + super(); + } + serialize(s: Serializer): void { + s.serializeU16(this.val); + } + } + serializer.serializeAsBytes(new SimpleU16(0x0102)); + // U16 serializes as 2 bytes, so length prefix is 2, then the LE bytes + expect(serializer.toUint8Array()).toEqual(new Uint8Array([2, 0x02, 0x01])); + }); + + test("serializeOption with value", () => { + serializer.serializeOption("hello"); + const expected = new Uint8Array([1, 5, 104, 101, 108, 108, 111]); + expect(serializer.toUint8Array()).toEqual(expected); + }); + + test("serializeOption without value", () => { + serializer.serializeOption(undefined); + expect(serializer.toUint8Array()).toEqual(new Uint8Array([0])); + }); + + // ── Buffer management ── + + test("grows buffer when needed", () => { + const small = new Serializer(4); + small.serializeFixedBytes(new Uint8Array(100)); + expect(small.toUint8Array().length).toBe(100); + }); + + test("reset clears buffer", () => { + serializer.serializeU8(42); + serializer.reset(); + expect(serializer.getOffset()).toBe(0); + }); + + test("toUint8ArrayView returns a view", () => { + serializer.serializeU8(1); + const view = serializer.toUint8ArrayView(); + expect(view.length).toBe(1); + expect(view[0]).toBe(1); + }); +}); + +describe("Validation helpers", () => { + test("ensureBoolean passes for boolean", () => { + expect(() => ensureBoolean(true)).not.toThrow(); + expect(() => ensureBoolean(false)).not.toThrow(); + }); + + test("ensureBoolean throws for non-boolean", () => { + expect(() => ensureBoolean(1)).toThrow(); + expect(() => ensureBoolean("true")).toThrow(); + }); + + test("validateNumberInRange passes for valid range", () => { + expect(() => validateNumberInRange(5, 0, 10)).not.toThrow(); + expect(() => validateNumberInRange(0n, 0n, 100n)).not.toThrow(); + }); + + test("validateNumberInRange throws for out of range", () => { + expect(() => validateNumberInRange(11, 0, 10)).toThrow(); + expect(() => validateNumberInRange(-1, 0, 10)).toThrow(); + }); + + test("outOfRangeErrorMessage formats correctly", () => { + expect(outOfRangeErrorMessage(5, 0, 3)).toBe("5 is out of range: [0, 3]"); + }); +}); diff --git a/v10/tests/unit/client/client.test.ts b/v10/tests/unit/client/client.test.ts new file mode 100644 index 000000000..09cb79cc2 --- /dev/null +++ b/v10/tests/unit/client/client.test.ts @@ -0,0 +1,368 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { aptosRequest } from "../../../src/client/aptos-request.js"; +import { get, paginateWithCursor } from "../../../src/client/get.js"; +import { post } from "../../../src/client/post.js"; +import type { Client } from "../../../src/client/types.js"; +import { AptosApiType, MimeType } from "../../../src/client/types.js"; +import { AptosApiError } from "../../../src/core/errors.js"; + +// Mock @aptos-labs/aptos-client — aptosRequest delegates to this +vi.mock("@aptos-labs/aptos-client", () => ({ + jsonRequest: vi.fn(), + bcsRequest: vi.fn(), +})); + +import { bcsRequest, jsonRequest } from "@aptos-labs/aptos-client"; + +const mockClient = vi.mocked(jsonRequest); +const mockBcsClient = vi.mocked(bcsRequest); + +/** Build a mock AptosClientResponse for JSON calls. */ +function mockJsonResponse(data: unknown, status = 200, headers: Record = {}) { + return { status, statusText: status === 200 ? "OK" : "Error", data, headers }; +} + +// ── aptosRequest ── + +describe("aptosRequest", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("makes a GET request and parses JSON response", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse({ chain_id: 4 })); + + const result = await aptosRequest<{ chain_id: number }>( + { url: "http://localhost:8080/v1", method: "GET", path: "" }, + AptosApiType.FULLNODE, + ); + + expect(result.status).toBe(200); + expect(result.data.chain_id).toBe(4); + }); + + it("includes SDK headers", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse({})); + + await aptosRequest({ url: "http://localhost:8080/v1", method: "GET", path: "" }, AptosApiType.FULLNODE); + + const requestArg = mockClient.mock.calls[0][0]; + expect(requestArg.headers["x-aptos-client"]).toMatch(/^aptos-typescript-sdk\//); + expect(requestArg.headers["content-type"]).toBe(MimeType.JSON); + expect(requestArg.headers.accept).toBe(MimeType.JSON); + }); + + it("includes origin method header when provided", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse({})); + + await aptosRequest( + { url: "http://localhost:8080/v1", method: "GET", path: "", originMethod: "getBalance" }, + AptosApiType.FULLNODE, + ); + + const requestArg = mockClient.mock.calls[0][0]; + expect(requestArg.headers["x-aptos-typescript-sdk-origin-method"]).toBe("getBalance"); + }); + + it("passes params to aptos-client for query string handling", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse({})); + + await aptosRequest( + { url: "http://localhost:8080/v1", method: "GET", path: "accounts", params: { limit: "25", start: "0" } }, + AptosApiType.FULLNODE, + ); + + const requestArg = mockClient.mock.calls[0][0]; + expect(requestArg.url).toBe("http://localhost:8080/v1/accounts"); + expect(requestArg.params).toEqual({ limit: "25", start: "0" }); + }); + + it("filters out undefined param values", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse({})); + + await aptosRequest( + { + url: "http://localhost:8080/v1", + method: "GET", + path: "accounts", + params: { limit: "25", start: undefined }, + }, + AptosApiType.FULLNODE, + ); + + const requestArg = mockClient.mock.calls[0][0]; + expect(requestArg.params).toEqual({ limit: "25" }); + }); + + it("sends POST body to aptos-client", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse({})); + + await aptosRequest( + { url: "http://localhost:8080/v1", method: "POST", path: "transactions", body: { sender: "0x1" } }, + AptosApiType.FULLNODE, + ); + + const requestArg = mockClient.mock.calls[0][0]; + expect(requestArg.method).toBe("POST"); + expect(requestArg.body).toEqual({ sender: "0x1" }); + }); + + it("throws AptosApiError on 401", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse({ message: "Unauthorized" }, 401)); + + await expect( + aptosRequest({ url: "http://localhost:8080/v1", method: "GET", path: "" }, AptosApiType.FULLNODE), + ).rejects.toThrow(AptosApiError); + }); + + it("throws AptosApiError on non-2xx status", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse({ message: "Not found" }, 404)); + + await expect( + aptosRequest({ url: "http://localhost:8080/v1", method: "GET", path: "" }, AptosApiType.FULLNODE), + ).rejects.toThrow(AptosApiError); + }); + + it("throws on indexer errors", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse({ errors: [{ message: "bad query" }] })); + + await expect( + aptosRequest({ url: "http://localhost:8090/v1/graphql", method: "POST", path: "" }, AptosApiType.INDEXER), + ).rejects.toThrow(AptosApiError); + }); + + it("unwraps indexer data wrapper", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse({ data: { account: { balance: 100 } } })); + + const result = await aptosRequest<{ account: { balance: number } }>( + { url: "http://localhost:8090/v1/graphql", method: "POST", path: "" }, + AptosApiType.INDEXER, + ); + + expect(result.data).toEqual({ account: { balance: 100 } }); + }); + + it("handles BCS response via bcsRequest", async () => { + const bcsData = new Uint8Array([1, 2, 3, 4]); + mockBcsClient.mockResolvedValueOnce({ + status: 200, + statusText: "OK", + data: Buffer.from(bcsData), + headers: {}, + }); + + const result = await aptosRequest( + { url: "http://localhost:8080/v1", method: "GET", path: "", acceptType: MimeType.BCS }, + AptosApiType.FULLNODE, + ); + + expect(result.data).toBeInstanceOf(Uint8Array); + expect(result.data).toEqual(bcsData); + expect(mockBcsClient).toHaveBeenCalledTimes(1); + expect(mockClient).not.toHaveBeenCalled(); + }); + + it("merges custom headers from overrides", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse({})); + + await aptosRequest( + { + url: "http://localhost:8080/v1", + method: "GET", + path: "", + overrides: { HEADERS: { "x-custom": "value" }, API_KEY: "test-key" }, + }, + AptosApiType.FULLNODE, + ); + + const requestArg = mockClient.mock.calls[0][0]; + expect(requestArg.headers["x-custom"]).toBe("value"); + expect(requestArg.headers.Authorization).toBe("Bearer test-key"); + }); + + it("prefers AUTH_TOKEN over API_KEY", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse({})); + + await aptosRequest( + { + url: "http://localhost:8080/v1", + method: "GET", + path: "", + overrides: { AUTH_TOKEN: "my-token", API_KEY: "my-key" }, + }, + AptosApiType.FULLNODE, + ); + + const requestArg = mockClient.mock.calls[0][0]; + expect(requestArg.headers.Authorization).toBe("Bearer my-token"); + }); +}); + +// ── get / post helpers ── + +describe("get helper", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("delegates to aptosRequest with GET method", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse({ ledger_version: "1" })); + + const result = await get<{ ledger_version: string }>({ + url: "http://localhost:8080/v1", + apiType: AptosApiType.FULLNODE, + path: "", + originMethod: "getLedgerInfo", + }); + + expect(result.data.ledger_version).toBe("1"); + const requestArg = mockClient.mock.calls[0][0]; + expect(requestArg.method).toBe("GET"); + }); +}); + +describe("post helper", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("delegates to aptosRequest with POST method", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse({ hash: "0xabc" })); + + const result = await post<{ hash: string }>({ + url: "http://localhost:8080/v1", + apiType: AptosApiType.FULLNODE, + path: "transactions", + originMethod: "submitTransaction", + body: { sender: "0x1" }, + }); + + expect(result.data.hash).toBe("0xabc"); + const requestArg = mockClient.mock.calls[0][0]; + expect(requestArg.method).toBe("POST"); + }); +}); + +// ── paginateWithCursor ── + +describe("paginateWithCursor", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("fetches all pages until no cursor", async () => { + // Page 1: has cursor + mockClient.mockResolvedValueOnce({ + status: 200, + statusText: "OK", + data: [{ id: 1 }, { id: 2 }], + headers: { "x-aptos-cursor": "cursor_abc" }, + }); + // Page 2: no cursor + mockClient.mockResolvedValueOnce(mockJsonResponse([{ id: 3 }])); + + const results = await paginateWithCursor>({ + url: "http://localhost:8080/v1", + apiType: AptosApiType.FULLNODE, + path: "accounts", + originMethod: "getAccounts", + }); + + expect(results).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]); + expect(mockClient).toHaveBeenCalledTimes(2); + }); + + it("returns single page when no cursor", async () => { + mockClient.mockResolvedValueOnce(mockJsonResponse([{ id: 1 }])); + + const results = await paginateWithCursor>({ + url: "http://localhost:8080/v1", + apiType: AptosApiType.FULLNODE, + path: "accounts", + originMethod: "getAccounts", + }); + + expect(results).toEqual([{ id: 1 }]); + expect(mockClient).toHaveBeenCalledTimes(1); + }); +}); + +// ── Custom client ── + +describe("custom Client", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("uses custom client.sendRequest instead of jsonRequest/bcsRequest", async () => { + const customClient: Client = { + sendRequest: vi.fn().mockResolvedValueOnce({ + status: 200, + statusText: "OK", + data: { custom: true }, + headers: {}, + }), + }; + + const result = await aptosRequest<{ custom: boolean }>( + { url: "http://localhost:8080/v1", method: "GET", path: "" }, + AptosApiType.FULLNODE, + customClient, + ); + + expect(result.data).toEqual({ custom: true }); + expect(customClient.sendRequest).toHaveBeenCalledTimes(1); + expect(mockClient).not.toHaveBeenCalled(); + expect(mockBcsClient).not.toHaveBeenCalled(); + }); + + it("forwards custom client through get helper", async () => { + const customClient: Client = { + sendRequest: vi.fn().mockResolvedValueOnce({ + status: 200, + statusText: "OK", + data: { via: "get" }, + headers: {}, + }), + }; + + const result = await get<{ via: string }>({ + url: "http://localhost:8080/v1", + apiType: AptosApiType.FULLNODE, + path: "", + originMethod: "test", + client: customClient, + }); + + expect(result.data).toEqual({ via: "get" }); + expect(customClient.sendRequest).toHaveBeenCalledTimes(1); + expect(mockClient).not.toHaveBeenCalled(); + }); + + it("forwards custom client through post helper", async () => { + const customClient: Client = { + sendRequest: vi.fn().mockResolvedValueOnce({ + status: 200, + statusText: "OK", + data: { via: "post" }, + headers: {}, + }), + }; + + const result = await post<{ via: string }>({ + url: "http://localhost:8080/v1", + apiType: AptosApiType.FULLNODE, + path: "transactions", + originMethod: "test", + body: { sender: "0x1" }, + client: customClient, + }); + + expect(result.data).toEqual({ via: "post" }); + expect(customClient.sendRequest).toHaveBeenCalledTimes(1); + expect(mockClient).not.toHaveBeenCalled(); + }); +}); diff --git a/v10/tests/unit/client/config.test.ts b/v10/tests/unit/client/config.test.ts new file mode 100644 index 000000000..c2a4b1a8c --- /dev/null +++ b/v10/tests/unit/client/config.test.ts @@ -0,0 +1,123 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; +import { AptosConfig, createConfig } from "../../../src/api/config.js"; +import { AptosApiType } from "../../../src/core/constants.js"; +import { Network } from "../../../src/core/network.js"; + +describe("AptosConfig", () => { + it("defaults to devnet", () => { + const config = new AptosConfig(); + expect(config.network).toBe(Network.DEVNET); + }); + + it("accepts custom network", () => { + const config = new AptosConfig({ network: Network.TESTNET }); + expect(config.network).toBe(Network.TESTNET); + }); + + it("throws when custom endpoints provided without network", () => { + expect(() => new AptosConfig({ fullnode: "http://localhost:8080/v1" })).toThrow( + "Custom endpoints require a network", + ); + }); + + it("accepts custom endpoints with a network", () => { + const config = new AptosConfig({ + network: Network.CUSTOM, + fullnode: "http://localhost:8080/v1", + }); + expect(config.getRequestUrl(AptosApiType.FULLNODE)).toBe("http://localhost:8080/v1"); + }); + + it("resolves fullnode URL from network", () => { + const config = new AptosConfig({ network: Network.TESTNET }); + const url = config.getRequestUrl(AptosApiType.FULLNODE); + expect(url).toContain("testnet"); + }); + + it("resolves indexer URL from network", () => { + const config = new AptosConfig({ network: Network.TESTNET }); + const url = config.getRequestUrl(AptosApiType.INDEXER); + expect(url).toContain("testnet"); + expect(url).toContain("graphql"); + }); + + it("resolves faucet URL for devnet", () => { + const config = new AptosConfig({ network: Network.DEVNET }); + const url = config.getRequestUrl(AptosApiType.FAUCET); + expect(url).toContain("faucet"); + }); + + it("throws when requesting testnet faucet", () => { + const config = new AptosConfig({ network: Network.TESTNET }); + expect(() => config.getRequestUrl(AptosApiType.FAUCET)).toThrow("no way to programmatically mint testnet APT"); + }); + + it("throws when requesting mainnet faucet", () => { + const config = new AptosConfig({ network: Network.MAINNET }); + expect(() => config.getRequestUrl(AptosApiType.FAUCET)).toThrow("no mainnet faucet"); + }); + + it("throws when custom network without endpoint", () => { + const config = new AptosConfig({ network: Network.CUSTOM, fullnode: "http://custom.com" }); + expect(() => config.getRequestUrl(AptosApiType.INDEXER)).toThrow("custom indexer url"); + }); + + it("resolves pepper and prover URLs", () => { + const config = new AptosConfig({ network: Network.TESTNET }); + expect(config.getRequestUrl(AptosApiType.PEPPER)).toContain("pepper"); + expect(config.getRequestUrl(AptosApiType.PROVER)).toContain("prover"); + }); + + it("uses default gas and expiry", () => { + const config = new AptosConfig(); + expect(config.defaultMaxGasAmount).toBe(200000); + expect(config.defaultTxnExpSecFromNow).toBe(20); + }); + + it("allows custom gas and expiry", () => { + const config = new AptosConfig({ defaultMaxGasAmount: 500000, defaultTxnExpSecFromNow: 60 }); + expect(config.defaultMaxGasAmount).toBe(500000); + expect(config.defaultTxnExpSecFromNow).toBe(60); + }); + + it("merges fullnode config with headers", () => { + const config = new AptosConfig({ + network: Network.TESTNET, + clientConfig: { HEADERS: { "x-global": "1" }, API_KEY: "key" }, + fullnodeConfig: { HEADERS: { "x-fullnode": "2" } }, + }); + const merged = config.getMergedFullnodeConfig({ HEADERS: { "x-override": "3" } }); + expect(merged.HEADERS).toEqual({ + "x-global": "1", + "x-fullnode": "2", + "x-override": "3", + }); + expect(merged.API_KEY).toBe("key"); + }); + + it("strips API_KEY from faucet config", () => { + const config = new AptosConfig({ + network: Network.DEVNET, + clientConfig: { API_KEY: "should-be-removed", HEADERS: { "x-global": "1" } }, + }); + const merged = config.getMergedFaucetConfig(); + expect(merged.API_KEY).toBeUndefined(); + expect(merged.HEADERS).toEqual({ "x-global": "1" }); + }); +}); + +describe("createConfig", () => { + it("creates AptosConfig instance", () => { + const config = createConfig({ network: Network.TESTNET }); + expect(config).toBeInstanceOf(AptosConfig); + expect(config.network).toBe(Network.TESTNET); + }); + + it("defaults when no args", () => { + const config = createConfig(); + expect(config.network).toBe(Network.DEVNET); + }); +}); diff --git a/v10/tests/unit/compat/compat.test.ts b/v10/tests/unit/compat/compat.test.ts new file mode 100644 index 000000000..25c5048b6 --- /dev/null +++ b/v10/tests/unit/compat/compat.test.ts @@ -0,0 +1,125 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; +import { AptosConfig } from "../../../src/api/config.js"; +import { Aptos } from "../../../src/compat/aptos.js"; +import { Network } from "../../../src/core/network.js"; + +describe("Compat Aptos class", () => { + it("accepts AptosConfig in constructor (v6 style)", () => { + const config = new AptosConfig({ network: Network.TESTNET }); + const aptos = new Aptos(config); + expect(aptos.config).toBe(config); + expect(aptos.config.network).toBe(Network.TESTNET); + }); + + it("accepts AptosSettings in constructor (v10 style)", () => { + const aptos = new Aptos({ network: Network.TESTNET }); + expect(aptos.config.network).toBe(Network.TESTNET); + }); + + it("accepts no args (defaults to DEVNET)", () => { + const aptos = new Aptos(); + expect(aptos.config.network).toBe(Network.DEVNET); + }); + + it("has v6-style flat methods", () => { + const aptos = new Aptos({ network: Network.TESTNET }); + + // General + expect(typeof aptos.getLedgerInfo).toBe("function"); + expect(typeof aptos.getChainId).toBe("function"); + expect(typeof aptos.getGasPriceEstimation).toBe("function"); + expect(typeof aptos.getBlockByHeight).toBe("function"); + expect(typeof aptos.getBlockByVersion).toBe("function"); + expect(typeof aptos.view).toBe("function"); + + // Account + expect(typeof aptos.getAccountInfo).toBe("function"); + expect(typeof aptos.getAccountModules).toBe("function"); + expect(typeof aptos.getAccountModule).toBe("function"); + expect(typeof aptos.getAccountResource).toBe("function"); + expect(typeof aptos.getAccountResources).toBe("function"); + expect(typeof aptos.getAccountTransactions).toBe("function"); + + // Transaction + expect(typeof aptos.signTransaction).toBe("function"); + expect(typeof aptos.signAndSubmitTransaction).toBe("function"); + expect(typeof aptos.waitForTransaction).toBe("function"); + expect(typeof aptos.getTransactions).toBe("function"); + expect(typeof aptos.getTransactionByHash).toBe("function"); + expect(typeof aptos.getTransactionByVersion).toBe("function"); + expect(typeof aptos.getSigningMessage).toBe("function"); + + // Faucet + expect(typeof aptos.fundAccount).toBe("function"); + + // Coin + expect(typeof aptos.transferCoinTransaction).toBe("function"); + + // Table + expect(typeof aptos.getTableItem).toBe("function"); + }); + + it("has v6-style transaction.build.simple()", () => { + const aptos = new Aptos({ network: Network.TESTNET }); + expect(typeof aptos.transaction.build).toBe("object"); + expect(typeof aptos.transaction.build.simple).toBe("function"); + }); + + it("still has v10 namespaced access", () => { + const aptos = new Aptos({ network: Network.TESTNET }); + expect(typeof aptos.general.getLedgerInfo).toBe("function"); + expect(typeof aptos.account.getInfo).toBe("function"); + expect(typeof aptos.transaction.buildSimple).toBe("function"); + expect(typeof aptos.faucet.fund).toBe("function"); + expect(typeof aptos.coin.transferTransaction).toBe("function"); + expect(typeof aptos.table.getItem).toBe("function"); + }); +}); + +describe("Compat barrel exports", () => { + it("re-exports Aptos class from compat/index", async () => { + const { Aptos: CompatAptos } = await import("../../../src/compat/index.js"); + expect(CompatAptos).toBeDefined(); + const aptos = new CompatAptos({ network: Network.TESTNET }); + expect(aptos.config.network).toBe(Network.TESTNET); + }); + + it("re-exports AptosConfig from compat/index", async () => { + const { AptosConfig: CompatConfig } = await import("../../../src/compat/index.js"); + expect(CompatConfig).toBeDefined(); + const config = new CompatConfig({ network: Network.LOCAL }); + expect(config.network).toBe(Network.LOCAL); + }); + + it("re-exports Network from compat/index", async () => { + const { Network: CompatNetwork } = await import("../../../src/compat/index.js"); + expect(CompatNetwork).toBeDefined(); + expect(CompatNetwork.TESTNET).toBe("testnet"); + }); + + it("re-exports core types from compat/index", async () => { + const { AccountAddress, Hex } = await import("../../../src/compat/index.js"); + expect(AccountAddress).toBeDefined(); + expect(Hex).toBeDefined(); + }); + + it("re-exports account factory from compat/index", async () => { + const { generateAccount, accountFromPrivateKey } = await import("../../../src/compat/index.js"); + expect(generateAccount).toBeDefined(); + expect(accountFromPrivateKey).toBeDefined(); + }); + + it("re-exports standalone API functions from compat/index", async () => { + const { getLedgerInfo, getChainId, getAccountInfo, buildSimpleTransaction, fundAccount, getTableItem } = + await import("../../../src/compat/index.js"); + expect(typeof getLedgerInfo).toBe("function"); + expect(typeof getChainId).toBe("function"); + expect(typeof getAccountInfo).toBe("function"); + expect(typeof buildSimpleTransaction).toBe("function"); + expect(typeof fundAccount).toBe("function"); + expect(typeof getTableItem).toBe("function"); + }); +}); diff --git a/v10/tests/unit/core/account-address.test.ts b/v10/tests/unit/core/account-address.test.ts new file mode 100644 index 000000000..303ad66e3 --- /dev/null +++ b/v10/tests/unit/core/account-address.test.ts @@ -0,0 +1,266 @@ +import { describe, expect, it } from "vitest"; +import { Deserializer } from "../../../src/bcs/deserializer.js"; +import { Serializer } from "../../../src/bcs/serializer.js"; +import { AccountAddress, AddressInvalidReason } from "../../../src/core/account-address.js"; + +type Addresses = { + shortWith0x: string; + shortWithout0x: string; + longWith0x: string; + longWithout0x: string; + bytes: Uint8Array; +}; + +const ADDRESS_ZERO: Addresses = { + shortWith0x: "0x0", + shortWithout0x: "0", + longWith0x: "0x0000000000000000000000000000000000000000000000000000000000000000", + longWithout0x: "0000000000000000000000000000000000000000000000000000000000000000", + bytes: new Uint8Array(32), +}; + +const ADDRESS_ONE: Addresses = { + shortWith0x: "0x1", + shortWithout0x: "1", + longWith0x: "0x0000000000000000000000000000000000000000000000000000000000000001", + longWithout0x: "0000000000000000000000000000000000000000000000000000000000000001", + bytes: new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + ]), +}; + +const ADDRESS_F: Addresses = { + shortWith0x: "0xf", + shortWithout0x: "f", + longWith0x: "0x000000000000000000000000000000000000000000000000000000000000000f", + longWithout0x: "000000000000000000000000000000000000000000000000000000000000000f", + bytes: new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, + ]), +}; + +const ADDRESS_TEN: Addresses = { + shortWith0x: "0x10", + shortWithout0x: "10", + longWith0x: "0x0000000000000000000000000000000000000000000000000000000000000010", + longWithout0x: "0000000000000000000000000000000000000000000000000000000000000010", + bytes: new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, + ]), +}; + +const ADDRESS_OTHER: Addresses = { + shortWith0x: "0xca843279e3427144cead5e4d5999a3d0ca843279e3427144cead5e4d5999a3d0", + shortWithout0x: "ca843279e3427144cead5e4d5999a3d0ca843279e3427144cead5e4d5999a3d0", + longWith0x: "0xca843279e3427144cead5e4d5999a3d0ca843279e3427144cead5e4d5999a3d0", + longWithout0x: "ca843279e3427144cead5e4d5999a3d0ca843279e3427144cead5e4d5999a3d0", + bytes: new Uint8Array([ + 202, 132, 50, 121, 227, 66, 113, 68, 206, 173, 94, 77, 89, 153, 163, 208, 202, 132, 50, 121, 227, 66, 113, 68, 206, + 173, 94, 77, 89, 153, 163, 208, + ]), +}; + +describe("AccountAddress fromString", () => { + it("parses special address: 0x0", () => { + expect(AccountAddress.fromString(ADDRESS_ZERO.longWith0x).toString()).toBe(ADDRESS_ZERO.shortWith0x); + expect(AccountAddress.fromString(ADDRESS_ZERO.longWithout0x).toString()).toBe(ADDRESS_ZERO.shortWith0x); + expect(AccountAddress.fromString(ADDRESS_ZERO.shortWith0x).toString()).toBe(ADDRESS_ZERO.shortWith0x); + expect(AccountAddress.fromString(ADDRESS_ZERO.shortWithout0x).toString()).toBe(ADDRESS_ZERO.shortWith0x); + }); + + it("parses special address: 0x1", () => { + expect(AccountAddress.fromString(ADDRESS_ONE.longWith0x).toString()).toBe(ADDRESS_ONE.shortWith0x); + expect(AccountAddress.fromString(ADDRESS_ONE.shortWith0x).toString()).toBe(ADDRESS_ONE.shortWith0x); + }); + + it("parses special address: 0xf", () => { + expect(AccountAddress.fromString(ADDRESS_F.longWith0x).toString()).toBe(ADDRESS_F.shortWith0x); + expect(AccountAddress.fromString(ADDRESS_F.shortWith0x).toString()).toBe(ADDRESS_F.shortWith0x); + }); + + it("parses padded short form: 0x0f", () => { + expect(AccountAddress.fromString("0x0f").toString()).toBe(ADDRESS_F.shortWith0x); + }); + + it("parses non-special address: 0x10 (long form)", () => { + expect(AccountAddress.fromString(ADDRESS_TEN.longWith0x).toString()).toBe(ADDRESS_TEN.longWith0x); + expect(AccountAddress.fromString(ADDRESS_TEN.longWithout0x).toString()).toBe(ADDRESS_TEN.longWith0x); + }); + + it("throws for non-special address in short form", () => { + expect(() => AccountAddress.fromString(ADDRESS_TEN.shortWith0x)).toThrow(); + expect(() => AccountAddress.fromString(ADDRESS_TEN.shortWithout0x)).toThrow(); + }); + + it("parses non-special full-length address", () => { + expect(AccountAddress.fromString(ADDRESS_OTHER.longWith0x).toString()).toBe(ADDRESS_OTHER.longWith0x); + }); + + it("parses values with custom maxMissingChars", () => { + expect(AccountAddress.fromString("0x0123456789abcdef", { maxMissingChars: 63 })); + expect(() => AccountAddress.fromString("0x0123456789abcdef", { maxMissingChars: 0 })).toThrow(); + }); +}); + +describe("AccountAddress fromStringStrict", () => { + it("parses special address: 0x0", () => { + expect(AccountAddress.fromStringStrict(ADDRESS_ZERO.longWith0x).toString()).toBe(ADDRESS_ZERO.shortWith0x); + expect(() => AccountAddress.fromStringStrict(ADDRESS_ZERO.longWithout0x)).toThrow(); + expect(AccountAddress.fromStringStrict(ADDRESS_ZERO.shortWith0x).toString()).toBe(ADDRESS_ZERO.shortWith0x); + expect(() => AccountAddress.fromStringStrict(ADDRESS_ZERO.shortWithout0x)).toThrow(); + }); + + it("throws for padded short form: 0x0f", () => { + expect(() => AccountAddress.fromStringStrict("0x0f")).toThrow(); + }); + + it("parses non-special address: 0x10 (only long form with prefix)", () => { + expect(AccountAddress.fromStringStrict(ADDRESS_TEN.longWith0x).toString()).toBe(ADDRESS_TEN.longWith0x); + expect(() => AccountAddress.fromStringStrict(ADDRESS_TEN.longWithout0x)).toThrow(); + expect(() => AccountAddress.fromStringStrict(ADDRESS_TEN.shortWith0x)).toThrow(); + }); +}); + +describe("AccountAddress static constants", () => { + it("has correct values", () => { + expect(AccountAddress.ZERO.toString()).toBe("0x0"); + expect(AccountAddress.ONE.toString()).toBe("0x1"); + expect(AccountAddress.TWO.toString()).toBe("0x2"); + expect(AccountAddress.THREE.toString()).toBe("0x3"); + expect(AccountAddress.FOUR.toString()).toBe("0x4"); + }); +}); + +describe("AccountAddress from", () => { + it("parses from string", () => { + expect(AccountAddress.from(ADDRESS_ONE.longWith0x).toString()).toBe(ADDRESS_ONE.shortWith0x); + expect(AccountAddress.from(ADDRESS_ONE.shortWith0x).toString()).toBe(ADDRESS_ONE.shortWith0x); + }); + + it("parses from Uint8Array", () => { + expect(AccountAddress.from(ADDRESS_ONE.bytes).toString()).toBe(ADDRESS_ONE.shortWith0x); + expect(AccountAddress.from(ADDRESS_TEN.bytes).toString()).toBe(ADDRESS_TEN.longWith0x); + }); + + it("returns same instance for AccountAddress input", () => { + const addr = AccountAddress.from("0x1"); + expect(AccountAddress.from(addr)).toBe(addr); + }); +}); + +describe("AccountAddress fromStrict", () => { + it("parses special address: 0x1", () => { + expect(AccountAddress.fromStrict(ADDRESS_ONE.longWith0x).toString()).toBe(ADDRESS_ONE.shortWith0x); + expect(() => AccountAddress.fromStrict(ADDRESS_ONE.longWithout0x)).toThrow(); + expect(AccountAddress.fromStrict(ADDRESS_ONE.bytes).toString()).toBe(ADDRESS_ONE.shortWith0x); + }); + + it("throws for short non-special address", () => { + expect(() => AccountAddress.fromStrict(ADDRESS_TEN.shortWith0x)).toThrow(); + }); +}); + +describe("AccountAddress toString variants", () => { + it("toStringWithoutPrefix for special address", () => { + expect(AccountAddress.fromStringStrict("0x0").toStringWithoutPrefix()).toBe("0"); + }); + + it("toStringWithoutPrefix for non-special address", () => { + expect(AccountAddress.fromStringStrict(ADDRESS_TEN.longWith0x).toStringWithoutPrefix()).toBe( + ADDRESS_TEN.longWithout0x, + ); + }); + + it("toStringLong for special address", () => { + expect(AccountAddress.fromStringStrict("0x0").toStringLong()).toBe(ADDRESS_ZERO.longWith0x); + }); + + it("toStringLong for non-special address", () => { + expect(AccountAddress.fromStringStrict(ADDRESS_TEN.longWith0x).toStringLong()).toBe(ADDRESS_TEN.longWith0x); + }); + + it("toStringShort strips leading zeros", () => { + expect(AccountAddress.fromString(ADDRESS_TEN.longWith0x).toStringShort()).toBe("0x10"); + }); + + it("toStringShort for zero address", () => { + expect(AccountAddress.fromString("0x0").toStringShort()).toBe("0x0"); + }); +}); + +describe("AccountAddress isValid", () => { + it("returns valid for correct input", () => { + const result = AccountAddress.isValid({ input: ADDRESS_F.longWith0x, strict: true }); + expect(result.valid).toBe(true); + }); + + it("returns invalid for too long input", () => { + const result = AccountAddress.isValid({ + input: `0x00${ADDRESS_F.longWithout0x}`, + strict: true, + }); + expect(result.valid).toBe(false); + expect(result.invalidReason).toBe(AddressInvalidReason.TOO_LONG); + }); +}); + +describe("AccountAddress equals", () => { + it("compares equal addresses", () => { + const a = AccountAddress.fromString("0x1"); + const b = AccountAddress.fromString("0x1"); + expect(a.equals(b)).toBe(true); + }); + + it("compares unequal addresses", () => { + const a = AccountAddress.fromString("0x1"); + const b = AccountAddress.fromString("0x2"); + expect(a.equals(b)).toBe(false); + }); +}); + +describe("AccountAddress serialization", () => { + it("serializes correctly (raw 32 bytes)", () => { + const address = AccountAddress.fromString(ADDRESS_OTHER.longWith0x); + const serializer = new Serializer(); + serializer.serialize(address); + expect(serializer.toUint8Array()).toEqual(ADDRESS_OTHER.bytes); + }); + + it("deserializes correctly", () => { + const deserializer = new Deserializer(ADDRESS_TEN.bytes); + const address = AccountAddress.deserialize(deserializer); + expect(address.toUint8Array()).toEqual(ADDRESS_TEN.bytes); + }); + + it("round-trips through serialization", () => { + const address = AccountAddress.fromString("0x0102030a0b0c", { maxMissingChars: 63 }); + const serializer = new Serializer(); + serializer.serialize(address); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = AccountAddress.deserialize(deserializer); + expect(deserialized.toUint8Array()).toEqual(address.toUint8Array()); + }); +}); + +describe("AccountAddress error cases", () => { + it("throws on too long hex string", () => { + expect(() => AccountAddress.fromStringStrict(`${ADDRESS_ONE.longWith0x}1`)).toThrow("too long"); + }); + + it("throws on invalid hex chars", () => { + expect(() => AccountAddress.fromStringStrict("0xxyz")).toThrow(); + }); + + it("throws on empty string", () => { + expect(() => AccountAddress.fromStringStrict("0x")).toThrow(); + expect(() => AccountAddress.fromStringStrict("")).toThrow(); + }); + + it("throws on missing 0x prefix in strict mode", () => { + expect(() => AccountAddress.fromStringStrict("0za")).toThrow(); + }); + + it("throws on incorrect byte length for constructor", () => { + expect(() => new AccountAddress(new Uint8Array(31))).toThrow("exactly 32 bytes"); + }); +}); diff --git a/v10/tests/unit/core/authentication-key.test.ts b/v10/tests/unit/core/authentication-key.test.ts new file mode 100644 index 000000000..1aefd5d1b --- /dev/null +++ b/v10/tests/unit/core/authentication-key.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { Deserializer } from "../../../src/bcs/deserializer.js"; +import { Serializer } from "../../../src/bcs/serializer.js"; +import { AccountAddress } from "../../../src/core/account-address.js"; +import { AuthenticationKey } from "../../../src/core/authentication-key.js"; +import { Ed25519PublicKey } from "../../../src/crypto/ed25519.js"; +import { SigningScheme } from "../../../src/crypto/types.js"; + +const ed25519 = { + publicKey: "0xde19e5d1880cac87d57484ce9ed2e84cf0f9599f12e7cc3a52e4e7657a763f2c", + authKey: "0x978c213990c4833df71548df7ce49d54c759d6b6d932de22b24d56060b7af2aa", +}; + +describe("AuthenticationKey", () => { + it("should create with correct length", () => { + const bytes = new Uint8Array(32).fill(1); + const key = new AuthenticationKey({ data: bytes }); + expect(key.toUint8Array().length).toBe(32); + }); + + it("should throw on incorrect length", () => { + expect(() => new AuthenticationKey({ data: new Uint8Array(16) })).toThrow("length should be 32"); + }); + + it("should derive from scheme and bytes (Ed25519)", () => { + const pubKey = new Ed25519PublicKey(ed25519.publicKey); + const authKey = AuthenticationKey.fromSchemeAndBytes({ + scheme: SigningScheme.Ed25519, + input: pubKey.toUint8Array(), + }); + expect(authKey.data.toString()).toBe(ed25519.authKey); + }); + + it("should derive address from authentication key", () => { + const bytes = new Uint8Array(32).fill(0xab); + const key = new AuthenticationKey({ data: bytes }); + const address = key.derivedAddress(); + expect(address).toBeInstanceOf(AccountAddress); + expect(address.toUint8Array()).toEqual(bytes); + }); + + it("should serialize and deserialize correctly", () => { + const bytes = new Uint8Array(32).fill(0x42); + const key = new AuthenticationKey({ data: bytes }); + const serializer = new Serializer(); + key.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = AuthenticationKey.deserialize(deserializer); + expect(deserialized.toUint8Array()).toEqual(bytes); + }); +}); diff --git a/v10/tests/unit/core/network.test.ts b/v10/tests/unit/core/network.test.ts new file mode 100644 index 000000000..9ca5185f5 --- /dev/null +++ b/v10/tests/unit/core/network.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import { + Network, + NetworkToChainId, + NetworkToFaucetAPI, + NetworkToIndexerAPI, + NetworkToNetworkName, + NetworkToNodeAPI, +} from "../../../src/core/network.js"; + +describe("Network enum", () => { + it("has expected values", () => { + expect(Network.MAINNET).toBe("mainnet"); + expect(Network.TESTNET).toBe("testnet"); + expect(Network.DEVNET).toBe("devnet"); + expect(Network.LOCAL).toBe("local"); + expect(Network.CUSTOM).toBe("custom"); + }); +}); + +describe("Network endpoint maps", () => { + it("NetworkToNodeAPI has correct mainnet URL", () => { + expect(NetworkToNodeAPI[Network.MAINNET]).toContain("mainnet.aptoslabs.com"); + }); + + it("NetworkToNodeAPI has correct local URL", () => { + expect(NetworkToNodeAPI[Network.LOCAL]).toContain("127.0.0.1"); + }); + + it("NetworkToIndexerAPI has correct testnet URL", () => { + expect(NetworkToIndexerAPI[Network.TESTNET]).toContain("testnet"); + }); + + it("NetworkToFaucetAPI has correct devnet URL", () => { + expect(NetworkToFaucetAPI[Network.DEVNET]).toContain("devnet"); + }); + + it("NetworkToChainId has mainnet chain ID 1", () => { + expect(NetworkToChainId[Network.MAINNET]).toBe(1); + }); + + it("NetworkToChainId has testnet chain ID 2", () => { + expect(NetworkToChainId[Network.TESTNET]).toBe(2); + }); + + it("NetworkToNetworkName maps correctly", () => { + expect(NetworkToNetworkName[Network.MAINNET]).toBe("mainnet"); + expect(NetworkToNetworkName[Network.TESTNET]).toBe("testnet"); + }); +}); diff --git a/v10/tests/unit/core/type-tag.test.ts b/v10/tests/unit/core/type-tag.test.ts new file mode 100644 index 000000000..9213a31f1 --- /dev/null +++ b/v10/tests/unit/core/type-tag.test.ts @@ -0,0 +1,275 @@ +import { describe, expect, it } from "vitest"; +import { Deserializer } from "../../../src/bcs/deserializer.js"; +import { Serializer } from "../../../src/bcs/serializer.js"; +import { AccountAddress } from "../../../src/core/account-address.js"; +import { + aptosCoinStructTag, + Identifier, + objectStructTag, + optionStructTag, + StructTag, + stringStructTag, + TypeTag, + TypeTagAddress, + TypeTagBool, + TypeTagGeneric, + TypeTagI8, + TypeTagI16, + TypeTagI32, + TypeTagI64, + TypeTagI128, + TypeTagI256, + TypeTagReference, + TypeTagSigner, + TypeTagStruct, + TypeTagU8, + TypeTagU16, + TypeTagU32, + TypeTagU64, + TypeTagU128, + TypeTagU256, + TypeTagVector, +} from "../../../src/core/type-tag.js"; + +describe("Primitive TypeTags", () => { + it("toString returns correct strings", () => { + expect(new TypeTagBool().toString()).toBe("bool"); + expect(new TypeTagU8().toString()).toBe("u8"); + expect(new TypeTagU16().toString()).toBe("u16"); + expect(new TypeTagU32().toString()).toBe("u32"); + expect(new TypeTagU64().toString()).toBe("u64"); + expect(new TypeTagU128().toString()).toBe("u128"); + expect(new TypeTagU256().toString()).toBe("u256"); + expect(new TypeTagI8().toString()).toBe("i8"); + expect(new TypeTagI16().toString()).toBe("i16"); + expect(new TypeTagI32().toString()).toBe("i32"); + expect(new TypeTagI64().toString()).toBe("i64"); + expect(new TypeTagI128().toString()).toBe("i128"); + expect(new TypeTagI256().toString()).toBe("i256"); + expect(new TypeTagAddress().toString()).toBe("address"); + expect(new TypeTagSigner().toString()).toBe("signer"); + }); + + it("serialize and deserialize all primitive types", () => { + const tags: TypeTag[] = [ + new TypeTagBool(), + new TypeTagU8(), + new TypeTagU16(), + new TypeTagU32(), + new TypeTagU64(), + new TypeTagU128(), + new TypeTagU256(), + new TypeTagI8(), + new TypeTagI16(), + new TypeTagI32(), + new TypeTagI64(), + new TypeTagI128(), + new TypeTagI256(), + new TypeTagAddress(), + new TypeTagSigner(), + ]; + + for (const tag of tags) { + const serializer = new Serializer(); + tag.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = TypeTag.deserialize(deserializer); + expect(deserialized.toString()).toBe(tag.toString()); + } + }); +}); + +describe("TypeTag type guards", () => { + it("isBool", () => { + expect(new TypeTagBool().isBool()).toBe(true); + expect(new TypeTagU8().isBool()).toBe(false); + }); + + it("isU8", () => { + expect(new TypeTagU8().isU8()).toBe(true); + expect(new TypeTagU16().isU8()).toBe(false); + }); + + it("isAddress", () => { + expect(new TypeTagAddress().isAddress()).toBe(true); + expect(new TypeTagBool().isAddress()).toBe(false); + }); + + it("isSigner", () => { + expect(new TypeTagSigner().isSigner()).toBe(true); + expect(new TypeTagAddress().isSigner()).toBe(false); + }); + + it("isVector", () => { + expect(new TypeTagVector(new TypeTagU8()).isVector()).toBe(true); + expect(new TypeTagU8().isVector()).toBe(false); + }); + + it("isStruct", () => { + const structTag = new StructTag(AccountAddress.ONE, new Identifier("test"), new Identifier("Test"), []); + expect(new TypeTagStruct(structTag).isStruct()).toBe(true); + expect(new TypeTagU8().isStruct()).toBe(false); + }); + + it("isGeneric", () => { + expect(new TypeTagGeneric(0).isGeneric()).toBe(true); + expect(new TypeTagU8().isGeneric()).toBe(false); + }); + + it("isPrimitive for all primitives", () => { + expect(new TypeTagBool().isPrimitive()).toBe(true); + expect(new TypeTagU8().isPrimitive()).toBe(true); + expect(new TypeTagAddress().isPrimitive()).toBe(true); + expect(new TypeTagSigner().isPrimitive()).toBe(true); + expect(new TypeTagI256().isPrimitive()).toBe(true); + }); + + it("isPrimitive is false for non-primitives", () => { + expect(new TypeTagVector(new TypeTagU8()).isPrimitive()).toBe(false); + expect(new TypeTagGeneric(0).isPrimitive()).toBe(false); + }); +}); + +describe("TypeTagVector", () => { + it("toString returns vector", () => { + expect(new TypeTagVector(new TypeTagU8()).toString()).toBe("vector"); + expect(new TypeTagVector(new TypeTagBool()).toString()).toBe("vector"); + }); + + it("u8() factory creates vector", () => { + expect(TypeTagVector.u8().toString()).toBe("vector"); + }); + + it("nested vectors work", () => { + const nested = new TypeTagVector(new TypeTagVector(new TypeTagU8())); + expect(nested.toString()).toBe("vector>"); + }); + + it("serializes and deserializes correctly", () => { + const tag = new TypeTagVector(new TypeTagAddress()); + const serializer = new Serializer(); + tag.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = TypeTag.deserialize(deserializer); + expect(deserialized.toString()).toBe("vector
"); + }); +}); + +describe("TypeTagStruct", () => { + it("toString returns full module path", () => { + const structTag = new StructTag(AccountAddress.ONE, new Identifier("aptos_coin"), new Identifier("AptosCoin"), []); + const typeTag = new TypeTagStruct(structTag); + expect(typeTag.toString()).toBe("0x1::aptos_coin::AptosCoin"); + }); + + it("toString includes type args", () => { + const structTag = new StructTag(AccountAddress.ONE, new Identifier("option"), new Identifier("Option"), [ + new TypeTagU64(), + ]); + const typeTag = new TypeTagStruct(structTag); + expect(typeTag.toString()).toBe("0x1::option::Option"); + }); + + it("isString/isOption/isObject work correctly", () => { + const strTag = new TypeTagStruct(stringStructTag()); + expect(strTag.isString()).toBe(true); + expect(strTag.isOption()).toBe(false); + + const optTag = new TypeTagStruct(optionStructTag(new TypeTagU8())); + expect(optTag.isOption()).toBe(true); + expect(optTag.isString()).toBe(false); + + const objTag = new TypeTagStruct(objectStructTag(new TypeTagAddress())); + expect(objTag.isObject()).toBe(true); + }); + + it("serializes and deserializes correctly", () => { + const structTag = aptosCoinStructTag(); + const typeTag = new TypeTagStruct(structTag); + const serializer = new Serializer(); + typeTag.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = TypeTag.deserialize(deserializer) as TypeTagStruct; + expect(deserialized.toString()).toBe("0x1::aptos_coin::AptosCoin"); + }); +}); + +describe("TypeTagGeneric", () => { + it("toString returns T", () => { + expect(new TypeTagGeneric(0).toString()).toBe("T0"); + expect(new TypeTagGeneric(5).toString()).toBe("T5"); + }); + + it("throws on negative index", () => { + expect(() => new TypeTagGeneric(-1)).toThrow("cannot be negative"); + }); + + it("serializes and deserializes correctly", () => { + const tag = new TypeTagGeneric(3); + const serializer = new Serializer(); + tag.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = TypeTag.deserialize(deserializer) as TypeTagGeneric; + expect(deserialized.value).toBe(3); + }); +}); + +describe("TypeTagReference", () => { + it("toString returns &type", () => { + expect(new TypeTagReference(new TypeTagSigner()).toString()).toBe("&signer"); + }); +}); + +describe("Identifier", () => { + it("serializes and deserializes correctly", () => { + const id = new Identifier("hello"); + const serializer = new Serializer(); + id.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = Identifier.deserialize(deserializer); + expect(deserialized.identifier).toBe("hello"); + }); +}); + +describe("StructTag", () => { + it("serializes and deserializes correctly", () => { + const tag = new StructTag(AccountAddress.ONE, new Identifier("coin"), new Identifier("Coin"), [new TypeTagBool()]); + const serializer = new Serializer(); + tag.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = StructTag.deserialize(deserializer); + expect(deserialized.address.equals(AccountAddress.ONE)).toBe(true); + expect(deserialized.moduleName.identifier).toBe("coin"); + expect(deserialized.name.identifier).toBe("Coin"); + expect(deserialized.typeArgs.length).toBe(1); + expect(deserialized.typeArgs[0].toString()).toBe("bool"); + }); +}); + +describe("Factory helpers", () => { + it("aptosCoinStructTag", () => { + const tag = aptosCoinStructTag(); + expect(tag.moduleName.identifier).toBe("aptos_coin"); + expect(tag.name.identifier).toBe("AptosCoin"); + expect(tag.address.equals(AccountAddress.ONE)).toBe(true); + }); + + it("stringStructTag", () => { + const tag = stringStructTag(); + expect(tag.moduleName.identifier).toBe("string"); + expect(tag.name.identifier).toBe("String"); + }); + + it("optionStructTag", () => { + const tag = optionStructTag(new TypeTagU64()); + expect(tag.moduleName.identifier).toBe("option"); + expect(tag.name.identifier).toBe("Option"); + expect(tag.typeArgs.length).toBe(1); + }); + + it("objectStructTag", () => { + const tag = objectStructTag(new TypeTagAddress()); + expect(tag.moduleName.identifier).toBe("object"); + expect(tag.name.identifier).toBe("Object"); + }); +}); diff --git a/v10/tests/unit/crypto/ed25519.test.ts b/v10/tests/unit/crypto/ed25519.test.ts new file mode 100644 index 000000000..6ee5ada54 --- /dev/null +++ b/v10/tests/unit/crypto/ed25519.test.ts @@ -0,0 +1,185 @@ +import { describe, expect, it } from "vitest"; +import { Deserializer } from "../../../src/bcs/deserializer.js"; +import { Serializer } from "../../../src/bcs/serializer.js"; +import { + Ed25519PrivateKey, + Ed25519PublicKey, + Ed25519Signature, + isCanonicalEd25519Signature, +} from "../../../src/crypto/ed25519.js"; +import { Hex } from "../../../src/hex/index.js"; + +const ed25519 = { + privateKey: "ed25519-priv-0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5", + privateKeyHex: "0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5", + publicKey: "0xde19e5d1880cac87d57484ce9ed2e84cf0f9599f12e7cc3a52e4e7657a763f2c", + messageEncoded: "68656c6c6f20776f726c64", + signatureHex: + "0x9e653d56a09247570bb174a389e85b9226abd5c403ea6c504b386626a145158cd4efd66fc5e071c0e19538a96a05ddbda24d3c51e1e6a9dacc6bb1ce775cce07", +}; + +const wallet = { + mnemonic: "shoot island position soft burden budget tooth cruel issue economy destroy above", + path: "m/44'/637'/0'/0'/0'", + privateKey: "ed25519-priv-0x5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec", + publicKey: "0xea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c", +}; + +describe("Ed25519PublicKey", () => { + it("should create from hex string", () => { + const hexStr = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + const publicKey = new Ed25519PublicKey(hexStr); + expect(publicKey).toBeInstanceOf(Ed25519PublicKey); + expect(publicKey.toString()).toEqual(hexStr); + }); + + it("should create from Uint8Array", () => { + const bytes = new Uint8Array([ + 1, 35, 69, 103, 137, 171, 205, 239, 1, 35, 69, 103, 137, 171, 205, 239, 1, 35, 69, 103, 137, 171, 205, 239, 1, 35, + 69, 103, 137, 171, 205, 239, + ]); + const publicKey = new Ed25519PublicKey(bytes); + expect(publicKey.toUint8Array()).toEqual(bytes); + }); + + it("should throw on invalid length", () => { + expect(() => new Ed25519PublicKey("0123456789abcdef")).toThrowError( + `PublicKey length should be ${Ed25519PublicKey.LENGTH}`, + ); + }); + + it("should verify signature correctly", () => { + const pubKey = new Ed25519PublicKey(ed25519.publicKey); + const signature = new Ed25519Signature(ed25519.signatureHex); + expect(pubKey.verifySignature({ message: ed25519.messageEncoded, signature })).toBe(true); + }); + + it("should reject incorrect signature", () => { + const pubKey = new Ed25519PublicKey(ed25519.publicKey); + const badSig = new Ed25519Signature( + "0xc5de9e40ac00b371cd83b1c197fa5b665b7449b33cd3cdd305bb78222e06a671a49625ab9aea8a039d4bb70e275768084d62b094bc1b31964f2357b7c1af7e0a", + ); + expect(pubKey.verifySignature({ message: ed25519.messageEncoded, signature: badSig })).toBe(false); + }); + + it("should serialize and deserialize correctly", () => { + const publicKey = new Ed25519PublicKey(ed25519.publicKey); + const serializer = new Serializer(); + publicKey.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = Ed25519PublicKey.deserialize(deserializer); + expect(deserialized.toString()).toEqual(ed25519.publicKey); + }); +}); + +describe("Ed25519PrivateKey", () => { + it("should create from AIP-80 compliant string", () => { + const privateKey = new Ed25519PrivateKey(ed25519.privateKey, false); + expect(privateKey).toBeInstanceOf(Ed25519PrivateKey); + expect(privateKey.toString()).toEqual(ed25519.privateKey); + }); + + it("should create from raw hex string", () => { + const privateKey = new Ed25519PrivateKey(ed25519.privateKeyHex, false); + expect(privateKey.toString()).toEqual(ed25519.privateKey); + }); + + it("should create from Uint8Array", () => { + const bytes = new Uint8Array([ + 197, 51, 140, 210, 81, 194, 45, 170, 140, 156, 156, 201, 79, 73, 140, 200, 165, 199, 225, 210, 231, 82, 135, 165, + 221, 169, 16, 150, 254, 100, 239, 165, + ]); + const privateKey = new Ed25519PrivateKey(bytes, false); + expect(privateKey.toHexString()).toEqual(Hex.fromHexInput(bytes).toString()); + }); + + it("should throw on invalid length", () => { + expect(() => new Ed25519PrivateKey("0123456789abcdef", false)).toThrowError( + `PrivateKey length should be ${Ed25519PrivateKey.LENGTH}`, + ); + }); + + it("should sign correctly", () => { + const privateKey = new Ed25519PrivateKey(ed25519.privateKey); + const signature = privateKey.sign(ed25519.messageEncoded); + expect(signature.toString()).toEqual(ed25519.signatureHex); + }); + + it("should derive the correct public key", () => { + const privateKey = new Ed25519PrivateKey(ed25519.privateKey); + const publicKey = privateKey.publicKey(); + expect(publicKey.toString()).toEqual(ed25519.publicKey); + }); + + it("should generate a random key", () => { + const key1 = Ed25519PrivateKey.generate(); + const key2 = Ed25519PrivateKey.generate(); + expect(key1.toUint8Array().length).toBe(Ed25519PrivateKey.LENGTH); + expect(key1.toString()).not.toEqual(key2.toString()); + }); + + it("should derive from path and mnemonic", () => { + const key = Ed25519PrivateKey.fromDerivationPath(wallet.path, wallet.mnemonic); + expect(key.toString()).toEqual(wallet.privateKey); + }); + + it("should reject invalid derivation path", () => { + expect(() => Ed25519PrivateKey.fromDerivationPath("1234", wallet.mnemonic)).toThrow("Invalid derivation path"); + }); + + it("should serialize and deserialize correctly", () => { + const privateKey = new Ed25519PrivateKey(ed25519.privateKey); + const serializer = new Serializer(); + privateKey.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = Ed25519PrivateKey.deserialize(deserializer); + expect(deserialized.toString()).toEqual(ed25519.privateKey); + }); + + it("should clear key material", () => { + const privateKey = new Ed25519PrivateKey(ed25519.privateKey); + expect(privateKey.isCleared()).toBe(false); + privateKey.clear(); + expect(privateKey.isCleared()).toBe(true); + expect(() => privateKey.sign(ed25519.messageEncoded)).toThrow("cleared from memory"); + }); +}); + +describe("Ed25519Signature", () => { + it("should create from hex string", () => { + const sig = new Ed25519Signature(ed25519.signatureHex); + expect(sig).toBeInstanceOf(Ed25519Signature); + expect(sig.toString()).toEqual(ed25519.signatureHex); + }); + + it("should throw on invalid length", () => { + expect(() => new Ed25519Signature(new Uint8Array(63))).toThrowError( + `Signature length should be ${Ed25519Signature.LENGTH}`, + ); + }); + + it("should serialize and deserialize correctly", () => { + const sig = new Ed25519Signature(ed25519.signatureHex); + const serializer = new Serializer(); + sig.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = Ed25519Signature.deserialize(deserializer); + expect(deserialized.toString()).toEqual(ed25519.signatureHex); + }); +}); + +describe("isCanonicalEd25519Signature", () => { + it("should reject malleable signatures at L", () => { + const sig = new Ed25519Signature( + "0x0000000000000000000000000000000000000000000000000000000000000000edd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010", + ); + expect(isCanonicalEd25519Signature(sig)).toBe(false); + }); + + it("should reject malleable signatures above L", () => { + const sig = new Ed25519Signature( + "0x0000000000000000000000000000000000000000000000000000000000000000edd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000011", + ); + expect(isCanonicalEd25519Signature(sig)).toBe(false); + }); +}); diff --git a/v10/tests/unit/crypto/multi-ed25519.test.ts b/v10/tests/unit/crypto/multi-ed25519.test.ts new file mode 100644 index 000000000..a0240235a --- /dev/null +++ b/v10/tests/unit/crypto/multi-ed25519.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "vitest"; +import { Deserializer } from "../../../src/bcs/deserializer.js"; +import { Serializer } from "../../../src/bcs/serializer.js"; +import { Ed25519PublicKey, Ed25519Signature } from "../../../src/crypto/ed25519.js"; +import { MultiEd25519PublicKey, MultiEd25519Signature } from "../../../src/crypto/multi-ed25519.js"; + +const multiEd25519PkTestObject = { + public_keys: [ + "b9c6ee1630ef3e711144a648db06bbb2284f7274cfbee53ffcee503cc1a49200", + "aef3f4a4b8eca1dfc343361bf8e436bd42de9259c04b8314eb8e2054dd6e82ab", + "8a5762e21ac1cdb3870442c77b4c3af58c7cedb8779d0270e6d4f1e2f7367d74", + ], + threshold: 2, + bytesInStringWithoutPrefix: + "b9c6ee1630ef3e711144a648db06bbb2284f7274cfbee53ffcee503cc1a49200aef3f4a4b8eca1dfc343361bf8e436bd42de9259c04b8314eb8e2054dd6e82ab8a5762e21ac1cdb3870442c77b4c3af58c7cedb8779d0270e6d4f1e2f7367d7402", +}; + +const multiEd25519SigTestObject = { + signatures: [ + "e6f3ba05469b2388492397840183945d4291f0dd3989150de3248e06b4cefe0ddf6180a80a0f04c045ee8f362870cb46918478cd9b56c66076f94f3efd5a8805", + "2ae0818b7e51b853f1e43dc4c89a1f5fabc9cb256030a908f9872f3eaeb048fb1e2b4ffd5a9d5d1caedd0c8b7d6155ed8071e913536fa5c5a64327b6f2d9a102", + ], + bitmap: "c0000000", +}; + +describe("MultiEd25519PublicKey", () => { + it("should create with valid public keys and threshold", () => { + const pubKeys = multiEd25519PkTestObject.public_keys.map((k) => new Ed25519PublicKey(`0x${k}`)); + const multiKey = new MultiEd25519PublicKey({ publicKeys: pubKeys, threshold: multiEd25519PkTestObject.threshold }); + expect(multiKey.publicKeys.length).toBe(3); + expect(multiKey.threshold).toBe(2); + }); + + it("should produce correct bytes representation", () => { + const pubKeys = multiEd25519PkTestObject.public_keys.map((k) => new Ed25519PublicKey(`0x${k}`)); + const multiKey = new MultiEd25519PublicKey({ publicKeys: pubKeys, threshold: multiEd25519PkTestObject.threshold }); + const expected = `0x${multiEd25519PkTestObject.bytesInStringWithoutPrefix}`; + const actual = `0x${Buffer.from(multiKey.toUint8Array()).toString("hex")}`; + expect(actual).toEqual(expected); + }); + + it("should throw if too few keys", () => { + const pubKey = new Ed25519PublicKey(`0x${multiEd25519PkTestObject.public_keys[0]}`); + expect(() => new MultiEd25519PublicKey({ publicKeys: [pubKey], threshold: 1 })).toThrow("Must have between"); + }); + + it("should throw if threshold exceeds key count", () => { + const pubKeys = multiEd25519PkTestObject.public_keys.map((k) => new Ed25519PublicKey(`0x${k}`)); + expect(() => new MultiEd25519PublicKey({ publicKeys: pubKeys, threshold: 4 })).toThrow("Threshold must be between"); + }); + + it("should serialize and deserialize correctly", () => { + const pubKeys = multiEd25519PkTestObject.public_keys.map((k) => new Ed25519PublicKey(`0x${k}`)); + const multiKey = new MultiEd25519PublicKey({ publicKeys: pubKeys, threshold: multiEd25519PkTestObject.threshold }); + const serializer = new Serializer(); + multiKey.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = MultiEd25519PublicKey.deserialize(deserializer); + expect(deserialized.threshold).toBe(multiKey.threshold); + expect(deserialized.publicKeys.length).toBe(multiKey.publicKeys.length); + }); +}); + +describe("MultiEd25519Signature", () => { + it("should create with signatures and bitmap", () => { + const sigs = multiEd25519SigTestObject.signatures.map((s) => new Ed25519Signature(`0x${s}`)); + const bitmap = new Uint8Array([0xc0, 0x00, 0x00, 0x00]); + const multiSig = new MultiEd25519Signature({ signatures: sigs, bitmap }); + expect(multiSig.signatures.length).toBe(2); + expect(multiSig.bitmap).toEqual(bitmap); + }); + + it("should create bitmap from bit indices", () => { + const bitmap = MultiEd25519Signature.createBitmap({ bits: [0, 1] }); + expect(bitmap).toEqual(new Uint8Array([0xc0, 0x00, 0x00, 0x00])); + }); + + it("should throw on duplicate bitmap bits", () => { + expect(() => MultiEd25519Signature.createBitmap({ bits: [0, 0] })).toThrow("Duplicate bits"); + }); + + it("should throw on unsorted bitmap bits", () => { + expect(() => MultiEd25519Signature.createBitmap({ bits: [1, 0] })).toThrow("sorted in ascending order"); + }); + + it("should throw on bit >= MAX_SIGNATURES_SUPPORTED", () => { + expect(() => MultiEd25519Signature.createBitmap({ bits: [32] })).toThrow("Cannot have a signature larger than"); + }); + + it("should serialize and deserialize correctly", () => { + const sigs = multiEd25519SigTestObject.signatures.map((s) => new Ed25519Signature(`0x${s}`)); + const bitmap = new Uint8Array([0xc0, 0x00, 0x00, 0x00]); + const multiSig = new MultiEd25519Signature({ signatures: sigs, bitmap }); + const serializer = new Serializer(); + multiSig.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = MultiEd25519Signature.deserialize(deserializer); + expect(deserialized.signatures.length).toBe(2); + expect(deserialized.bitmap).toEqual(bitmap); + }); +}); diff --git a/v10/tests/unit/crypto/secp256k1.test.ts b/v10/tests/unit/crypto/secp256k1.test.ts new file mode 100644 index 000000000..b24eb1e68 --- /dev/null +++ b/v10/tests/unit/crypto/secp256k1.test.ts @@ -0,0 +1,139 @@ +import { describe, expect, it } from "vitest"; +import { Deserializer } from "../../../src/bcs/deserializer.js"; +import { Serializer } from "../../../src/bcs/serializer.js"; +import { Secp256k1PrivateKey, Secp256k1PublicKey, Secp256k1Signature } from "../../../src/crypto/secp256k1.js"; + +const secp256k1TestObject = { + privateKey: "secp256k1-priv-0xd107155adf816a0a94c6db3c9489c13ad8a1eda7ada2e558ba3bfa47c020347e", + privateKeyHex: "0xd107155adf816a0a94c6db3c9489c13ad8a1eda7ada2e558ba3bfa47c020347e", + publicKey: + "0x04acdd16651b839c24665b7e2033b55225f384554949fef46c397b5275f37f6ee95554d70fb5d9f93c5831ebf695c7206e7477ce708f03ae9bb2862dc6c9e033ea", + messageEncoded: "68656c6c6f20776f726c64", + signatureHex: + "0xd0d634e843b61339473b028105930ace022980708b2855954b977da09df84a770c0b68c29c8ca1b5409a5085b0ec263be80e433c83fcf6debb82f3447e71edca", +}; + +const secp256k1Wallet = { + mnemonic: "shoot island position soft burden budget tooth cruel issue economy destroy above", + path: "m/44'/637'/0'/0/0", + privateKey: "secp256k1-priv-0x1eec55afc2f72c4ab7b46c84d761739035ac420a2b6b22cef3411adaf91ce1f7", + publicKey: + "0x04913871f1d6cb7b867e8671cf63cf7b4c43819539fa0074ff933434bf20bab825b335535251f720fff72fd8b567e414af84aacf2f26ec804562081f2e0b0c9478", +}; + +describe("Secp256k1PublicKey", () => { + it("should create from uncompressed hex (65 bytes)", () => { + const pubKey = new Secp256k1PublicKey(secp256k1TestObject.publicKey); + expect(pubKey).toBeInstanceOf(Secp256k1PublicKey); + expect(pubKey.toUint8Array().length).toBe(Secp256k1PublicKey.LENGTH); + }); + + it("should throw on invalid length", () => { + expect(() => new Secp256k1PublicKey("0x0123456789abcdef")).toThrow("PublicKey length should be"); + }); + + it("should verify signature correctly", () => { + const pubKey = new Secp256k1PublicKey(secp256k1TestObject.publicKey); + const signature = new Secp256k1Signature(secp256k1TestObject.signatureHex); + expect(pubKey.verifySignature({ message: secp256k1TestObject.messageEncoded, signature })).toBe(true); + }); + + it("should reject incorrect signature", () => { + const pubKey = new Secp256k1PublicKey(secp256k1TestObject.publicKey); + const badSig = new Secp256k1Signature(new Uint8Array(64)); + expect(pubKey.verifySignature({ message: secp256k1TestObject.messageEncoded, signature: badSig })).toBe(false); + }); + + it("should serialize and deserialize correctly", () => { + const pubKey = new Secp256k1PublicKey(secp256k1TestObject.publicKey); + const serializer = new Serializer(); + pubKey.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = Secp256k1PublicKey.deserialize(deserializer); + expect(deserialized.toUint8Array()).toEqual(pubKey.toUint8Array()); + }); +}); + +describe("Secp256k1PrivateKey", () => { + it("should create from AIP-80 string", () => { + const key = new Secp256k1PrivateKey(secp256k1TestObject.privateKey, false); + expect(key).toBeInstanceOf(Secp256k1PrivateKey); + expect(key.toString()).toEqual(secp256k1TestObject.privateKey); + }); + + it("should create from raw hex", () => { + const key = new Secp256k1PrivateKey(secp256k1TestObject.privateKeyHex, false); + expect(key.toString()).toEqual(secp256k1TestObject.privateKey); + }); + + it("should throw on invalid length", () => { + expect(() => new Secp256k1PrivateKey("0x0123", false)).toThrow("PrivateKey length should be"); + }); + + it("should sign correctly", () => { + const key = new Secp256k1PrivateKey(secp256k1TestObject.privateKey); + const sig = key.sign(secp256k1TestObject.messageEncoded); + expect(sig.toString()).toEqual(secp256k1TestObject.signatureHex); + }); + + it("should derive the correct public key", () => { + const key = new Secp256k1PrivateKey(secp256k1TestObject.privateKey); + const pubKey = key.publicKey(); + expect(pubKey.toUint8Array()).toEqual(new Secp256k1PublicKey(secp256k1TestObject.publicKey).toUint8Array()); + }); + + it("should generate a random key", () => { + const key1 = Secp256k1PrivateKey.generate(); + const key2 = Secp256k1PrivateKey.generate(); + expect(key1.toUint8Array().length).toBe(Secp256k1PrivateKey.LENGTH); + expect(key1.toString()).not.toEqual(key2.toString()); + }); + + it("should derive from path and mnemonic", () => { + const key = Secp256k1PrivateKey.fromDerivationPath(secp256k1Wallet.path, secp256k1Wallet.mnemonic); + expect(key.toString()).toEqual(secp256k1Wallet.privateKey); + }); + + it("should reject invalid derivation path", () => { + expect(() => Secp256k1PrivateKey.fromDerivationPath("bad/path", secp256k1Wallet.mnemonic)).toThrow( + "Invalid derivation path", + ); + }); + + it("should clear key material", () => { + const key = new Secp256k1PrivateKey(secp256k1TestObject.privateKey); + expect(key.isCleared()).toBe(false); + key.clear(); + expect(key.isCleared()).toBe(true); + expect(() => key.sign(secp256k1TestObject.messageEncoded)).toThrow("cleared from memory"); + }); + + it("should serialize and deserialize correctly", () => { + const key = new Secp256k1PrivateKey(secp256k1TestObject.privateKey); + const serializer = new Serializer(); + key.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = Secp256k1PrivateKey.deserialize(deserializer); + expect(deserialized.toString()).toEqual(secp256k1TestObject.privateKey); + }); +}); + +describe("Secp256k1Signature", () => { + it("should create from hex string", () => { + const sig = new Secp256k1Signature(secp256k1TestObject.signatureHex); + expect(sig.toUint8Array().length).toBe(Secp256k1Signature.LENGTH); + }); + + it("should throw on invalid length", () => { + expect(() => new Secp256k1Signature(new Uint8Array(32))).toThrow("Signature length should be"); + }); + + it("should serialize and deserialize correctly", () => { + const sig = new Secp256k1Signature(secp256k1TestObject.signatureHex); + const serializer = new Serializer(); + sig.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = Secp256k1Signature.deserialize(deserializer); + expect(deserialized.toUint8Array()).toEqual(sig.toUint8Array()); + }); +}); diff --git a/v10/tests/unit/crypto/secp256r1.test.ts b/v10/tests/unit/crypto/secp256r1.test.ts new file mode 100644 index 000000000..c3292e23d --- /dev/null +++ b/v10/tests/unit/crypto/secp256r1.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from "vitest"; +import { Deserializer } from "../../../src/bcs/deserializer.js"; +import { Serializer } from "../../../src/bcs/serializer.js"; +import { Secp256r1PrivateKey, Secp256r1PublicKey, Secp256r1Signature } from "../../../src/crypto/secp256r1.js"; + +const singleSignerSecp256r1 = { + publicKey: + "0x046c761075b12769e9d0cc9995706275352e1bfb8e0085420625aa9cf849e6d62c2c140f0b3b7c53faf78c16648343966d769ccbc8f2fd14bb2c38f6befb91c77b", + privateKey: "secp256r1-priv-0xa814fde3edc91aedf78c0e75bacbcf5e479cd4b27746961cfa1dc8e9b0e4481c", + messageEncoded: "68656c6c6f20776f726c64", + signatureHex: + "0x4fc4bc5f8ed851aec68c64499fa56360b11ea0c8b73fe3f93279e97b700582e55cb9e2ada7ae38951c2bc33d7755529fffc6201504180405c7960715ae0d4ff5", +}; + +describe("Secp256r1PublicKey", () => { + it("should create from uncompressed hex (65 bytes)", () => { + const pubKey = new Secp256r1PublicKey(singleSignerSecp256r1.publicKey); + expect(pubKey.toUint8Array().length).toBe(Secp256r1PublicKey.LENGTH); + }); + + it("should throw on invalid length", () => { + expect(() => new Secp256r1PublicKey("0x0123456789abcdef")).toThrow("PublicKey length should be"); + }); + + it("should verify signature correctly", () => { + const pubKey = new Secp256r1PublicKey(singleSignerSecp256r1.publicKey); + const signature = new Secp256r1Signature(singleSignerSecp256r1.signatureHex); + expect(pubKey.verifySignature({ message: singleSignerSecp256r1.messageEncoded, signature })).toBe(true); + }); + + it("should serialize and deserialize correctly", () => { + const pubKey = new Secp256r1PublicKey(singleSignerSecp256r1.publicKey); + const serializer = new Serializer(); + pubKey.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = Secp256r1PublicKey.deserialize(deserializer); + expect(deserialized.toUint8Array()).toEqual(pubKey.toUint8Array()); + }); +}); + +describe("Secp256r1PrivateKey", () => { + it("should create from AIP-80 string", () => { + const key = new Secp256r1PrivateKey(singleSignerSecp256r1.privateKey); + expect(key.toString()).toEqual(singleSignerSecp256r1.privateKey); + }); + + it("should throw on invalid length", () => { + expect(() => new Secp256r1PrivateKey("0x0123")).toThrow("PrivateKey length should be"); + }); + + it("should sign and verify round-trip", () => { + const key = new Secp256r1PrivateKey(singleSignerSecp256r1.privateKey); + const sig = key.sign(singleSignerSecp256r1.messageEncoded); + const pubKey = key.publicKey(); + expect(pubKey.verifySignature({ message: singleSignerSecp256r1.messageEncoded, signature: sig })).toBe(true); + }); + + it("should derive the correct public key", () => { + const key = new Secp256r1PrivateKey(singleSignerSecp256r1.privateKey); + const pubKey = key.publicKey(); + expect(pubKey.toUint8Array()).toEqual(new Secp256r1PublicKey(singleSignerSecp256r1.publicKey).toUint8Array()); + }); + + it("should generate a random key", () => { + const key1 = Secp256r1PrivateKey.generate(); + const key2 = Secp256r1PrivateKey.generate(); + expect(key1.toUint8Array().length).toBe(Secp256r1PrivateKey.LENGTH); + expect(key1.toString()).not.toEqual(key2.toString()); + }); +}); + +describe("Secp256r1Signature", () => { + it("should create from hex and normalize S", () => { + const sig = new Secp256r1Signature(singleSignerSecp256r1.signatureHex); + expect(sig.toUint8Array().length).toBe(Secp256r1Signature.LENGTH); + }); + + it("should throw on invalid length", () => { + expect(() => new Secp256r1Signature(new Uint8Array(32))).toThrow("Signature length should be"); + }); + + it("should serialize and deserialize correctly", () => { + const sig = new Secp256r1Signature(singleSignerSecp256r1.signatureHex); + const serializer = new Serializer(); + sig.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = Secp256r1Signature.deserialize(deserializer); + expect(deserialized.toUint8Array()).toEqual(sig.toUint8Array()); + }); +}); diff --git a/v10/tests/unit/crypto/single-key.test.ts b/v10/tests/unit/crypto/single-key.test.ts new file mode 100644 index 000000000..aad181fb4 --- /dev/null +++ b/v10/tests/unit/crypto/single-key.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "vitest"; +import { Deserializer } from "../../../src/bcs/deserializer.js"; +import { Serializer } from "../../../src/bcs/serializer.js"; +import { Ed25519PublicKey, Ed25519Signature } from "../../../src/crypto/ed25519.js"; +import { Secp256k1PublicKey, Secp256k1Signature } from "../../../src/crypto/secp256k1.js"; +import { AnyPublicKey, AnySignature } from "../../../src/crypto/single-key.js"; +import { AnyPublicKeyVariant } from "../../../src/crypto/types.js"; + +// Ensure keyless types are registered (imported via barrel) +import "../../../src/crypto/index.js"; + +const ed25519 = { + privateKey: "ed25519-priv-0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5", + publicKey: "0xde19e5d1880cac87d57484ce9ed2e84cf0f9599f12e7cc3a52e4e7657a763f2c", + messageEncoded: "68656c6c6f20776f726c64", + signatureHex: + "0x9e653d56a09247570bb174a389e85b9226abd5c403ea6c504b386626a145158cd4efd66fc5e071c0e19538a96a05ddbda24d3c51e1e6a9dacc6bb1ce775cce07", +}; + +const singleSignerED25519 = { + publicKey: "0xe425451a5dc888ac871976c3c724dec6118910e7d11d344b4b07a22cd94e8c2e", + privateKey: "ed25519-priv-0xf508cbef4e0fe463204aab724a90791c9a9dbe60a53b4978bbddbc712b55f2fd", + messageEncoded: "68656c6c6f20776f726c64", + signatureHex: + "0xc6f50f4e0cb1961f6f7b28be1a1d80e3ece240dfbb7bd8a8b03cc26bfd144fc176295d7c322c5bf3d9669d2ad49d8bdbfe77254b4a6393d8c49da04b40cee600", +}; + +describe("AnyPublicKey", () => { + it("should wrap Ed25519 key with correct variant", () => { + const innerKey = new Ed25519PublicKey(ed25519.publicKey); + const anyKey = new AnyPublicKey(innerKey); + expect(anyKey.variant).toBe(AnyPublicKeyVariant.Ed25519); + expect(anyKey.publicKey).toBe(innerKey); + }); + + it("should wrap Secp256k1 key with correct variant", () => { + const innerKey = new Secp256k1PublicKey( + "0x04acdd16651b839c24665b7e2033b55225f384554949fef46c397b5275f37f6ee95554d70fb5d9f93c5831ebf695c7206e7477ce708f03ae9bb2862dc6c9e033ea", + ); + const anyKey = new AnyPublicKey(innerKey); + expect(anyKey.variant).toBe(AnyPublicKeyVariant.Secp256k1); + }); + + it("should verify Ed25519 signature via AnyPublicKey", () => { + const innerKey = new Ed25519PublicKey(singleSignerED25519.publicKey); + const anyKey = new AnyPublicKey(innerKey); + const sig = new Ed25519Signature(singleSignerED25519.signatureHex); + const anySig = new AnySignature(sig); + expect(anyKey.verifySignature({ message: singleSignerED25519.messageEncoded, signature: anySig })).toBe(true); + }); + + it("should serialize and deserialize Ed25519 AnyPublicKey", () => { + const innerKey = new Ed25519PublicKey(ed25519.publicKey); + const anyKey = new AnyPublicKey(innerKey); + const serializer = new Serializer(); + anyKey.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = AnyPublicKey.deserialize(deserializer); + expect(deserialized.variant).toBe(AnyPublicKeyVariant.Ed25519); + expect(deserialized.publicKey.toUint8Array()).toEqual(innerKey.toUint8Array()); + }); + + it("should serialize and deserialize Secp256k1 AnyPublicKey", () => { + const innerKey = new Secp256k1PublicKey( + "0x04acdd16651b839c24665b7e2033b55225f384554949fef46c397b5275f37f6ee95554d70fb5d9f93c5831ebf695c7206e7477ce708f03ae9bb2862dc6c9e033ea", + ); + const anyKey = new AnyPublicKey(innerKey); + const serializer = new Serializer(); + anyKey.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = AnyPublicKey.deserialize(deserializer); + expect(deserialized.variant).toBe(AnyPublicKeyVariant.Secp256k1); + }); +}); + +describe("AnySignature", () => { + it("should wrap Ed25519 signature with correct variant", () => { + const sig = new Ed25519Signature(ed25519.signatureHex); + const anySig = new AnySignature(sig); + expect(anySig.signature).toBe(sig); + }); + + it("should wrap Secp256k1 signature with correct variant", () => { + const sig = new Secp256k1Signature( + "0xd0d634e843b61339473b028105930ace022980708b2855954b977da09df84a770c0b68c29c8ca1b5409a5085b0ec263be80e433c83fcf6debb82f3447e71edca", + ); + const anySig = new AnySignature(sig); + expect(anySig.signature).toBe(sig); + }); + + it("should serialize and deserialize Ed25519 AnySignature", () => { + const sig = new Ed25519Signature(ed25519.signatureHex); + const anySig = new AnySignature(sig); + const serializer = new Serializer(); + anySig.serialize(serializer); + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserialized = AnySignature.deserialize(deserializer); + expect(deserialized.signature.toUint8Array()).toEqual(sig.toUint8Array()); + }); +}); diff --git a/v10/tests/unit/hex/hex.test.ts b/v10/tests/unit/hex/hex.test.ts new file mode 100644 index 000000000..07bbb9693 --- /dev/null +++ b/v10/tests/unit/hex/hex.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, test } from "vitest"; +import { Hex, HexInvalidReason, hexToAsciiString, ParsingError } from "../../../src/hex/index.js"; + +describe("Hex", () => { + test("creates from Uint8Array", () => { + const hex = new Hex(new Uint8Array([0xab, 0xcd])); + expect(hex.toUint8Array()).toEqual(new Uint8Array([0xab, 0xcd])); + }); + + test("converts to string with 0x prefix", () => { + const hex = new Hex(new Uint8Array([0x41, 0x42])); + expect(hex.toString()).toBe("0x4142"); + }); + + test("converts to string without 0x prefix", () => { + const hex = new Hex(new Uint8Array([0x41, 0x42])); + expect(hex.toStringWithoutPrefix()).toBe("4142"); + }); + + test("fromHexString with 0x prefix", () => { + const hex = Hex.fromHexString("0xabcd"); + expect(hex.toUint8Array()).toEqual(new Uint8Array([0xab, 0xcd])); + }); + + test("fromHexString without 0x prefix", () => { + const hex = Hex.fromHexString("abcd"); + expect(hex.toUint8Array()).toEqual(new Uint8Array([0xab, 0xcd])); + }); + + test("fromHexString throws on empty string", () => { + expect(() => Hex.fromHexString("0x")).toThrow(ParsingError); + expect(() => Hex.fromHexString("")).toThrow(ParsingError); + }); + + test("fromHexString throws on odd-length string", () => { + expect(() => Hex.fromHexString("abc")).toThrow(ParsingError); + }); + + test("fromHexString throws on invalid hex chars", () => { + expect(() => Hex.fromHexString("0xgg")).toThrow(ParsingError); + }); + + test("fromHexInput handles string", () => { + const hex = Hex.fromHexInput("0x1234"); + expect(hex.toUint8Array()).toEqual(new Uint8Array([0x12, 0x34])); + }); + + test("fromHexInput handles Uint8Array", () => { + const bytes = new Uint8Array([1, 2, 3]); + const hex = Hex.fromHexInput(bytes); + expect(hex.toUint8Array()).toBe(bytes); + }); + + test("hexInputToUint8Array", () => { + expect(Hex.hexInputToUint8Array("0x0102")).toEqual(new Uint8Array([1, 2])); + const bytes = new Uint8Array([5, 6]); + expect(Hex.hexInputToUint8Array(bytes)).toBe(bytes); + }); + + test("hexInputToString", () => { + expect(Hex.hexInputToString("abcd")).toBe("0xabcd"); + expect(Hex.hexInputToString(new Uint8Array([0xab, 0xcd]))).toBe("0xabcd"); + }); + + test("hexInputToStringWithoutPrefix", () => { + expect(Hex.hexInputToStringWithoutPrefix("0xabcd")).toBe("abcd"); + }); + + test("isValid returns valid for good input", () => { + const result = Hex.isValid("0xabcd"); + expect(result.valid).toBe(true); + }); + + test("isValid returns invalid reason for bad input", () => { + const result = Hex.isValid("0x"); + expect(result.valid).toBe(false); + expect(result.invalidReason).toBe(HexInvalidReason.TOO_SHORT); + }); + + test("isValid detects odd length", () => { + const result = Hex.isValid("abc"); + expect(result.valid).toBe(false); + expect(result.invalidReason).toBe(HexInvalidReason.INVALID_LENGTH); + }); + + test("isValid detects invalid chars", () => { + const result = Hex.isValid("0xzzzz"); + expect(result.valid).toBe(false); + expect(result.invalidReason).toBe(HexInvalidReason.INVALID_HEX_CHARS); + }); + + test("equals compares correctly", () => { + const a = Hex.fromHexString("0x1234"); + const b = Hex.fromHexString("1234"); + const c = Hex.fromHexString("0x5678"); + expect(a.equals(b)).toBe(true); + expect(a.equals(c)).toBe(false); + }); + + test("equals returns false for different lengths", () => { + const a = Hex.fromHexString("0x12"); + const b = Hex.fromHexString("0x1234"); + expect(a.equals(b)).toBe(false); + }); +}); + +describe("hexToAsciiString", () => { + test("converts hex to ASCII", () => { + expect(hexToAsciiString("0x48656c6c6f")).toBe("Hello"); + }); +}); diff --git a/v10/tests/unit/transactions/authenticator.test.ts b/v10/tests/unit/transactions/authenticator.test.ts new file mode 100644 index 000000000..01be447ca --- /dev/null +++ b/v10/tests/unit/transactions/authenticator.test.ts @@ -0,0 +1,108 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; +import { Deserializer } from "../../../src/bcs/deserializer.js"; +import { Serializer } from "../../../src/bcs/serializer.js"; +import { Ed25519PrivateKey, Ed25519PublicKey, type Ed25519Signature } from "../../../src/crypto/ed25519.js"; +import { AnyPublicKey, AnySignature } from "../../../src/crypto/single-key.js"; +import { + AccountAbstractionMessage, + AccountAuthenticatorAbstraction, + AccountAuthenticatorEd25519, + AccountAuthenticatorSingleKey, +} from "../../../src/transactions/authenticator.js"; + +const ed25519Fixtures = { + privateKey: "ed25519-priv-0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5", + publicKey: "0xde19e5d1880cac87d57484ce9ed2e84cf0f9599f12e7cc3a52e4e7657a763f2c", + message: "68656c6c6f20776f726c64", +}; + +describe("AccountAuthenticatorEd25519", () => { + it("serializes and deserializes", () => { + const publicKey = new Ed25519PublicKey(ed25519Fixtures.publicKey); + const privateKey = new Ed25519PrivateKey(ed25519Fixtures.privateKey); + const signature = privateKey.sign(ed25519Fixtures.message); + + const auth = new AccountAuthenticatorEd25519(publicKey, signature as Ed25519Signature); + const serializer = new Serializer(); + auth.serialize(serializer); + const bytes = serializer.toUint8Array(); + + const des = new Deserializer(bytes); + // Skip the variant index + des.deserializeUleb128AsU32(); + const restored = AccountAuthenticatorEd25519.load(des); + expect(restored.public_key.toString()).toEqual(publicKey.toString()); + expect(restored.signature.toString()).toEqual(signature.toString()); + }); +}); + +describe("AccountAuthenticatorSingleKey", () => { + it("serializes and deserializes", () => { + const privateKey = new Ed25519PrivateKey(ed25519Fixtures.privateKey); + const anyPubKey = new AnyPublicKey(privateKey.publicKey()); + const rawSig = privateKey.sign(ed25519Fixtures.message); + const anySig = new AnySignature(rawSig); + + const auth = new AccountAuthenticatorSingleKey(anyPubKey, anySig); + const serializer = new Serializer(); + auth.serialize(serializer); + const bytes = serializer.toUint8Array(); + + const des = new Deserializer(bytes); + des.deserializeUleb128AsU32(); // skip variant + const restored = AccountAuthenticatorSingleKey.load(des); + expect(restored.public_key.toString()).toEqual(anyPubKey.toString()); + }); +}); + +describe("AccountAuthenticatorAbstraction", () => { + it("creates with valid function info", () => { + const auth = new AccountAuthenticatorAbstraction( + "0x1::permissioned_delegation::authenticate", + new Uint8Array(32), + new Uint8Array(64), + ); + expect(auth.functionInfo).toBe("0x1::permissioned_delegation::authenticate"); + }); + + it("throws on invalid function info", () => { + expect(() => new AccountAuthenticatorAbstraction("invalid", new Uint8Array(32), new Uint8Array(64))).toThrow( + "Invalid function info", + ); + }); + + it("serializes and deserializes without account identity", () => { + const auth = new AccountAuthenticatorAbstraction( + "0x1::permissioned_delegation::authenticate", + new Uint8Array(32), + new Uint8Array(64), + ); + const serializer = new Serializer(); + auth.serialize(serializer); + const bytes = serializer.toUint8Array(); + + const des = new Deserializer(bytes); + des.deserializeUleb128AsU32(); // skip outer variant + const restored = AccountAuthenticatorAbstraction.load(des); + expect(restored.functionInfo).toBe("0x1::permissioned_delegation::authenticate"); + }); +}); + +describe("AccountAbstractionMessage", () => { + it("serializes and deserializes", () => { + const message = new AccountAbstractionMessage( + new Uint8Array([1, 2, 3]), + "0x1::permissioned_delegation::authenticate", + ); + const serializer = new Serializer(); + message.serialize(serializer); + const bytes = serializer.toUint8Array(); + + const des = new Deserializer(bytes); + const restored = AccountAbstractionMessage.deserialize(des); + expect(restored.functionInfo).toBe("0x1::permissioned_delegation::authenticate"); + }); +}); diff --git a/v10/tests/unit/transactions/transactions.test.ts b/v10/tests/unit/transactions/transactions.test.ts new file mode 100644 index 000000000..f9515dc81 --- /dev/null +++ b/v10/tests/unit/transactions/transactions.test.ts @@ -0,0 +1,159 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; +import { Deserializer } from "../../../src/bcs/deserializer.js"; +import { Serializer } from "../../../src/bcs/serializer.js"; +import { AccountAddress } from "../../../src/core/account-address.js"; +import { ChainId } from "../../../src/transactions/chain-id.js"; +import { + FeePayerRawTransaction, + MultiAgentRawTransaction, + RawTransaction, + RawTransactionWithData, +} from "../../../src/transactions/raw-transaction.js"; +import { generateSigningMessage } from "../../../src/transactions/signing-message.js"; +import { + EntryFunction, + TransactionPayload, + TransactionPayloadEntryFunction, +} from "../../../src/transactions/transaction-payload.js"; + +// ── ChainId ── + +describe("ChainId", () => { + it("serializes and deserializes", () => { + const chainId = new ChainId(4); + const serializer = new Serializer(); + chainId.serialize(serializer); + const bytes = serializer.toUint8Array(); + + const deserialized = ChainId.deserialize(new Deserializer(bytes)); + expect(deserialized.chainId).toBe(4); + }); +}); + +// ── RawTransaction ── + +describe("RawTransaction", () => { + it("serializes and deserializes a basic transaction", () => { + const sender = AccountAddress.fromString("0x1"); + const payload = new TransactionPayloadEntryFunction(EntryFunction.build("0x1::aptos_account", "transfer", [], [])); + const rawTxn = new RawTransaction(sender, 1n, payload, 200000n, 100n, 100n, new ChainId(4)); + + const serializer = new Serializer(); + rawTxn.serialize(serializer); + const bytes = serializer.toUint8Array(); + + const deserialized = RawTransaction.deserialize(new Deserializer(bytes)); + expect(deserialized.sender.toString()).toEqual(sender.toString()); + expect(deserialized.sequence_number).toEqual(1n); + expect(deserialized.max_gas_amount).toEqual(200000n); + expect(deserialized.gas_unit_price).toEqual(100n); + expect(deserialized.expiration_timestamp_secs).toEqual(100n); + expect(deserialized.chain_id.chainId).toEqual(4); + }); +}); + +// ── FeePayerRawTransaction ── + +describe("FeePayerRawTransaction", () => { + it("serializes and deserializes via RawTransactionWithData", () => { + const sender = AccountAddress.fromString("0x1"); + const feePayer = AccountAddress.fromString("0x2"); + const payload = new TransactionPayloadEntryFunction(EntryFunction.build("0x1::aptos_account", "transfer", [], [])); + const rawTxn = new RawTransaction(sender, 0n, payload, 200000n, 100n, 100n, new ChainId(4)); + const feePayerTxn = new FeePayerRawTransaction(rawTxn, [], feePayer); + + const serializer = new Serializer(); + feePayerTxn.serialize(serializer); + const bytes = serializer.toUint8Array(); + + // Deserialize goes through RawTransactionWithData which consumes the variant + const deserialized = RawTransactionWithData.deserialize(new Deserializer(bytes)); + expect(deserialized).toBeInstanceOf(FeePayerRawTransaction); + const fptxn = deserialized as FeePayerRawTransaction; + expect(fptxn.raw_txn.sender.toString()).toEqual(sender.toString()); + expect(fptxn.fee_payer_address.toString()).toEqual(feePayer.toString()); + expect(fptxn.secondary_signer_addresses.length).toBe(0); + }); +}); + +// ── MultiAgentRawTransaction ── + +describe("MultiAgentRawTransaction", () => { + it("serializes and deserializes via RawTransactionWithData", () => { + const sender = AccountAddress.fromString("0x1"); + const secondary = AccountAddress.fromString("0x3"); + const payload = new TransactionPayloadEntryFunction(EntryFunction.build("0x1::aptos_account", "transfer", [], [])); + const rawTxn = new RawTransaction(sender, 0n, payload, 200000n, 100n, 100n, new ChainId(4)); + const multiAgentTxn = new MultiAgentRawTransaction(rawTxn, [secondary]); + + const serializer = new Serializer(); + multiAgentTxn.serialize(serializer); + const bytes = serializer.toUint8Array(); + + const deserialized = RawTransactionWithData.deserialize(new Deserializer(bytes)); + expect(deserialized).toBeInstanceOf(MultiAgentRawTransaction); + const matxn = deserialized as MultiAgentRawTransaction; + expect(matxn.raw_txn.sender.toString()).toEqual(sender.toString()); + expect(matxn.secondary_signer_addresses.length).toBe(1); + expect(matxn.secondary_signer_addresses[0].toString()).toEqual(secondary.toString()); + }); +}); + +// ── TransactionPayload ── + +describe("TransactionPayload", () => { + it("serializes and deserializes EntryFunction payload with no type args", () => { + const payload = new TransactionPayloadEntryFunction(EntryFunction.build("0x1::aptos_account", "transfer", [], [])); + + const serializer = new Serializer(); + payload.serialize(serializer); + const bytes = serializer.toUint8Array(); + + const deserialized = TransactionPayload.deserialize(new Deserializer(bytes)); + expect(deserialized).toBeInstanceOf(TransactionPayloadEntryFunction); + }); +}); + +// ── generateSigningMessage ── + +describe("generateSigningMessage", () => { + it("generates signing message with valid domain separator", () => { + const data = new Uint8Array([1, 2, 3]); + const message = generateSigningMessage(data, "APTOS::RawTransaction"); + expect(message).toBeInstanceOf(Uint8Array); + // generateSigningMessage returns SHA3-256(domain_separator) || data + // = 32 bytes prefix + 3 bytes data = 35 bytes + expect(message.length).toBe(35); + }); + + it("throws on invalid domain separator", () => { + const data = new Uint8Array([1, 2, 3]); + expect(() => generateSigningMessage(data, "Invalid::Separator")).toThrow("APTOS::"); + }); +}); + +// ── EntryFunction.build ── + +describe("EntryFunction.build", () => { + it("parses module ID correctly", () => { + const ef = EntryFunction.build("0x1::aptos_account", "transfer", [], []); + // AccountAddress.toString() returns short form + expect(ef.module_name.address.toString()).toEqual("0x1"); + expect(ef.module_name.name.identifier).toEqual("aptos_account"); + expect(ef.function_name.identifier).toEqual("transfer"); + }); + + it("serializes and deserializes", () => { + const ef = EntryFunction.build("0x1::aptos_account", "transfer", [], []); + const serializer = new Serializer(); + ef.serialize(serializer); + const bytes = serializer.toUint8Array(); + + const deserialized = EntryFunction.deserialize(new Deserializer(bytes)); + expect(deserialized.module_name.name.identifier).toBe("aptos_account"); + expect(deserialized.function_name.identifier).toBe("transfer"); + }); +}); diff --git a/v10/tsconfig.build.json b/v10/tsconfig.build.json new file mode 100644 index 000000000..0a33ed8c3 --- /dev/null +++ b/v10/tsconfig.build.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "declarationDir": "./dist/types", + "declarationMap": true, + "outDir": "./dist/esm", + "sourceMap": true, + "strict": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "skipLibCheck": true, + "esModuleInterop": false, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +} diff --git a/v10/tsconfig.json b/v10/tsconfig.json new file mode 100644 index 000000000..22deb534e --- /dev/null +++ b/v10/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "skipLibCheck": true, + "esModuleInterop": false, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src", "tests"] +} diff --git a/v10/vitest.config.ts b/v10/vitest.config.ts new file mode 100644 index 000000000..3c25dac4a --- /dev/null +++ b/v10/vitest.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + globals: true, + include: ["tests/**/*.test.ts"], + pool: "forks", + maxForks: 4, + testTimeout: 30_000, + hookTimeout: 120_000, + coverage: { + provider: "v8", + thresholds: { + branches: 60, + functions: 70, + lines: 70, + statements: 70, + }, + exclude: ["dist", "node_modules", "tests"], + }, + }, +}); diff --git a/v10/vitest.e2e.config.ts b/v10/vitest.e2e.config.ts new file mode 100644 index 000000000..9b95dc1a8 --- /dev/null +++ b/v10/vitest.e2e.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + globals: true, + include: ["tests/e2e/**/*.test.ts"], + pool: "forks", + maxForks: 2, + testTimeout: 60_000, + hookTimeout: 120_000, + globalSetup: "tests/e2e/setup.ts", + }, +});