diff --git a/.gitignore b/.gitignore
index 1e2b3be..ec50cb5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,6 +44,7 @@ yarn-error.log*
# JUnit test-result output (CI artifact)
/reports/
+/junit.xml
# oh-my-claudecode runtime state (operational artifacts, never committed)
.omc/
diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs
index b2bca3e..da93c60 100644
--- a/.pnpmfile.cjs
+++ b/.pnpmfile.cjs
@@ -4,9 +4,9 @@
* pnpm hook to resolve @wireio packages from the local wire-libraries-ts monorepo.
*
* Usage:
- * 1. Add packages you want to link to the `localOverrides` map below.
- * 2. Run `pnpm install` — pnpm will use these local paths instead of the registry.
- * 3. Comment out or remove entries to revert to registry versions.
+ * 1. Build any supported sibling package outputs you want to use locally.
+ * 2. Run `pnpm install --lockfile=false` to consume available local packages.
+ * 3. Remove those outputs to resolve packages from the registry again.
*
* Docs: https://pnpm.io/pnpmfile
*/
@@ -28,30 +28,32 @@ function isDirectory(dirPath) {
}
}
-/**
- * Map of package names to their local directory in wire-libraries-ts.
- * Uncomment the entries you want to link locally.
- */
+/** Map of locally available packages keyed by package name. */
const localOverrides = {}
// AS THE PROTOBUF LIBS HAVE BEEN RELOCATED TO SYSIO
// WE CAN NOW USE THE MODELS WITHOUT ISSUE.
// CIRCULAR DEP REMOVED
-const wireOPPPkgPaths = ["typescript", "solidity"].map(target => [
- `@wireio/opp-${target}-models`,
- Path.resolve(__dirname, "..", "wire-sysio", "build", "opp", target)
-])
+const wirePkgPaths = [
+ ...["typescript", "solidity"].map(target => [
+ `@wireio/opp-${target}-models`,
+ Path.resolve(__dirname, "..", "wire-sysio", "build", "opp", target)
+ ]),
+ ...["ethereum","solana"].map(target => [
+ `@wireio/outpost-${target}-artifacts`,
+ Path.resolve(__dirname, "..", `wire-${target}`, "build", "sdk-artifacts")
+ ])
+]
-wireOPPPkgPaths
+wirePkgPaths
.filter(([, path]) => isDirectory(path))
.forEach(([pkgName, path]) => {
localOverrides[pkgName] = path
})
/**
- * `readPackage` hook, which links locally available versions of
- * shared libraries and models.
+ * `readPackage` hook, which links locally available packages.
*
* @param pkg
* @param context
diff --git a/CLAUDE.md b/CLAUDE.md
index a6eac91..5aa5f5c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -5,9 +5,7 @@
## Build & Development
```bash
-pnpm install # Install registry deps (pnpm 10.34.5, Node >=22)
-# Link local OPP models from wire-sysio:
-WIRE_LINK_LOCAL_OPP_MODELS=1 pnpm install --lockfile=false
+pnpm install # Install deps (pnpm 10.34.5, Node >=22); available sibling OPP models link automatically
pnpm build # Build all packages via tsc -b
pnpm build:dev # Watch mode (incremental)
pnpm test # Build + jest (all packages)
@@ -36,6 +34,7 @@ pnpm workspaces with TypeScript composite project references. No Lerna/Nx.
| `@wireio/shared-web` | Web-specific utilities | No | ESM |
| `@wireio/shared-node` | Node.js utilities | Yes | Hybrid ESM+CJS |
| `@wireio/sdk-core` | Wire blockchain SDK types/primitives | Yes | Hybrid ESM+CJS |
+| `@wireio/sdk-outpost` | Typed, verified external-chain outpost clients | No — first release pending | Hybrid ESM+CJS |
| `@wireio/wallet-ext-sdk` | Wallet extension client SDK | Yes | ESM |
| `@wireio/wallet-browser-ext` | Chrome extension developer wallet | No | Webpack bundle |
@@ -46,6 +45,8 @@ shared ──→ shared-web
──→ shared-node
sdk-core ──→ wallet-ext-sdk ──→ wallet-browser-ext
+
+source artifact libraries ──→ sdk-outpost external-chain clients
```
Protoc plugins and bundler are standalone (no internal deps).
@@ -68,7 +69,7 @@ Root `tsconfig.json` has project references to all packages. Build order is reso
## Hybrid ESM/CJS Build Pattern
-Packages that publish both ESM and CJS (`shared`, `sdk-core`, `shared-node`) use:
+Packages that publish both ESM and CJS (`shared`, `sdk-core`, `sdk-outpost`, `shared-node`) use:
1. Two tsconfig files: one for `lib/esm/`, one for `lib/cjs/`
2. Post-build: `scripts/fix-hybrid-output.mjs` patches relative imports with `.js` extensions and creates `lib/cjs/package.json` with `{"type":"commonjs"}`
@@ -191,12 +192,19 @@ Every new/modified symbol ships unit tests in the same change. Tests never assum
## CI/CD
-GitHub Actions (`.github/workflows/publish-npm.yaml`):
-- Triggers on push to `master` (skips if `[skip release]` in commit message)
-- Bumps all packages patch version (`pnpm -r exec -- pnpm version patch`)
-- Auto-commits `chore(release): bump patch [skip release]`
-- Publishes all non-private packages to npm with provenance (`pnpm -r publish --access public --provenance`)
-- Published package manifests must keep `repository.url` set to `https://github.com/Wire-Network/wire-libraries-ts` so npm provenance matches GitHub Actions source metadata.
+GitHub Actions uses a two-gate release flow:
+- `prepare-release.yaml` is manually dispatched with a bump type. It bumps each
+ package on its own version track and opens a release-preparation PR; it never
+ pushes directly to `master` or publishes.
+- After that PR is reviewed and merged, `tag-release.yaml` is manually
+ dispatched and pauses for approval in the `release` environment.
+- The approved job installs the workspace, builds and tests, publishes all
+ non-private packages in dependency order, creates the annotated tag, and
+ creates the GitHub release.
+- `workspace:*` dependencies become concrete current workspace versions at
+ publish time. Published package manifests must keep `repository.url` set to
+ `https://github.com/Wire-Network/wire-libraries-ts` so npm provenance matches
+ GitHub Actions source metadata.
## Documentation Comments
@@ -216,6 +224,15 @@ All generated or modified code **must** include JSDoc comments (`/** ... */`), c
- System-contract `prepare` prefers synchronous ABI encoding but must retain a typed `AnyAction` fallback so `APIClient` can resolve the deployed ABI. Never invent a default write authorization in the public SDK.
- `packages/sdk-core/src/contracts/sysio/reserv` owns public `sysio.reserv` registry reads, normalized rows, matching, rewards, and read-only quote helpers. External-chain reserve custody belongs in the ABI/IDL-owning chain SDK.
- `packages/sdk-core/src/contracts/sysio/uwrit` preserves raw request bytes and exposes `sourceRequestId` for swap correlation. External outpost ids are big-endian; synthetic WIRE queue ids are little-endian and high-bit tagged.
+- `packages/sdk-outpost` owns typed Ethereum/Solana clients and validates caller-supplied immutable deployment profiles. Canonical ABIs, IDLs, runtime templates, program binaries, ethers v6 factories, and Anchor types come from exact packages owned by `wire-ethereum` and `wire-solana`. For local platform development, the root pnpm hook links available sibling artifact outputs; never commit `file:`/`link:` specs to package manifests or the lockfile.
+- Producer repositories assemble those npm packages from checksummed deployment handoffs. `sdk-outpost` never downloads the handoff or owns producer publication inputs.
+- `sdk-outpost` registers exact producer package pairs in an internal compile-time artifact-suite registry. Select suites from deployment-profile ABI/IDL identity and verify exact live runtime identity; never key suite selection by environment names or turn it into an endpoint catalog/runtime package loader.
+- `OutpostClient.create` is sdk-outpost's only published client-construction facade. It delegates family selection to an internal factory; concrete Ethereum/Solana instance types remain available for typing, but their backend factories and module paths are not package entrypoints.
+- `packages/sdk-outpost` owns external reserve lifecycle, swap execution, and BAR-backed Ethereum node-owner registration. BAR is an optional deployment capability: profiles without it preserve reserve and swap behavior, while node-owner access fails closed. Node-owner registration must use BAR's canonical WireNodes address and is not staking; staking remains outside this package until its dedicated migration.
+- `sdk-outpost` accepts caller-owned providers and deployment profiles, verifies exact Ethereum implementations and Solana ProgramData against source-owned runtime artifacts, and never owns mutable endpoint catalogs. A same-code cluster respin requires a new profile, not an artifact or SDK release; any deployable binary change requires both a producer artifact and SDK release.
+- A connected outpost client proves deployment compatibility, not swap or stake readiness. Wire-chain orchestration remains in `sdk-core`, and consumers must retain flow-specific capability gates.
+- Publish `sdk-outpost` only through the repository release workflow after its normal build and tests pass.
+- Do not describe `sdk-outpost` as npm-available until `npm view` succeeds for both exact producer artifact versions and `@wireio/sdk-outpost`, and a clean platform-compatible install passes without sibling artifact links.
- `wallet-browser-ext` uses a global shim to avoid `new Function()` restrictions in Chrome MV3
- Path aliases in tsconfig base resolve to `src/` for dev, but published packages use `lib/` — jest module name maps handle this mismatch
- Node >=22 required (package.json says >=22, README says >=24 — actual CI uses v24)
diff --git a/README.md b/README.md
index dedfcf2..46d4ca2 100644
--- a/README.md
+++ b/README.md
@@ -10,9 +10,17 @@ A monorepo containing shared TypeScript libraries for Wire applications, providi
| [`@wireio/shared-web`](packages/shared-web/) | Web-specific utilities | *private* |
| [`@wireio/shared-node`](packages/shared-node/) | Node.js-specific utilities | *private* |
| [`@wireio/sdk-core`](packages/sdk-core/) | Wire blockchain SDK core types, primitives, signing helpers, generated `sysio` contract proxy, and domain workflows such as multisig and reserves | [](https://www.npmjs.com/package/@wireio/sdk-core) |
+| [`@wireio/sdk-outpost`](packages/sdk-outpost/) | Strictly typed Ethereum and Solana outpost clients consuming source-owned artifact libraries | *first release pending* |
| [`@wireio/wallet-ext-sdk`](packages/wallet-ext-sdk/) | Client SDK for the Wire Wallet browser extension | [](https://www.npmjs.com/package/@wireio/wallet-ext-sdk) |
| [`@wireio/wallet-browser-ext`](packages/wallet-browser-ext/) | Chrome extension developer wallet for Wire | *private* |
+The sdk-outpost package consumes exact published versions of the Ethereum and
+Solana artifact libraries, including their ethers v6 factories and Anchor
+types. Chain bindings are generated and verified by those producer repos, not
+inside this monorepo. An internal compile-time artifact-suite registry selects
+compatible producer bindings from caller-supplied deployment profiles without
+owning endpoints or environment configuration.
+
## Examples
| Example | Description |
@@ -30,9 +38,6 @@ A monorepo containing shared TypeScript libraries for Wire applications, providi
# Install dependencies
pnpm install
-# Install with locally generated OPP models from wire-sysio
-WIRE_LINK_LOCAL_OPP_MODELS=1 pnpm install --lockfile=false
-
# Build all packages
pnpm build
@@ -56,6 +61,7 @@ wire-libraries-ts/
│ ├── shared-web/ # Web-specific utilities
│ ├── shared-node/ # Node.js-specific utilities
│ ├── sdk-core/ # Wire blockchain SDK core
+│ ├── sdk-outpost/ # Typed external-chain outpost SDK
│ ├── wallet-ext-sdk/ # Wallet extension client SDK
│ ├── wallet-browser-ext/ # Chrome extension wallet
│ ├── protoc-gen-solana/ # protoc plugin → Rust/Solana
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 0000000..8bb575b
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,116 @@
+# Releasing `@wireio/sdk-outpost`
+
+`@wireio/sdk-outpost` participates in the same repository-wide release as
+`@wireio/sdk-core`. Do not manually change its version or publish a workspace
+directory outside this process.
+
+## Current first-release state
+
+Ethereum and Solana producer packages `0.3.0` are the exact prerequisites for
+the first SDK release. Both are publicly installable and provide directly
+importable TypeScript libraries, ethers v6 factories, Anchor types, manifests,
+and runtime artifacts. The first `@wireio/sdk-outpost` release remains pending
+platform review. Its workspace version remains `0.0.0` until the existing
+repository-wide patch workflow bumps and publishes it.
+
+## Artifact prerequisites
+
+The package consumes exact runtime versions of:
+
+- `@wireio/outpost-ethereum-artifacts`, published from `wire-ethereum`;
+- `@wireio/outpost-solana-artifacts`, published from `wire-solana`.
+
+Publish a new producer package when its ABI/IDL or deployable contract/program
+binary changes. Publish `sdk-outpost` when its public behavior changes or a new
+ABI, IDL, normalized Ethereum runtime, or Solana program binary must be compiled
+into deployment verification. A same-code deployment respin, address change, or
+endpoint rotation does not require either package to be republished; emit a new
+immutable deployment profile for a respin and update the separate endpoint
+catalog for mutable endpoints.
+
+Before updating either dependency, verify its registry integrity/signature,
+source revision, artifact checksums, and immutable version. The producer repos
+are non-public, so their current npm releases do not carry public provenance.
+Keep both versions exact in `packages/sdk-outpost/package.json`. Update
+`pnpm-lock.yaml` with the repository-pinned pnpm version only after both exact
+versions resolve from npm, then run the repository install and test flow.
+
+## Release requirements
+
+- Use Node.js 24 and the repository-pinned pnpm version through Corepack.
+- Import ethers v6 factories, Anchor types, runtime artifacts, and manifests
+ directly from the exact producer packages; `sdk-outpost` must not regenerate
+ or duplicate them.
+- Confirm the package contains no secrets, RPC credentials, private keys,
+ deployment addresses, or mutable environment configuration.
+- Confirm deployment profiles are distributed through the authenticated
+ platform release channel; their schema and checksum-derived IDs are not
+ signatures.
+- Keep `repository.url` exactly equal to
+ `https://github.com/Wire-Network/wire-libraries-ts` for npm provenance.
+
+Local checkouts are verification environments, not publishing authorities. Run
+the checks there, but let the protected GitHub Actions workflow create the
+release.
+
+## Before merge
+
+From the repository root:
+
+```sh
+corepack pnpm install --no-frozen-lockfile --ignore-scripts
+corepack pnpm run lint
+corepack pnpm run test:ci
+corepack pnpm --dir packages/sdk-outpost run build
+```
+
+Package verification requires the publishable files to remain limited to the
+README and CJS/ESM build outputs. Raw producer packages and generated source
+trees must not be published by `sdk-outpost`.
+
+## First npm listing
+
+The first successful publish creates the npm package page. Release sequence:
+
+1. Confirm both exact `0.3.0` producer artifact versions are publicly
+ installable.
+2. Confirm the `wireio` organization exists on npm and the release owner can
+ publish public packages in that scope.
+3. Confirm npm two-factor authentication is enabled for the release owner.
+4. Confirm the GitHub repository secret `NPM_TOKEN` can publish to the `wireio`
+ organization under its required authentication policy.
+5. Confirm the GitHub `release` environment exists with required reviewers; the
+ environment approval is the second release gate.
+6. Merge the reviewed feature pull request into `master`.
+7. Dispatch **Prepare Release** with the intended bump, approve its checks, and
+ merge the generated version-bump pull request.
+8. Dispatch **Tag Release** and approve the `release` environment gate.
+
+The preparation gate keeps each package on its existing version track. A patch
+bump therefore makes the first SDK release `@wireio/sdk-outpost@0.0.1`, while
+`@wireio/sdk-core` advances by one patch on its independent track.
+The Tag Release gate must install the workspace, build and test every package,
+and publish with npm provenance.
+
+Do not create the first `sdk-outpost` version manually. A failed publish must be
+corrected in source and released as the next patch; published npm versions are
+immutable.
+
+## After the first publication
+
+Verify the listing and install path:
+
+```sh
+npm view @wireio/sdk-outpost version dist-tags repository --json
+npm install @wireio/sdk-outpost
+```
+
+Then configure npm trusted publishing for `tag-release.yaml`. After a trusted
+publish succeeds, remove the long-lived write token from the publish step and
+restrict token-based publishing in npm package settings. Keep `id-token: write`
+so npm can generate provenance.
+
+References:
+
+-
+-
diff --git a/etc/tsconfig/tsconfig.base.json b/etc/tsconfig/tsconfig.base.json
index 491de49..a79aea6 100755
--- a/etc/tsconfig/tsconfig.base.json
+++ b/etc/tsconfig/tsconfig.base.json
@@ -59,6 +59,12 @@
"@wireio/sdk-core/*": [
"./packages/sdk-core/src/*"
],
+ "@wireio/sdk-outpost": [
+ "./packages/sdk-outpost/src"
+ ],
+ "@wireio/sdk-outpost/*": [
+ "./packages/sdk-outpost/src/*"
+ ],
"@wireio/wallet-ext-sdk": [
"./packages/wallet-ext-sdk/src"
],
diff --git a/jest.config.ts b/jest.config.ts
index 0679654..69d99f0 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -6,6 +6,7 @@ const config: Config = {
"packages/shared-node",
"packages/shared-web",
"packages/sdk-core",
+ "packages/sdk-outpost",
"packages/wallet-browser-ext",
"packages/wallet-ext-sdk"
]
diff --git a/packages/sdk-core/README.md b/packages/sdk-core/README.md
index 483042f..1016b50 100644
--- a/packages/sdk-core/README.md
+++ b/packages/sdk-core/README.md
@@ -122,8 +122,23 @@ await reserves.pushMatchReserve({
matcher: "alice",
wireAmount: pending[0].requestedWireAmount
})
+
+// Several pending rows can be activated atomically in one signed transaction.
+await reserves.pushMatchReserves({
+ matches: pending.map(reserve => ({
+ chainCode: reserve.chainCode,
+ tokenCode: reserve.tokenCode,
+ reserveCode: reserve.reserveCode,
+ matcher: "alice",
+ wireAmount: reserve.requestedWireAmount
+ }))
+})
```
+`pushMatchReserves` preserves the supplied action order and rejects an empty
+match list. The Wire transaction is atomic: either every `matchreserve` action
+is accepted or none is applied.
+
## Reserve swaps
Reserve swap integrations compose three on-chain sources instead of carrying a
diff --git a/packages/sdk-core/src/contracts/sysio/reserv/Client.ts b/packages/sdk-core/src/contracts/sysio/reserv/Client.ts
index 8f000b9..a425e7f 100644
--- a/packages/sdk-core/src/contracts/sysio/reserv/Client.ts
+++ b/packages/sdk-core/src/contracts/sysio/reserv/Client.ts
@@ -14,7 +14,11 @@ import type * as SysioContracts from "../../../types/SysioContractTypes.js"
import type { ContractTableRowsOptions } from "../../Contract.js"
import { getSysioContract, type SysioContractClient } from "../Client.js"
-import { buildSwapQuoteAction, matchReserveActionData } from "./Actions.js"
+import {
+ buildMatchReserveAction,
+ buildSwapQuoteAction,
+ matchReserveActionData
+} from "./Actions.js"
import {
DEFAULT_RESERV_CONTRACT,
DEFAULT_RESERVE_QUERY_LIMIT
@@ -27,6 +31,7 @@ import {
import type {
ListReservesOptions,
PushMatchReserveOptions,
+ PushMatchReservesOptions,
ReserveClientOptions,
ReserveIdentity,
ReserveQuoteOptions,
@@ -34,6 +39,9 @@ import type {
ReserveRewards
} from "./Types.js"
+/** Push-transaction response returned by the configured API client. */
+type APIClientPushTransactionResponse = ReturnType
+
function enumValue>(
enumType: T,
value: number | keyof T
@@ -227,8 +235,7 @@ export class ReserveClient {
limit: Number.MAX_SAFE_INTEGER
}),
row = rows.find(
- candidate =>
- reserveRowSlugValue(candidate.reserve_code) === reserveCode
+ candidate => reserveRowSlugValue(candidate.reserve_code) === reserveCode
)
return row ? normalizeReserveRow(row) : null
@@ -256,7 +263,7 @@ export class ReserveClient {
async pushMatchReserve(
options: PushMatchReserveOptions,
pushOptions: TransactionExtraOptions = options.pushOptions || {}
- ): Promise>> {
+ ): APIClientPushTransactionResponse {
return this.contractClient.actions.matchreserve.invoke(
matchReserveActionData(options),
{
@@ -271,6 +278,21 @@ export class ReserveClient {
)
}
+ /** Builds and pushes one signed Wire transaction that activates several pending reserves atomically. */
+ async pushMatchReserves(
+ options: PushMatchReservesOptions,
+ pushOptions: TransactionExtraOptions = options.pushOptions || {}
+ ): APIClientPushTransactionResponse {
+ if (options.matches.length === 0) {
+ throw new Error("At least one reserve match is required.")
+ }
+
+ return this.client.pushTransaction(
+ options.matches.map(buildMatchReserveAction),
+ pushOptions
+ )
+ }
+
/** Reads the current on-chain quote for one reserve route. */
async getSwapQuote(options: ReserveQuoteOptions): Promise {
const response = await this.sendReadOnlyAction(
diff --git a/packages/sdk-core/src/contracts/sysio/reserv/Types.ts b/packages/sdk-core/src/contracts/sysio/reserv/Types.ts
index cf0a434..ed5b265 100644
--- a/packages/sdk-core/src/contracts/sysio/reserv/Types.ts
+++ b/packages/sdk-core/src/contracts/sysio/reserv/Types.ts
@@ -59,6 +59,14 @@ export interface PushMatchReserveOptions extends MatchReserveOptions {
pushOptions?: TransactionExtraOptions
}
+/** Options for atomically matching several pending reserves on Wire. */
+export interface PushMatchReservesOptions {
+ /** Ordered reserve matches included in one Wire transaction. */
+ matches: readonly MatchReserveOptions[]
+ /** Optional push behavior such as finality waiting. */
+ pushOptions?: TransactionExtraOptions
+}
+
/** Options for a read-only reserve-to-reserve quote. */
export interface ReserveQuoteOptions {
/** Source reserve identity. */
diff --git a/packages/sdk-core/tests/contracts/sysio/reserv/Client.test.ts b/packages/sdk-core/tests/contracts/sysio/reserv/Client.test.ts
index 359a1c2..a52a606 100644
--- a/packages/sdk-core/tests/contracts/sysio/reserv/Client.test.ts
+++ b/packages/sdk-core/tests/contracts/sysio/reserv/Client.test.ts
@@ -163,6 +163,51 @@ describe("ReserveClient", () => {
expect(action.authorization.map(String)).toEqual(["alice@active"])
})
+ test("pushes ordered reserve matches in one Wire transaction", async () => {
+ const { client, pushTransaction } = clientFixture()
+
+ await expect(
+ client.pushMatchReserves({
+ matches: [
+ {
+ chainCode: "ETHEREUM",
+ tokenCode: "ETH",
+ reserveCode: "PRIVATE",
+ matcher: "alice",
+ wireAmount: "2500000000"
+ },
+ {
+ chainCode: "SOLANA",
+ tokenCode: "USDC",
+ reserveCode: "PRIVATE",
+ matcher: "alice",
+ wireAmount: "2500000000"
+ }
+ ]
+ })
+ ).resolves.toEqual({ transaction_id: "reserve-trx" })
+
+ const [actions] = pushTransaction.mock.calls[0]
+ expect(actions).toHaveLength(2)
+ expect(actions.map(action => action.name.toString())).toEqual([
+ "matchreserve",
+ "matchreserve"
+ ])
+ expect(actions.map(action => action.authorization.map(String))).toEqual([
+ ["alice@active"],
+ ["alice@active"]
+ ])
+ })
+
+ test("rejects an empty atomic reserve match", async () => {
+ const { client, pushTransaction } = clientFixture()
+
+ await expect(client.pushMatchReserves({ matches: [] })).rejects.toThrow(
+ "At least one reserve match is required."
+ )
+ expect(pushTransaction).not.toHaveBeenCalled()
+ })
+
test("decodes read-only swapquote and rewards values", async () => {
const { client } = clientFixture()
diff --git a/packages/sdk-outpost/README.md b/packages/sdk-outpost/README.md
new file mode 100644
index 0000000..810f0ca
--- /dev/null
+++ b/packages/sdk-outpost/README.md
@@ -0,0 +1,309 @@
+# `@wireio/sdk-outpost`
+
+Strictly typed access to the Ethereum contracts and Solana programs deployed
+alongside a Wire chain.
+
+`@wireio/sdk-core` owns Wire-chain identity, signing, and `sysio.*` workflows.
+This package owns verified external-chain clients. Contract ABIs are published
+by `wire-ethereum`, the Solana IDL is published by `wire-solana`, and immutable
+deployment profiles remain caller-supplied.
+
+The current source pins Ethereum and Solana artifacts `0.3.0`, directly
+importable TypeScript libraries assembled from the same verified deployment
+handoff. Their published manifests identify the exact producer commits, runtime
+artifacts, and ethers v6/Anchor bindings used by the SDK. The workspace version
+remains `0.0.0` until the repository release workflow performs its patch bump.
+
+## Install after the first SDK release
+
+```sh
+npm install @wireio/sdk-outpost
+```
+
+Before installing, verify that `@wireio/sdk-outpost` resolves through `npm view`.
+The SDK consumes exact npm versions of
+[`@wireio/outpost-ethereum-artifacts`](https://www.npmjs.com/package/@wireio/outpost-ethereum-artifacts)
+and
+[`@wireio/outpost-solana-artifacts`](https://www.npmjs.com/package/@wireio/outpost-solana-artifacts);
+do not replace them with committed machine-local links.
+
+Node.js 22 or newer and ethers v6 are supported. The package publishes CommonJS
+and ES module entrypoints with TypeScript declarations.
+
+For local `wire-platform` development, the repository pnpm hook links available
+sibling artifact-package outputs automatically. Run `pnpm install --lockfile=false`
+after building those outputs; otherwise the exact registry versions remain in use.
+
+## Supported surfaces
+
+| Family | Generated clients and workflows |
+| -------- | ------------------------------------------------------------------------- |
+| Ethereum | `OPP`, `OPPInbound`, `OperatorRegistry`, `ReserveManager`, reserve lifecycle and swaps |
+| Solana | `liqsol_core`, configured reserve lifecycle, native SOL and classic SPL reserve swaps |
+
+Client creation verifies all four boundaries before returning:
+
+- the supplied ABI/IDL digests match the producer packages compiled into this
+ SDK release;
+- the provider is connected to the expected external chain;
+- every Ethereum proxy resolves through its EIP-1967 implementation slot to the
+ configured implementation address, exact implementation code hash, and the
+ producer package's normalized runtime template;
+- every Solana program resolves through the upgradeable loader to the configured
+ ProgramData account, exact ProgramData hash, and producer program binary.
+
+`OutpostClient.create` is the single public construction facade. Its family
+discriminator preserves the precise `EthereumOutpostClient` or
+`SolanaOutpostClient` instance type without publishing separate chain-specific
+factory entrypoints or internal module paths.
+
+These checks prove deployment compatibility, not end-to-end feature readiness.
+Applications must still gate swaps, staking, settlement, retry, funding, and
+underwriting using platform capability evidence.
+
+## Deployment profiles
+
+The SDK does not contain a mutable network or endpoint catalog. Resolve the
+selected Wire network group in the application, load its immutable deployment
+profile from the platform release/deployment pipeline, and validate that
+untrusted input with `parseOutpostDeploymentProfile`.
+
+Schema validation and the checksum-derived profile ID do not authenticate a
+profile. Load profiles only through the platform's authenticated release
+channel; deployment-profile signing and distribution remain release-pipeline
+responsibilities rather than SDK-owned mutable network data.
+
+A deployment profile carries:
+
+- the full parent Wire chain ID;
+- one deployment checksum and deployment-checksum-derived profile ID;
+- the Ethereum chain ID, proxy addresses, implementation addresses, ABI hashes,
+ and exact live implementation code hashes;
+- the Solana genesis hash, program and ProgramData addresses, IDL hash, and
+ exact live ProgramData hash.
+
+RPC URLs, private keys, wallet state, and mutable capability results are not SDK
+data. Keep mutable RPC/explorer endpoints in a separate catalog that points to a
+deployment-profile ID. A cluster respin with the same deployable code creates a
+new profile without requiring a producer-artifact or SDK release.
+
+| Change | Producer artifact release | `sdk-outpost` release | Deployment profile |
+| ------------------------------------------------------- | ------------------------- | --------------------- | ----------------------------------------- |
+| Same-code chain respin | No | No | New |
+| Contract/program binary change with unchanged ABI/IDL | Yes | Yes | New |
+| ABI or IDL change | Yes | Yes | New |
+| Asset/reserve onboarding without code/interface changes | No | No | Update operational configuration/evidence |
+| RPC or explorer rotation | No | No | Update endpoint catalog only |
+
+## Artifact suite selection
+
+The SDK keeps an internal, compile-time registry of supported Ethereum and
+Solana producer package pairs. Client creation selects compatible bindings from
+the deployment profile's ABI or IDL digests, then verifies the selected
+candidate against the exact live Ethereum runtime or Solana ProgramData before
+returning a client.
+
+This registry is not a network, endpoint, or environment catalog. It contains no
+RPC URLs, does not download code, and is not keyed by names such as sandbox or
+devnet. A new deployment profile that uses an already-registered artifact suite
+works without an SDK release. A deployable code or interface change requires a
+producer artifact release, a corresponding internal suite entry, and an SDK
+release; consumers continue to use the same `OutpostClient.create` facade.
+
+## Usage
+
+Validate caller-owned deployment data and provide the matching external-chain
+provider:
+
+```ts
+import { JsonRpcProvider } from "ethers"
+
+import {
+ EthereumContractName,
+ OutpostChainFamily,
+ OutpostClient,
+ parseOutpostDeploymentProfile
+} from "@wireio/sdk-outpost"
+
+const profile = parseOutpostDeploymentProfile(platformRelease.outpostProfile)
+const ethereum = await OutpostClient.create({
+ family: OutpostChainFamily.ethereum,
+ options: {
+ profile,
+ connection: new JsonRpcProvider(ethereumRpcUrl)
+ }
+})
+const reserves = ethereum.contract(EthereumContractName.ReserveManager)
+```
+
+Wallet-connected clients expose verified reserve-swap workflows without a
+separate deployment address book:
+
+```ts
+const submission = await ethereum.swaps.requestNative({
+ sourceTokenCode,
+ sourceReserveCode,
+ sourceAmount,
+ targetChainCode,
+ targetTokenCode,
+ targetReserveCode,
+ targetRecipient,
+ targetAmount,
+ targetToleranceBps
+})
+
+// Correlate this protocol id with sysio.uwrit; the transaction hash alone is
+// only source-chain submission evidence.
+console.log(submission.sourceRequestId)
+```
+
+Ethereum also exposes `requestErc20WithApproval`, `nativeBalance`, and
+`erc20Balance`. Solana exposes `requestNative`, `requestSpl`, `nativeBalance`,
+and `splBalance` through the same `client.swaps` ownership boundary.
+
+## Ethereum node owners
+
+The verified Ethereum client exposes `nodeOwners` for the external half of the
+node-owner flow. It resolves the canonical WireNodes ERC-1155 contract from
+BAR, reads owned tiers, obtains approval only when needed, and submits
+`BAR.commitNode`. Wire account authority parsing reuses `@wireio/sdk-core`; the
+SDK validates that the uncompressed depositor key belongs to the EVM signer.
+BAR is an optional deployment capability: schema-v1 profiles without a BAR
+identity continue to support the existing reserve and swap clients, while
+accessing `nodeOwners` fails closed with an explicit availability error.
+
+```ts
+const slots = await ethereum.nodeOwners.ownedSlots(ownerAddress)
+const submission = await ethereum.nodeOwners.commit({
+ tokenId: slots[0].tokenId,
+ wireAccountName: Name.from(wireAccountName),
+ wirePublicKey: PublicKey.from(wirePublicKey),
+ depositorPublicKey
+})
+```
+
+This surface does not mint test tokens, guess a fallback contract, create the
+Wire account directly, or infer protocol completion from the EVM receipt. Hub
+must keep the action disabled unless deployment and capability evidence both
+advertise the complete node-owner flow, then follow the resulting Wire-side
+registration state separately.
+
+## Reserve lifecycle
+
+Wallet-connected clients expose the external half of the post-bootstrap
+reserve lifecycle. The external create escrows reserve capital and emits the
+attestation that creates a pending `sysio.reserv` row. A signed
+`@wireio/sdk-core` `ReserveClient` then supplies the exact requested WIRE amount
+and activates that row.
+
+```ts
+const ethereumSubmission = await ethereum.reserves.createNative({
+ tokenCode,
+ reserveCode,
+ externalTokenAmount,
+ requestedWireAmount,
+ connectorWeightBps: 5_000,
+ name: "Private ETH reserve",
+ description: "",
+ isPrivate: true,
+ creatorPubKey
+})
+
+const configuredTokens = await solana.reserves.getConfiguredTokens()
+const splToken = configuredTokens.find(token => !token.isNative)
+if (splToken == null) throw new Error("No configured SPL reserve token.")
+
+const solanaSubmission = await solana.reserves.create({
+ tokenCode: splToken.tokenCode,
+ reserveCode,
+ externalTokenAmount: splAmount,
+ requestedWireAmount,
+ connectorWeightBps: 5_000,
+ name: "Private SPL reserve",
+ description: "",
+ isPrivate: true,
+ mint: splToken.mint
+})
+```
+
+Ethereum supports native creation, ERC-20 approval or permit creation, pending
+cancellation, and local reserve reads. Solana supports deployment-configured
+token discovery, instruction assembly, creation, pending cancellation, address
+derivation, and local reserve reads. `cancel` is valid only while creation is
+pending and drives the protocol refund path.
+
+The all-zero mint returned for a configured native SOL route is protocol
+metadata, not an Anchor account. The current `create_reserve` account context
+still requires a real placeholder SPL mint and the creator's token account for
+native SOL creation. Consumers that have not provisioned those accounts should
+select a configured non-native SPL route, as in the example above.
+
+Private is a routing constraint, not access control or confidentiality. Private
+reserves cannot use WIRE as a swap endpoint; when either external route leg is
+private, Wire requires both active reserves to have the same non-empty owner.
+The current protocol exposes no creator withdrawal, close, or redemption after
+activation. This SDK intentionally does not invent an active-reserve exit API.
+
+Solana uses the same facade and returns the precise Anchor program type at the
+runtime program address:
+
+```ts
+import {
+ OutpostChainFamily,
+ OutpostClient,
+ SolanaProgramName
+} from "@wireio/sdk-outpost"
+
+const solana = await OutpostClient.create({
+ family: OutpostChainFamily.solana,
+ options: { profile, provider: anchorProvider }
+})
+const liqsol = solana.program(SolanaProgramName.liqsolCore)
+```
+
+The producer-owned `LiqsolCore` type preserves the IDL's literal account namespace,
+including `Program["account"]["outpostConfig"]` and
+`Program["account"]["reserve"]`. Import it from
+`@wireio/outpost-solana-artifacts`; never widen the IDL to the base `Idl` type.
+
+## Artifact ownership
+
+`@wireio/outpost-ethereum-artifacts` and `@wireio/outpost-solana-artifacts` are
+normal runtime dependencies. Their producers verify and publish the ABIs, IDL,
+runtime bytes, manifests, ethers v6 factories, and Anchor types together from a
+checksummed deployment artifact handoff. `sdk-outpost` imports those published
+libraries directly, registers their generated bindings as one internal artifact
+suite, and uses their manifests for live deployment verification; it does not
+download handoffs or regenerate chain code.
+
+## Consumer boundaries
+
+- Use this package for typed external `ReserveManager`, `OperatorRegistry`,
+ `OPP`, `OPPInbound`, `liqsol_core`, reserve lifecycle, and source reserve-swap
+ execution.
+- Use `@wireio/sdk-core` for Wire transaction construction, reserve and token
+ registries, underwriting state, and settlement correlation.
+- Recreate external clients whenever the selected deployment profile changes.
+- Combine SDK deployment verification with flow-specific capability gates before
+ enabling a product action.
+- Ethereum reserve-swap submissions estimate the live call and add 25% gas
+ headroom for nested OPP execution; unused gas is not charged.
+
+## Maintainer commands
+
+```sh
+pnpm --dir packages/sdk-outpost run build
+pnpm --dir packages/sdk-outpost run test
+```
+
+The artifact tests read the installed producer-package payloads, verify their
+published runtime checksums and required generated bindings, and assert that the
+internal registry accepts only the exact paired Ethereum and Solana suite.
+
+Release versions are managed by the monorepo-wide patch workflow. See the
+[repository release guide](https://github.com/Wire-Network/wire-libraries-ts/blob/master/RELEASING.md)
+for artifact prerequisites and the verification checklist.
+
+## License
+
+FSL-1.1-Apache-2.0
diff --git a/packages/sdk-outpost/jest.config.ts b/packages/sdk-outpost/jest.config.ts
new file mode 100644
index 0000000..b247257
--- /dev/null
+++ b/packages/sdk-outpost/jest.config.ts
@@ -0,0 +1,23 @@
+import type { Config } from "jest"
+
+const config: Config = {
+ displayName: "@wireio/sdk-outpost",
+ testEnvironment: "node",
+ roots: ["/tests"],
+ testMatch: ["**/*.test.ts"],
+ transform: {
+ "^.+\\.[tj]s$": [
+ "ts-jest",
+ {
+ tsconfig: "/tsconfig.cjs.jest.json"
+ }
+ ]
+ },
+ moduleNameMapper: {
+ "^@wireio/sdk-outpost$": "/src/index",
+ "^@wireio/sdk-outpost/(.*)$": "/src/$1",
+ "^(\\.\\.?/.*)\\.js$": "$1"
+ }
+}
+
+export default config
diff --git a/packages/sdk-outpost/package.json b/packages/sdk-outpost/package.json
new file mode 100644
index 0000000..c3e42bb
--- /dev/null
+++ b/packages/sdk-outpost/package.json
@@ -0,0 +1,58 @@
+{
+ "name": "@wireio/sdk-outpost",
+ "description": "Strictly typed Ethereum and Solana clients for Wire outposts.",
+ "version": "0.0.0",
+ "private": false,
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/Wire-Network/wire-libraries-ts",
+ "directory": "packages/sdk-outpost"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "homepage": "https://github.com/Wire-Network/wire-libraries-ts/tree/master/packages/sdk-outpost",
+ "bugs": {
+ "url": "https://github.com/Wire-Network/wire-libraries-ts/issues"
+ },
+ "keywords": ["wire", "ethereum", "solana", "outpost", "sdk"],
+ "engines": {
+ "node": ">=22"
+ },
+ "sideEffects": false,
+ "files": ["lib/cjs", "lib/esm", "README.md"],
+ "types": "lib/esm/index.d.ts",
+ "main": "lib/cjs/index.js",
+ "module": "lib/esm/index.js",
+ "exports": {
+ ".": {
+ "import": "./lib/esm/index.js",
+ "require": "./lib/cjs/index.js",
+ "types": "./lib/esm/index.d.ts"
+ }
+ },
+ "access": "public",
+ "license": "FSL-1.1-Apache-2.0",
+ "scripts": {
+ "compile": "tsc -b tsconfig.json",
+ "compile:watch": "tsc -b tsconfig.json -w",
+ "build": "pnpm run compile && pnpm run fix:hybrid:exports",
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
+ "fix:hybrid:exports": "node ../../scripts/fix-hybrid-output.mjs ."
+ },
+ "dependencies": {
+ "@coral-xyz/anchor": "^0.32.1",
+ "@solana/spl-token": "^0.3.11",
+ "@solana/web3.js": "^1.98.4",
+ "@wireio/outpost-ethereum-artifacts": "0.3.0",
+ "@wireio/outpost-solana-artifacts": "0.3.0",
+ "@wireio/sdk-core": "workspace:*",
+ "ethers": "^6.15.0",
+ "ts-pattern": "^5.9.0",
+ "zod": "^4.4.3"
+ },
+ "devDependencies": {
+ "rpc-websockets": "9.3.8",
+ "typescript": "6.0.2"
+ }
+}
diff --git a/packages/sdk-outpost/src/artifacts/Compatibility.ts b/packages/sdk-outpost/src/artifacts/Compatibility.ts
new file mode 100644
index 0000000..8b76b6a
--- /dev/null
+++ b/packages/sdk-outpost/src/artifacts/Compatibility.ts
@@ -0,0 +1,48 @@
+import {
+ EthereumContractName,
+ OutpostChainFamily,
+ SolanaProgramName,
+ type OutpostDeploymentProfile
+} from "../deployments/index.js"
+import { CurrentOutpostArtifactSuite } from "./Registry.js"
+import {
+ assertEthereumRuntimeSuiteCompatibility,
+ assertOutpostArtifactSuiteCompatibility,
+ assertSolanaProgramSuiteCompatibility
+} from "./SuiteCompatibility.js"
+
+/** Verify live Ethereum code against its source-owned runtime template. */
+export function assertEthereumRuntimeArtifactCompatibility(
+ contractName: EthereumContractName,
+ code: string
+): void {
+ assertEthereumRuntimeSuiteCompatibility(
+ contractName,
+ code,
+ CurrentOutpostArtifactSuite
+ )
+}
+
+/** Verify live Solana executable bytes against the source-owned program binary. */
+export function assertSolanaProgramArtifactCompatibility(
+ programName: SolanaProgramName,
+ programData: Uint8Array
+): void {
+ assertSolanaProgramSuiteCompatibility(
+ programName,
+ programData,
+ CurrentOutpostArtifactSuite
+ )
+}
+
+/** Verify that a deployment profile matches this SDK's source-owned interfaces. */
+export function assertOutpostArtifactCompatibility(
+ profile: OutpostDeploymentProfile,
+ family: OutpostChainFamily
+): void {
+ assertOutpostArtifactSuiteCompatibility(
+ profile,
+ family,
+ CurrentOutpostArtifactSuite
+ )
+}
diff --git a/packages/sdk-outpost/src/artifacts/Manifests.ts b/packages/sdk-outpost/src/artifacts/Manifests.ts
new file mode 100644
index 0000000..7254c38
--- /dev/null
+++ b/packages/sdk-outpost/src/artifacts/Manifests.ts
@@ -0,0 +1,7 @@
+import { CurrentOutpostArtifactSuite } from "./Registry.js"
+
+/** Exact producer manifests compiled into this SDK release. */
+export const OutpostArtifactManifests = {
+ ethereum: CurrentOutpostArtifactSuite.ethereum.manifest,
+ solana: CurrentOutpostArtifactSuite.solana.manifest
+} as const
diff --git a/packages/sdk-outpost/src/artifacts/Registry.ts b/packages/sdk-outpost/src/artifacts/Registry.ts
new file mode 100644
index 0000000..2ce4fef
--- /dev/null
+++ b/packages/sdk-outpost/src/artifacts/Registry.ts
@@ -0,0 +1,185 @@
+import {
+ BAR__factory,
+ EthereumOutpostArtifactManifest,
+ OPPInbound__factory,
+ OPP__factory,
+ OperatorRegistry__factory,
+ ReserveManager__factory
+} from "@wireio/outpost-ethereum-artifacts"
+import {
+ SolanaOutpostArtifactManifest,
+ liqsolCoreIdl
+} from "@wireio/outpost-solana-artifacts"
+import { match } from "ts-pattern"
+
+import {
+ EthereumContractName,
+ OutpostChainFamily,
+ SolanaProgramName,
+ type OutpostDeploymentProfile
+} from "../deployments/index.js"
+
+/** Generated ethers v6 factories keyed by deployment identity. */
+export interface EthereumOutpostArtifactFactories {
+ readonly [EthereumContractName.BAR]: typeof BAR__factory
+ readonly [EthereumContractName.OPP]: typeof OPP__factory
+ readonly [EthereumContractName.OPPInbound]: typeof OPPInbound__factory
+ readonly [EthereumContractName.OperatorRegistry]: typeof OperatorRegistry__factory
+ readonly [EthereumContractName.ReserveManager]: typeof ReserveManager__factory
+}
+
+/** Generated Anchor IDLs keyed by deployment identity. */
+export interface SolanaOutpostArtifactIdls {
+ readonly [SolanaProgramName.liqsolCore]: typeof liqsolCoreIdl
+}
+
+/** Ethereum producer bindings and manifest owned by one artifact release. */
+export interface EthereumOutpostArtifactSuite {
+ /** Exact producer manifest used for compatibility verification. */
+ readonly manifest: typeof EthereumOutpostArtifactManifest
+ /** Generated ethers v6 factories keyed by deployment identity. */
+ readonly factories: EthereumOutpostArtifactFactories
+}
+
+/** Solana producer bindings and manifest owned by one artifact release. */
+export interface SolanaOutpostArtifactSuite {
+ /** Exact producer manifest used for compatibility verification. */
+ readonly manifest: typeof SolanaOutpostArtifactManifest
+ /** Generated Anchor IDLs keyed by deployment identity. */
+ readonly idls: SolanaOutpostArtifactIdls
+}
+
+/** Paired producer bindings accepted by one SDK Outpost release. */
+export interface OutpostArtifactSuite {
+ /** Ethereum half of the release suite. */
+ readonly ethereum: EthereumOutpostArtifactSuite
+ /** Solana half of the release suite. */
+ readonly solana: SolanaOutpostArtifactSuite
+}
+
+/** One deployment-interface mismatch against a registered artifact suite. */
+export interface OutpostArtifactInterfaceMismatch {
+ /** Human-readable interface identity. */
+ readonly label: string
+ /** Digest recorded by the deployment profile. */
+ readonly actual: string
+ /** Digest published by the producer artifact package. */
+ readonly expected: string
+}
+
+/** Current producer package pair compiled into this SDK branch. */
+export const CurrentOutpostArtifactSuite: OutpostArtifactSuite = {
+ ethereum: {
+ manifest: EthereumOutpostArtifactManifest,
+ factories: {
+ [EthereumContractName.BAR]: BAR__factory,
+ [EthereumContractName.OPP]: OPP__factory,
+ [EthereumContractName.OPPInbound]: OPPInbound__factory,
+ [EthereumContractName.OperatorRegistry]: OperatorRegistry__factory,
+ [EthereumContractName.ReserveManager]: ReserveManager__factory
+ }
+ },
+ solana: {
+ manifest: SolanaOutpostArtifactManifest,
+ idls: {
+ [SolanaProgramName.liqsolCore]: liqsolCoreIdl
+ }
+ }
+}
+
+/** Artifact suites supported by this SDK release. */
+const RegisteredOutpostArtifactSuites: readonly OutpostArtifactSuite[] = [
+ CurrentOutpostArtifactSuite
+]
+
+/** Return Ethereum interface mismatches for one artifact suite. */
+function ethereumInterfaceMismatches(
+ profile: OutpostDeploymentProfile,
+ suite: OutpostArtifactSuite
+): OutpostArtifactInterfaceMismatch[] {
+ return Object.values(EthereumContractName).flatMap(contractName => {
+ const deployment = profile.ethereum.contracts[contractName]
+ if (deployment == null) return []
+ const expected = suite.ethereum.manifest.contracts[contractName].abiSha256
+ return deployment.abiSha256 === expected
+ ? []
+ : [
+ {
+ label: `Ethereum ${contractName} ABI`,
+ actual: deployment.abiSha256,
+ expected
+ }
+ ]
+ })
+}
+
+/** Return Solana interface mismatches for one artifact suite. */
+function solanaInterfaceMismatches(
+ profile: OutpostDeploymentProfile,
+ suite: OutpostArtifactSuite
+): OutpostArtifactInterfaceMismatch[] {
+ return Object.values(SolanaProgramName).flatMap(programName => {
+ const actual = profile.solana.programs[programName].idlSha256,
+ expected = suite.solana.manifest.programs[programName].idlSha256
+ return actual === expected
+ ? []
+ : [
+ {
+ label: `Solana ${programName} IDL`,
+ actual,
+ expected
+ }
+ ]
+ })
+}
+
+/** Return all profile interface mismatches for one family and suite. */
+export function outpostArtifactInterfaceMismatches(
+ profile: OutpostDeploymentProfile,
+ family: OutpostChainFamily,
+ suite: OutpostArtifactSuite
+): OutpostArtifactInterfaceMismatch[] {
+ return match(family)
+ .with(OutpostChainFamily.ethereum, () =>
+ ethereumInterfaceMismatches(profile, suite)
+ )
+ .with(OutpostChainFamily.solana, () =>
+ solanaInterfaceMismatches(profile, suite)
+ )
+ .exhaustive()
+}
+
+/** Internal registry for producer package suites compiled into this SDK. */
+export namespace OutpostArtifactRegistry {
+ /** Return suites whose paired generated interfaces match one profile. */
+ export function candidates(
+ profile: OutpostDeploymentProfile
+ ): readonly OutpostArtifactSuite[] {
+ return RegisteredOutpostArtifactSuites.filter(
+ suite =>
+ outpostArtifactInterfaceMismatches(
+ profile,
+ OutpostChainFamily.ethereum,
+ suite
+ ).length === 0 &&
+ outpostArtifactInterfaceMismatches(
+ profile,
+ OutpostChainFamily.solana,
+ suite
+ ).length === 0
+ )
+ }
+
+ /** Resolve paired generated bindings for a compatible deployment profile. */
+ export function resolve(
+ profile: OutpostDeploymentProfile
+ ): OutpostArtifactSuite {
+ const suite = candidates(profile)[0]
+ if (suite == null) {
+ throw new Error(
+ `No registered artifact suite matches deployment profile ${profile.id}`
+ )
+ }
+ return suite
+ }
+}
diff --git a/packages/sdk-outpost/src/artifacts/SuiteCompatibility.ts b/packages/sdk-outpost/src/artifacts/SuiteCompatibility.ts
new file mode 100644
index 0000000..25cbc21
--- /dev/null
+++ b/packages/sdk-outpost/src/artifacts/SuiteCompatibility.ts
@@ -0,0 +1,116 @@
+import { getBytes, sha256 as ethersSha256 } from "ethers"
+
+import {
+ EthereumContractName,
+ OutpostChainFamily,
+ SolanaProgramName,
+ type OutpostDeploymentProfile
+} from "../deployments/index.js"
+import {
+ outpostArtifactInterfaceMismatches,
+ type OutpostArtifactSuite
+} from "./Registry.js"
+
+const SolanaProgramDataMetadataByteLength = 45
+
+/** Deployment-specific byte range in one Ethereum runtime template. */
+interface EthereumRuntimeReference {
+ readonly start: number
+ readonly length: number
+}
+
+/** Return the SHA-256 digest for chain runtime bytes. */
+function sha256(value: Uint8Array): string {
+ return ethersSha256(value).slice(2)
+}
+
+/** Zero environment-specific ranges in live Ethereum runtime code. */
+function normalizeEthereumRuntimeCode(
+ code: string,
+ runtimeReferences: readonly EthereumRuntimeReference[]
+): Uint8Array {
+ const runtimeCode = Uint8Array.from(getBytes(code))
+ let previousReferenceEnd = 0
+
+ runtimeReferences.forEach(({ start, length }) => {
+ const referenceEnd = start + length
+ if (
+ !Number.isInteger(start) ||
+ !Number.isInteger(length) ||
+ length <= 0 ||
+ start < previousReferenceEnd ||
+ referenceEnd > runtimeCode.length
+ ) {
+ throw new Error("Ethereum artifact has invalid runtime references")
+ }
+ runtimeCode.fill(0, start, referenceEnd)
+ previousReferenceEnd = referenceEnd
+ })
+
+ return runtimeCode
+}
+
+/** Verify live Ethereum code against one producer runtime template. */
+export function assertEthereumRuntimeSuiteCompatibility(
+ contractName: EthereumContractName,
+ code: string,
+ suite: OutpostArtifactSuite
+): void {
+ const artifact = suite.ethereum.manifest.contracts[contractName],
+ runtimeReferences = [
+ ...artifact.runtimeLinkReferences,
+ ...artifact.runtimeImmutableReferences
+ ].sort((left, right) => left.start - right.start),
+ normalizedCode = normalizeEthereumRuntimeCode(code, runtimeReferences)
+
+ if (normalizedCode.length !== artifact.runtimeBytecodeLength) {
+ throw new Error(
+ `Ethereum ${contractName} artifact runtime length mismatch: expected ${artifact.runtimeBytecodeLength}, received ${normalizedCode.length}`
+ )
+ }
+ const digest = sha256(normalizedCode)
+ if (digest !== artifact.runtimeBytecodeSha256) {
+ throw new Error(
+ `Ethereum ${contractName} artifact runtime mismatch: expected ${artifact.runtimeBytecodeSha256}, received ${digest}`
+ )
+ }
+}
+
+/** Verify live Solana executable bytes against one producer program binary. */
+export function assertSolanaProgramSuiteCompatibility(
+ programName: SolanaProgramName,
+ programData: Uint8Array,
+ suite: OutpostArtifactSuite
+): void {
+ const artifact = suite.solana.manifest.programs[programName],
+ programBinaryEnd =
+ SolanaProgramDataMetadataByteLength + artifact.programBinaryLength
+
+ if (programData.length < programBinaryEnd) {
+ throw new Error(
+ `Solana ${programName} artifact program is truncated: expected ${artifact.programBinaryLength} executable bytes`
+ )
+ }
+ const digest = sha256(
+ programData.subarray(SolanaProgramDataMetadataByteLength, programBinaryEnd)
+ )
+ if (digest !== artifact.programBinarySha256) {
+ throw new Error(
+ `Solana ${programName} artifact program mismatch: expected ${artifact.programBinarySha256}, received ${digest}`
+ )
+ }
+}
+
+/** Verify profile interfaces against one producer artifact suite. */
+export function assertOutpostArtifactSuiteCompatibility(
+ profile: OutpostDeploymentProfile,
+ family: OutpostChainFamily,
+ suite: OutpostArtifactSuite
+): void {
+ const mismatch = outpostArtifactInterfaceMismatches(profile, family, suite)[0]
+ if (mismatch != null) {
+ throw new Error(
+ `${mismatch.label} interface mismatch: expected ${mismatch.expected}, received ${mismatch.actual}`
+ )
+ }
+}
diff --git a/packages/sdk-outpost/src/artifacts/index.ts b/packages/sdk-outpost/src/artifacts/index.ts
new file mode 100644
index 0000000..2f22aed
--- /dev/null
+++ b/packages/sdk-outpost/src/artifacts/index.ts
@@ -0,0 +1,2 @@
+export * from "./Compatibility.js"
+export * from "./Manifests.js"
diff --git a/packages/sdk-outpost/src/clients/OutpostClient.ts b/packages/sdk-outpost/src/clients/OutpostClient.ts
new file mode 100644
index 0000000..b777e18
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/OutpostClient.ts
@@ -0,0 +1,12 @@
+import { OutpostClientFactory } from "./OutpostClientFactory.js"
+import { OutpostClientFor, OutpostClientInput } from "./Types.js"
+
+/** Cross-chain facade for creating a verified, family-specific outpost client. */
+export namespace OutpostClient {
+ /** Create the precise client type selected by the request discriminator. */
+ export async function create(
+ input: T
+ ): Promise> {
+ return OutpostClientFactory.create(input)
+ }
+}
diff --git a/packages/sdk-outpost/src/clients/OutpostClientFactory.ts b/packages/sdk-outpost/src/clients/OutpostClientFactory.ts
new file mode 100644
index 0000000..3d70456
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/OutpostClientFactory.ts
@@ -0,0 +1,25 @@
+import { match } from "ts-pattern"
+
+import { OutpostChainFamily } from "../deployments/index.js"
+import { EthereumOutpostClient } from "./ethereum/EthereumOutpostClient.js"
+import { SolanaOutpostClient } from "./solana/SolanaOutpostClient.js"
+import { OutpostClientFor, OutpostClientInput } from "./Types.js"
+
+/** Internal factory that owns family-specific client construction. */
+export namespace OutpostClientFactory {
+ /** Create the precise client selected by an outpost-family request. */
+ export async function create(
+ input: T
+ ): Promise> {
+ const client = await match(input as OutpostClientInput)
+ .with({ family: OutpostChainFamily.ethereum }, ({ options }) =>
+ EthereumOutpostClient.create(options)
+ )
+ .with({ family: OutpostChainFamily.solana }, ({ options }) =>
+ SolanaOutpostClient.create(options)
+ )
+ .exhaustive()
+
+ return client as OutpostClientFor
+ }
+}
diff --git a/packages/sdk-outpost/src/clients/Types.ts b/packages/sdk-outpost/src/clients/Types.ts
new file mode 100644
index 0000000..68713c3
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/Types.ts
@@ -0,0 +1,43 @@
+import type { EthereumOutpostClientOptions } from "./ethereum/index.js"
+import type { EthereumOutpostClient as EthereumOutpostClientImplementation } from "./ethereum/EthereumOutpostClient.js"
+import type { SolanaOutpostClientOptions } from "./solana/index.js"
+import type { SolanaOutpostClient as SolanaOutpostClientImplementation } from "./solana/SolanaOutpostClient.js"
+import { OutpostChainFamily } from "../deployments/index.js"
+
+/** Verified Ethereum outpost client instance returned by `OutpostClient.create`. */
+export type EthereumOutpostClient = EthereumOutpostClientImplementation
+
+/** Verified Solana outpost client instance returned by `OutpostClient.create`. */
+export type SolanaOutpostClient = SolanaOutpostClientImplementation
+
+/** Request for an Ethereum outpost client. */
+export interface EthereumOutpostClientInput {
+ /** External-chain family discriminator. */
+ family: OutpostChainFamily.ethereum
+ /** Ethereum-specific client options. */
+ options: EthereumOutpostClientOptions
+}
+
+/** Request for a Solana outpost client. */
+export interface SolanaOutpostClientInput {
+ /** External-chain family discriminator. */
+ family: OutpostChainFamily.solana
+ /** Solana-specific client options. */
+ options: SolanaOutpostClientOptions
+}
+
+/** Typed request accepted by the cross-chain client facade. */
+export type OutpostClientInput =
+ | EthereumOutpostClientInput
+ | SolanaOutpostClientInput
+
+/** Concrete client types keyed by external-chain family. */
+export interface OutpostClientMap {
+ /** Ethereum client type. */
+ [OutpostChainFamily.ethereum]: EthereumOutpostClient
+ /** Solana client type. */
+ [OutpostChainFamily.solana]: SolanaOutpostClient
+}
+
+/** Client returned for a selected external-chain family. */
+export type OutpostClientFor = OutpostClientMap[T]
diff --git a/packages/sdk-outpost/src/clients/ethereum/Connection.ts b/packages/sdk-outpost/src/clients/ethereum/Connection.ts
new file mode 100644
index 0000000..1f7537e
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/ethereum/Connection.ts
@@ -0,0 +1,31 @@
+import type { Provider, Signer } from "ethers"
+
+/** Narrow an ethers v6 connection to a signer without relying on a v5 static guard. */
+export function isEthereumSigner(
+ connection: Provider | Signer
+): connection is Signer {
+ return (
+ "sendTransaction" in connection &&
+ typeof connection.sendTransaction === "function"
+ )
+}
+
+/** Return the provider owned directly or through a connected signer. */
+export function ethereumProvider(connection: Provider | Signer): Provider {
+ if (!isEthereumSigner(connection)) return connection
+ if (connection.provider == null) {
+ throw new Error("Ethereum signer must be connected to a provider")
+ }
+ return connection.provider
+}
+
+/** Require a signer before an Ethereum operation can open a wallet prompt. */
+export function assertEthereumSigner(
+ connection: Provider | Signer,
+ operation: string
+): Signer {
+ if (!isEthereumSigner(connection)) {
+ throw new Error(`${operation} requires a connected signer.`)
+ }
+ return connection
+}
diff --git a/packages/sdk-outpost/src/clients/ethereum/EthereumNodeOwnerClient.ts b/packages/sdk-outpost/src/clients/ethereum/EthereumNodeOwnerClient.ts
new file mode 100644
index 0000000..eaeaf19
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/ethereum/EthereumNodeOwnerClient.ts
@@ -0,0 +1,256 @@
+import type { BAR } from "@wireio/outpost-ethereum-artifacts"
+import { IERC1155__factory } from "@wireio/outpost-ethereum-artifacts"
+import { KeyType, type Name, type PublicKey } from "@wireio/sdk-core"
+import {
+ ZeroAddress,
+ computeAddress,
+ getAddress,
+ getBigInt,
+ getBytes,
+ hexlify,
+ type BigNumberish,
+ type BytesLike,
+ type EventLog,
+ type Log,
+ type Provider,
+ type Signer
+} from "ethers"
+import { match } from "ts-pattern"
+
+import { assertEthereumSigner } from "./Connection.js"
+
+const ConfirmationCount = 1,
+ NodeCommittedEventName = "NodeCommitted",
+ UncompressedPublicKeyByteLength = 65,
+ UncompressedPublicKeyPrefix = 4
+
+/** BAR WireKey numeric variants accepted for node-owner account authority. */
+enum NodeOwnerWireKeyType {
+ K1 = 1,
+ R1 = 2,
+ EM = 4,
+ ED = 5
+}
+
+/** WireNodes ERC-1155 token ids accepted by BAR as node-owner tiers. */
+export enum EthereumNodeOwnerTier {
+ T1 = 1,
+ T2 = 2,
+ T3 = 3
+}
+
+const DefaultNodeOwnerTiers = [
+ EthereumNodeOwnerTier.T1,
+ EthereumNodeOwnerTier.T2,
+ EthereumNodeOwnerTier.T3
+] as const
+
+/** One owned WireNodes tier returned from the canonical ERC-1155 contract. */
+export interface EthereumNodeOwnerSlotBalance {
+ /** WireNodes token id and node-owner tier. */
+ tokenId: EthereumNodeOwnerTier
+ /** Number of units held by the queried owner. */
+ balance: bigint
+ /** Canonical WireNodes contract configured by BAR. */
+ tokenContractAddress: string
+}
+
+/** Inputs required to escrow a WireNodes unit and register its owner. */
+export interface EthereumNodeOwnerCommitRequest {
+ /** WireNodes token id and node-owner tier to commit. */
+ tokenId: EthereumNodeOwnerTier
+ /** Canonical Wire account name to create or register. */
+ wireAccountName: Name
+ /** Wire account owner/active authority. */
+ wirePublicKey: PublicKey
+ /** Uncompressed SEC1 secp256k1 public key belonging to the EVM signer. */
+ depositorPublicKey: BytesLike
+}
+
+/** Canonical `NodeCommitted` fields emitted by BAR. */
+export interface EthereumNodeCommittedEvent {
+ /** EVM owner that committed the token. */
+ owner: string
+ /** WireNodes token id and node-owner tier. */
+ tokenId: EthereumNodeOwnerTier
+ /** Canonical WireNodes contract from which BAR pulled custody. */
+ tokenContractAddress: string
+ /** Wire account submitted for registration. */
+ wireAccountName: string
+}
+
+/** Confirmed node-owner registration submission. */
+export interface EthereumNodeOwnerCommitSubmission {
+ /** BAR commit transaction hash. */
+ transactionId: string
+ /** ERC-1155 approval transaction hash when approval was required. */
+ approvalTransactionId?: string
+ /** Confirmed BAR event proving the submitted registration. */
+ committed: EthereumNodeCommittedEvent
+}
+
+/** Node-owner slot reads, approval, and registration for one verified outpost. */
+export class EthereumNodeOwnerClient {
+ /** Bind node-owner operations to a generated BAR contract. */
+ constructor(
+ private readonly bar: BAR,
+ private readonly connection: Provider | Signer
+ ) {}
+
+ /** Resolve the governance-configured canonical WireNodes contract. */
+ async canonicalTokenContractAddress(): Promise {
+ const address = getAddress(await this.bar.wireNodesContract())
+ if (address === ZeroAddress) {
+ throw new Error("BAR has no canonical WireNodes contract configured.")
+ }
+ return address
+ }
+
+ /** Return non-zero WireNodes balances for the requested owner and tiers. */
+ async ownedSlots(
+ owner: string,
+ tokenIds: readonly EthereumNodeOwnerTier[] = DefaultNodeOwnerTiers
+ ): Promise {
+ const normalizedOwner = getAddress(owner),
+ tokenContractAddress = await this.canonicalTokenContractAddress(),
+ token = IERC1155__factory.connect(tokenContractAddress, this.connection),
+ balances = await Promise.all(
+ tokenIds.map(async tokenId => ({
+ tokenId,
+ balance: getBigInt(await token.balanceOf(normalizedOwner, tokenId)),
+ tokenContractAddress
+ }))
+ )
+
+ return balances.filter(({ balance }) => balance > 0n)
+ }
+
+ /** Approve BAR when needed, commit one WireNodes unit, and return its event. */
+ async commit(
+ request: EthereumNodeOwnerCommitRequest
+ ): Promise {
+ const signer = this.assertSigner(),
+ owner = getAddress(await signer.getAddress()),
+ depositorPublicKey = this.assertDepositorPublicKey(
+ request.depositorPublicKey,
+ owner
+ ),
+ wireAccountName = this.canonicalAccountName(
+ request.wireAccountName.toString()
+ ),
+ tokenContractAddress = await this.canonicalTokenContractAddress(),
+ token = IERC1155__factory.connect(tokenContractAddress, signer),
+ barAddress = await this.bar.getAddress(),
+ approved = await token.isApprovedForAll(owner, barAddress)
+
+ let approvalTransactionId: string | undefined
+ if (!approved) {
+ const approval = await token.setApprovalForAll(barAddress, true)
+ approvalTransactionId = approval.hash
+ await approval.wait(ConfirmationCount)
+ }
+
+ const transaction = await this.bar.commitNode(
+ request.tokenId,
+ wireAccountName,
+ this.wireKey(request.wirePublicKey),
+ depositorPublicKey
+ ),
+ receipt = await transaction.wait(ConfirmationCount)
+
+ return {
+ transactionId: transaction.hash,
+ approvalTransactionId,
+ committed: EthereumNodeOwnerClient.committedEvent(receipt?.logs)
+ }
+ }
+
+ /** Normalize the canonical `NodeCommitted` event from a BAR receipt. */
+ static committedEvent(
+ events: readonly (EventLog | Log)[] | undefined
+ ): EthereumNodeCommittedEvent {
+ const event = events?.find(
+ candidate =>
+ "eventName" in candidate &&
+ candidate.eventName === NodeCommittedEventName
+ ),
+ arguments_ = event != null && "args" in event ? event.args : undefined,
+ owner = arguments_?.[0],
+ tokenId = arguments_?.[1],
+ tokenContractAddress = arguments_?.[2],
+ wireAccountName = arguments_?.[3]
+
+ if (
+ owner == null ||
+ tokenId == null ||
+ tokenContractAddress == null ||
+ wireAccountName == null
+ ) {
+ throw new Error("Confirmed BAR transaction did not emit NodeCommitted.")
+ }
+ return {
+ owner: getAddress(owner),
+ tokenId: EthereumNodeOwnerClient.nodeOwnerTier(tokenId),
+ tokenContractAddress: getAddress(tokenContractAddress),
+ wireAccountName
+ }
+ }
+
+ /** Require a connected EVM signer for node-owner writes. */
+ private assertSigner(): Signer {
+ return assertEthereumSigner(this.connection, "Ethereum node-owner commit")
+ }
+
+ /** Validate the depositor key shape and its relationship to the signer. */
+ private assertDepositorPublicKey(
+ value: BytesLike,
+ owner: string
+ ): Uint8Array {
+ const publicKey = getBytes(value)
+ if (
+ publicKey.length !== UncompressedPublicKeyByteLength ||
+ publicKey[0] !== UncompressedPublicKeyPrefix
+ ) {
+ throw new Error(
+ "depositorPublicKey must be a 65-byte uncompressed SEC1 key."
+ )
+ }
+ if (getAddress(computeAddress(hexlify(publicKey))) !== owner) {
+ throw new Error("depositorPublicKey does not belong to the EVM signer.")
+ }
+ return publicKey
+ }
+
+ /** Validate and canonicalize the non-empty Wire account name. */
+ private canonicalAccountName(value: string): string {
+ if (value.length === 0) {
+ throw new Error("wireAccountName must not be empty.")
+ }
+ return value
+ }
+
+ /** Convert an sdk-core public key into BAR's generated WireKey structure. */
+ private wireKey(publicKey: PublicKey) {
+ const keyType = match(publicKey.type)
+ .with(KeyType.K1, () => NodeOwnerWireKeyType.K1)
+ .with(KeyType.R1, () => NodeOwnerWireKeyType.R1)
+ .with(KeyType.EM, () => NodeOwnerWireKeyType.EM)
+ .with(KeyType.ED, () => NodeOwnerWireKeyType.ED)
+ .otherwise(type => {
+ throw new Error(`${type} is not a node-owner authority key type.`)
+ })
+
+ return { keyType, key: publicKey.data.array }
+ }
+
+ /** Normalize one emitted token id to BAR's supported node-owner tier. */
+ private static nodeOwnerTier(value: BigNumberish): EthereumNodeOwnerTier {
+ return match(Number(value.toString()))
+ .with(EthereumNodeOwnerTier.T1, () => EthereumNodeOwnerTier.T1)
+ .with(EthereumNodeOwnerTier.T2, () => EthereumNodeOwnerTier.T2)
+ .with(EthereumNodeOwnerTier.T3, () => EthereumNodeOwnerTier.T3)
+ .otherwise(tokenId => {
+ throw new Error(`NodeCommitted emitted unsupported tier ${tokenId}.`)
+ })
+ }
+}
diff --git a/packages/sdk-outpost/src/clients/ethereum/EthereumOutpostClient.ts b/packages/sdk-outpost/src/clients/ethereum/EthereumOutpostClient.ts
new file mode 100644
index 0000000..418f421
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/ethereum/EthereumOutpostClient.ts
@@ -0,0 +1,132 @@
+import type { Provider } from "ethers"
+import { match } from "ts-pattern"
+
+import {
+ OutpostArtifactRegistry,
+ type OutpostArtifactSuite
+} from "../../artifacts/Registry.js"
+import {
+ EthereumContractName,
+ OutpostChainFamily
+} from "../../deployments/index.js"
+import { OutpostDeploymentVerifier } from "../../verification/index.js"
+import { ethereumProvider } from "./Connection.js"
+import { EthereumContractMap, EthereumOutpostClientOptions } from "./Types.js"
+import { EthereumReserveClient } from "./EthereumReserveClient.js"
+import { EthereumReserveSwapClient } from "./EthereumReserveSwapClient.js"
+import { EthereumNodeOwnerClient } from "./EthereumNodeOwnerClient.js"
+
+/** Strictly typed access to one verified Ethereum outpost deployment. */
+export class EthereumOutpostClient {
+ /** Create the Ethereum backend for the package-level outpost client facade. */
+ static async create(
+ options: EthereumOutpostClientOptions
+ ): Promise {
+ const { connection, profile } = options,
+ provider = ethereumProvider(connection)
+
+ await OutpostDeploymentVerifier.verify({
+ family: OutpostChainFamily.ethereum,
+ profile,
+ provider
+ })
+ return new EthereumOutpostClient(
+ options,
+ provider,
+ OutpostArtifactRegistry.resolve(profile)
+ )
+ }
+
+ private constructor(
+ private readonly options: EthereumOutpostClientOptions,
+ /** Provider verified against the configured Ethereum chain. */
+ readonly provider: Provider,
+ private readonly artifactSuite: OutpostArtifactSuite
+ ) {
+ this.reserves = new EthereumReserveClient(
+ this.contract(EthereumContractName.ReserveManager),
+ options.connection
+ )
+ this.swaps = new EthereumReserveSwapClient(
+ this.contract(EthereumContractName.ReserveManager),
+ options.connection
+ )
+ if (options.profile.ethereum.contracts[EthereumContractName.BAR] != null) {
+ this.nodeOwnerClient = new EthereumNodeOwnerClient(
+ this.contract(EthereumContractName.BAR),
+ options.connection
+ )
+ }
+ }
+
+ /** Reserve creation, cancellation, and reads for this verified outpost. */
+ readonly reserves: EthereumReserveClient
+
+ /** Reserve-swap writes and balance reads for this verified outpost. */
+ readonly swaps: EthereumReserveSwapClient
+
+ private readonly nodeOwnerClient?: EthereumNodeOwnerClient
+
+ /** Node-owner slot reads, approvals, and BAR registration when deployed. */
+ get nodeOwners(): EthereumNodeOwnerClient {
+ if (this.nodeOwnerClient == null) {
+ throw new Error(
+ "Ethereum node owners are unavailable because this deployment profile has no BAR identity."
+ )
+ }
+ return this.nodeOwnerClient
+ }
+
+ /** Deployment profile used to verify and connect this client. */
+ get profile(): EthereumOutpostClientOptions["profile"] {
+ return this.options.profile
+ }
+
+ /** Connect a generated contract client by its typed deployment name. */
+ contract(name: T): EthereumContractMap[T] {
+ const { connection, profile } = this.options,
+ factories = this.artifactSuite.ethereum.factories,
+ contract = match(name as EthereumContractName)
+ .with(EthereumContractName.BAR, () => {
+ const bar = profile.ethereum.contracts[EthereumContractName.BAR]
+ if (bar == null) {
+ throw new Error(
+ "Ethereum node owners are unavailable because this deployment profile has no BAR identity."
+ )
+ }
+ return factories[EthereumContractName.BAR].connect(
+ bar.address,
+ connection
+ )
+ })
+ .with(EthereumContractName.OPP, () =>
+ factories[EthereumContractName.OPP].connect(
+ profile.ethereum.contracts[EthereumContractName.OPP].address,
+ connection
+ )
+ )
+ .with(EthereumContractName.OPPInbound, () =>
+ factories[EthereumContractName.OPPInbound].connect(
+ profile.ethereum.contracts[EthereumContractName.OPPInbound].address,
+ connection
+ )
+ )
+ .with(EthereumContractName.OperatorRegistry, () =>
+ factories[EthereumContractName.OperatorRegistry].connect(
+ profile.ethereum.contracts[EthereumContractName.OperatorRegistry]
+ .address,
+ connection
+ )
+ )
+ .with(EthereumContractName.ReserveManager, () =>
+ factories[EthereumContractName.ReserveManager].connect(
+ profile.ethereum.contracts[EthereumContractName.ReserveManager]
+ .address,
+ connection
+ )
+ )
+ .exhaustive()
+
+ return contract as EthereumContractMap[T]
+ }
+}
diff --git a/packages/sdk-outpost/src/clients/ethereum/EthereumReserveClient.ts b/packages/sdk-outpost/src/clients/ethereum/EthereumReserveClient.ts
new file mode 100644
index 0000000..32622ee
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/ethereum/EthereumReserveClient.ts
@@ -0,0 +1,223 @@
+import type {
+ ReserveManager,
+ ReserveManagerLib
+} from "@wireio/outpost-ethereum-artifacts"
+import {
+ Contract,
+ getAddress,
+ getBigInt,
+ ZeroAddress,
+ type Provider,
+ type Signer
+} from "ethers"
+import { match } from "ts-pattern"
+
+import {
+ assertEthereumReserveCreateRequest,
+ assertReserveUnsigned64,
+ OutpostReserveStatus,
+ type EthereumReserveCreateRequest,
+ type EthereumReservePermitSignature,
+ type EthereumReserveRecord,
+ type OutpostReserveIdentity,
+ type OutpostReserveSubmission
+} from "../../reserves/index.js"
+import { assertEthereumSigner } from "./Connection.js"
+
+const ConfirmationCount = 1,
+ EthereumLocalReserveStatus = {
+ pending: 0n,
+ active: 1n,
+ cancelled: 2n
+ } as const,
+ Erc20Interface = [
+ "function allowance(address owner,address spender) view returns (uint256)",
+ "function approve(address spender,uint256 amount) returns (bool)"
+ ]
+
+/** Reserve creation, cancellation, and reads for one verified Ethereum outpost. */
+export class EthereumReserveClient {
+ /** Bind reserve lifecycle operations to a generated ReserveManager client. */
+ constructor(
+ private readonly reserveManager: ReserveManager,
+ private readonly connection: Provider | Signer
+ ) {}
+
+ /** Create a pending native-token reserve. */
+ async createNative(
+ request: EthereumReserveCreateRequest
+ ): Promise {
+ assertEthereumReserveCreateRequest(request)
+ this.assertSigner()
+ const parameters = this.nativeParameters(request),
+ overrides = { value: request.externalTokenAmount }
+
+ await this.reserveManager.create_reserve.staticCall(
+ ...parameters,
+ overrides
+ )
+ const transaction = await this.reserveManager.create_reserve(
+ ...parameters,
+ overrides
+ )
+ await transaction.wait(ConfirmationCount)
+ return { transactionId: transaction.hash }
+ }
+
+ /** Approve and create a pending ERC-20 reserve. */
+ async createErc20WithApproval(
+ request: EthereumReserveCreateRequest,
+ tokenAddress?: string
+ ): Promise {
+ assertEthereumReserveCreateRequest(request)
+ const signer = this.assertSigner(),
+ owner = await signer.getAddress(),
+ configuredTokenAddress = await this.reserveManager.tokenAddressesByCode(
+ request.tokenCode
+ )
+
+ if (configuredTokenAddress === ZeroAddress) {
+ throw new Error(
+ `No ERC-20 address is configured for tokenCode ${request.tokenCode.toString()}.`
+ )
+ }
+ if (
+ tokenAddress != null &&
+ getAddress(tokenAddress) !== getAddress(configuredTokenAddress)
+ ) {
+ throw new Error(
+ `ERC-20 address ${tokenAddress} does not match the configured route ${configuredTokenAddress}.`
+ )
+ }
+
+ const token = new Contract(configuredTokenAddress, Erc20Interface, signer),
+ reserveManagerAddress = await this.reserveManager.getAddress(),
+ allowance = getBigInt(
+ await token.allowance(owner, reserveManagerAddress)
+ )
+ if (allowance < getBigInt(request.externalTokenAmount)) {
+ const approval = await token.approve(
+ reserveManagerAddress,
+ request.externalTokenAmount
+ )
+ await approval.wait(ConfirmationCount)
+ }
+
+ const arguments_ = this.createArguments(request)
+ await this.reserveManager.requestReserveCreateErc20WithApproval.staticCall(
+ arguments_
+ )
+ const transaction =
+ await this.reserveManager.requestReserveCreateErc20WithApproval(
+ arguments_
+ )
+ await transaction.wait(ConfirmationCount)
+ return { transactionId: transaction.hash }
+ }
+
+ /** Create a pending ERC-20 reserve using an EIP-2612 permit. */
+ async createErc20WithPermit(
+ request: EthereumReserveCreateRequest,
+ permitSignature: EthereumReservePermitSignature
+ ): Promise {
+ assertEthereumReserveCreateRequest(request)
+ this.assertSigner()
+ const arguments_ = this.createArguments(request)
+
+ await this.reserveManager.requestReserveCreateErc20WithPermit.staticCall(
+ arguments_,
+ permitSignature
+ )
+ const transaction =
+ await this.reserveManager.requestReserveCreateErc20WithPermit(
+ arguments_,
+ permitSignature
+ )
+ await transaction.wait(ConfirmationCount)
+ return { transactionId: transaction.hash }
+ }
+
+ /** Request cancellation and refund of a pending reserve. */
+ async cancel(
+ identity: OutpostReserveIdentity
+ ): Promise {
+ this.assertSigner()
+ assertReserveUnsigned64(identity.tokenCode, "tokenCode")
+ assertReserveUnsigned64(identity.reserveCode, "reserveCode")
+ const transaction = await this.reserveManager.cancel_create_reserve(
+ identity.tokenCode,
+ identity.reserveCode
+ )
+ await transaction.wait(ConfirmationCount)
+ return { transactionId: transaction.hash }
+ }
+
+ /** Read and normalize one local ReserveManager record. */
+ async get(identity: OutpostReserveIdentity): Promise {
+ assertReserveUnsigned64(identity.tokenCode, "tokenCode")
+ assertReserveUnsigned64(identity.reserveCode, "reserveCode")
+ const reserve = await this.reserveManager.getReserve(
+ identity.tokenCode,
+ identity.reserveCode
+ )
+ return {
+ tokenCode: reserve.tokenCode,
+ reserveCode: reserve.reserveCode,
+ externalTokenAmount: reserve.externalTokenAmount,
+ requestedWireAmount: reserve.requestedWireAmount,
+ connectorWeightBps: Number(reserve.connectorWeightBps),
+ status: match(reserve.status)
+ .with(
+ EthereumLocalReserveStatus.pending,
+ () => OutpostReserveStatus.pending
+ )
+ .with(
+ EthereumLocalReserveStatus.active,
+ () => OutpostReserveStatus.active
+ )
+ .with(
+ EthereumLocalReserveStatus.cancelled,
+ () => OutpostReserveStatus.cancelled
+ )
+ .otherwise(value => {
+ throw new Error(`Unsupported Ethereum reserve status ${value}.`)
+ }),
+ creator: reserve.creator,
+ exists: reserve.exists
+ }
+ }
+
+ private assertSigner(): Signer {
+ return assertEthereumSigner(this.connection, "Ethereum reserve operation")
+ }
+
+ private createArguments(
+ request: EthereumReserveCreateRequest
+ ): ReserveManagerLib.ReserveCreateArgsStruct {
+ return {
+ tokenCode: request.tokenCode,
+ reserveCode: request.reserveCode,
+ externalTokenAmount: request.externalTokenAmount,
+ requestedWireAmount: request.requestedWireAmount,
+ connectorWeightBps: request.connectorWeightBps,
+ name: request.name,
+ description: request.description,
+ isPrivate: request.isPrivate,
+ creatorPubKey: request.creatorPubKey
+ }
+ }
+
+ private nativeParameters(request: EthereumReserveCreateRequest) {
+ return [
+ request.tokenCode,
+ request.reserveCode,
+ request.externalTokenAmount,
+ request.requestedWireAmount,
+ request.connectorWeightBps,
+ request.name,
+ request.description,
+ request.isPrivate,
+ request.creatorPubKey
+ ] as const
+ }
+}
diff --git a/packages/sdk-outpost/src/clients/ethereum/EthereumReserveSwapClient.ts b/packages/sdk-outpost/src/clients/ethereum/EthereumReserveSwapClient.ts
new file mode 100644
index 0000000..a712476
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/ethereum/EthereumReserveSwapClient.ts
@@ -0,0 +1,201 @@
+import type {
+ ReserveManager,
+ ReserveManagerLib
+} from "@wireio/outpost-ethereum-artifacts"
+import {
+ Contract,
+ getBigInt,
+ type BigNumberish,
+ type EventLog,
+ type Log,
+ type Provider,
+ type Signer
+} from "ethers"
+
+import {
+ assertReserveSwapRequest,
+ type ReserveSwapRequest,
+ type ReserveSwapSubmission
+} from "../../reserves/index.js"
+import { assertEthereumSigner, ethereumProvider } from "./Connection.js"
+
+const BasisPointDenominator = 10_000,
+ ConfirmationCount = 1,
+ Erc20Interface = [
+ "function allowance(address owner,address spender) view returns (uint256)",
+ "function approve(address spender,uint256 amount) returns (bool)",
+ "function balanceOf(address owner) view returns (uint256)"
+ ],
+ SubmissionGasHeadroomBps = 2_500
+
+/** Reserve-swap writes and balance reads for one verified Ethereum outpost. */
+export class EthereumReserveSwapClient {
+ /** Create an Ethereum reserve-swap workflow bound to a verified deployment. */
+ constructor(
+ private readonly reserveManager: ReserveManager,
+ private readonly connection: Provider | Signer
+ ) {}
+
+ /** Escrow native ETH and return the confirmed protocol deposit id. */
+ async requestNative(
+ request: ReserveSwapRequest
+ ): Promise {
+ assertReserveSwapRequest(request)
+ this.assertSigner()
+ const parameters = this.nativeParameters(request),
+ overrides = { value: request.sourceAmount }
+
+ await this.reserveManager.requestSwap.staticCall(...parameters, overrides)
+ const estimatedGas = await this.reserveManager.requestSwap.estimateGas(
+ ...parameters,
+ overrides
+ ),
+ submissionOverrides = {
+ ...overrides,
+ gasLimit:
+ EthereumReserveSwapClient.addSubmissionGasHeadroom(estimatedGas)
+ }
+
+ const transaction = await this.reserveManager.requestSwap(
+ ...parameters,
+ submissionOverrides
+ ),
+ receipt = await transaction.wait(ConfirmationCount)
+ return {
+ transactionId: transaction.hash,
+ sourceRequestId: EthereumReserveSwapClient.parseSourceRequestId(
+ receipt?.logs
+ )
+ }
+ }
+
+ /** Approve and escrow an ERC-20 source amount, then return its deposit id. */
+ async requestErc20WithApproval(
+ request: ReserveSwapRequest,
+ tokenAddress: string
+ ): Promise {
+ assertReserveSwapRequest(request)
+ const signer = this.assertSigner(),
+ owner = await signer.getAddress(),
+ token = new Contract(tokenAddress, Erc20Interface, signer),
+ reserveManagerAddress = await this.reserveManager.getAddress(),
+ allowance = await token.allowance(owner, reserveManagerAddress)
+
+ await EthereumReserveSwapClient.approvalAmounts(
+ allowance,
+ request.sourceAmount
+ ).reduce>(async (previousApproval, amount) => {
+ await previousApproval
+ const approval = await token.approve(reserveManagerAddress, amount)
+ await approval.wait(ConfirmationCount)
+ }, Promise.resolve())
+
+ const arguments_ = this.swapArguments(request)
+ await this.reserveManager.requestSwapErc20WithApproval.staticCall(
+ arguments_
+ )
+ const estimatedGas =
+ await this.reserveManager.requestSwapErc20WithApproval.estimateGas(
+ arguments_
+ ),
+ overrides = {
+ gasLimit:
+ EthereumReserveSwapClient.addSubmissionGasHeadroom(estimatedGas)
+ }
+ const transaction = await this.reserveManager.requestSwapErc20WithApproval(
+ arguments_,
+ overrides
+ ),
+ receipt = await transaction.wait(ConfirmationCount)
+ return {
+ transactionId: transaction.hash,
+ sourceRequestId: EthereumReserveSwapClient.parseSourceRequestId(
+ receipt?.logs
+ )
+ }
+ }
+
+ /** Read the native balance of an Ethereum account. */
+ async nativeBalance(address: string): Promise {
+ return ethereumProvider(this.connection).getBalance(address)
+ }
+
+ /** Read the ERC-20 balance of an Ethereum account. */
+ async erc20Balance(tokenAddress: string, address: string): Promise {
+ const token = new Contract(tokenAddress, Erc20Interface, this.connection)
+ return getBigInt(await token.balanceOf(address))
+ }
+
+ private assertSigner(): Signer {
+ return assertEthereumSigner(this.connection, "Ethereum reserve swap")
+ }
+
+ private nativeParameters(request: ReserveSwapRequest) {
+ return [
+ request.sourceTokenCode,
+ request.sourceReserveCode,
+ request.targetChainCode,
+ request.targetTokenCode,
+ request.targetReserveCode,
+ request.targetRecipient,
+ request.targetAmount,
+ request.targetToleranceBps
+ ] as const
+ }
+
+ private swapArguments(
+ request: ReserveSwapRequest
+ ): ReserveManagerLib.SwapArgsStruct {
+ return {
+ sourceTokenCode: request.sourceTokenCode,
+ sourceReserveCode: request.sourceReserveCode,
+ sourceAmount: request.sourceAmount,
+ targetChainCode: request.targetChainCode,
+ targetTokenCode: request.targetTokenCode,
+ targetReserveCode: request.targetReserveCode,
+ targetRecipient: request.targetRecipient,
+ targetAmount: request.targetAmount,
+ targetToleranceBps: request.targetToleranceBps
+ }
+ }
+
+ /** Add bounded headroom to estimates that traverse OPP delegate calls. */
+ static addSubmissionGasHeadroom(estimatedGas: BigNumberish): bigint {
+ const gas = getBigInt(estimatedGas),
+ denominator = BigInt(BasisPointDenominator)
+ return (
+ (gas * BigInt(BasisPointDenominator + SubmissionGasHeadroomBps) +
+ denominator -
+ 1n) /
+ denominator
+ )
+ }
+
+ /** Return the safe approval sequence for zero-first ERC-20 implementations. */
+ static approvalAmounts(
+ currentAllowance: BigNumberish,
+ requiredAllowance: BigNumberish
+ ): readonly bigint[] {
+ const current = getBigInt(currentAllowance),
+ required = getBigInt(requiredAllowance)
+ if (current >= required) return []
+ return current === 0n ? [required] : [0n, required]
+ }
+
+ /** Parse the canonical deposit id emitted by `requestSwap*`. */
+ static parseSourceRequestId(
+ events: readonly (EventLog | Log)[] | undefined
+ ): bigint {
+ const event = events?.find(
+ candidate =>
+ "eventName" in candidate && candidate.eventName === "SwapDeposit"
+ ),
+ id = event != null && "args" in event ? event.args[0] : null
+ if (id == null) {
+ throw new Error(
+ "Confirmed Ethereum reserve swap did not emit SwapDeposit."
+ )
+ }
+ return BigInt(id.toString())
+ }
+}
diff --git a/packages/sdk-outpost/src/clients/ethereum/Types.ts b/packages/sdk-outpost/src/clients/ethereum/Types.ts
new file mode 100644
index 0000000..001d764
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/ethereum/Types.ts
@@ -0,0 +1,33 @@
+import type {
+ BAR,
+ OPP,
+ OPPInbound,
+ OperatorRegistry,
+ ReserveManager
+} from "@wireio/outpost-ethereum-artifacts"
+import type { Provider, Signer } from "ethers"
+
+import type { OutpostDeploymentProfile } from "../../deployments/index.js"
+import { EthereumContractName } from "../../deployments/index.js"
+
+/** Inputs required to connect an Ethereum outpost client. */
+export interface EthereumOutpostClientOptions {
+ /** Immutable deployment profile selected from the parent Wire chain. */
+ profile: OutpostDeploymentProfile
+ /** Ethers provider or connected signer for the target Ethereum chain. */
+ connection: Provider | Signer
+}
+
+/** Generated contract clients keyed by their deployment identity. */
+export interface EthereumContractMap {
+ /** Bond and node-owner registration contract. */
+ [EthereumContractName.BAR]: BAR
+ /** Outbound OPP endpoint. */
+ [EthereumContractName.OPP]: OPP
+ /** Inbound OPP endpoint. */
+ [EthereumContractName.OPPInbound]: OPPInbound
+ /** Operator collateral registry. */
+ [EthereumContractName.OperatorRegistry]: OperatorRegistry
+ /** Reserve custody manager. */
+ [EthereumContractName.ReserveManager]: ReserveManager
+}
diff --git a/packages/sdk-outpost/src/clients/ethereum/index.ts b/packages/sdk-outpost/src/clients/ethereum/index.ts
new file mode 100644
index 0000000..7f8f194
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/ethereum/index.ts
@@ -0,0 +1,4 @@
+export * from "./EthereumReserveSwapClient.js"
+export * from "./EthereumReserveClient.js"
+export * from "./EthereumNodeOwnerClient.js"
+export * from "./Types.js"
diff --git a/packages/sdk-outpost/src/clients/index.ts b/packages/sdk-outpost/src/clients/index.ts
new file mode 100644
index 0000000..70c41f7
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/index.ts
@@ -0,0 +1,4 @@
+export * from "./ethereum/index.js"
+export * from "./OutpostClient.js"
+export * from "./solana/index.js"
+export * from "./Types.js"
diff --git a/packages/sdk-outpost/src/clients/solana/SolanaOutpostClient.ts b/packages/sdk-outpost/src/clients/solana/SolanaOutpostClient.ts
new file mode 100644
index 0000000..01f0566
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/solana/SolanaOutpostClient.ts
@@ -0,0 +1,81 @@
+import { Program } from "@coral-xyz/anchor"
+import type { LiqsolCore } from "@wireio/outpost-solana-artifacts"
+import { match } from "ts-pattern"
+
+import {
+ OutpostArtifactRegistry,
+ type OutpostArtifactSuite
+} from "../../artifacts/Registry.js"
+import {
+ OutpostChainFamily,
+ SolanaProgramName
+} from "../../deployments/index.js"
+import { OutpostDeploymentVerifier } from "../../verification/index.js"
+import { SolanaOutpostClientOptions, SolanaProgramMap } from "./Types.js"
+import { SolanaReserveClient } from "./SolanaReserveClient.js"
+import { SolanaReserveSwapClient } from "./SolanaReserveSwapClient.js"
+
+/** Strictly typed access to one verified Solana outpost deployment. */
+export class SolanaOutpostClient {
+ /** Create the Solana backend for the package-level outpost client facade. */
+ static async create(
+ options: SolanaOutpostClientOptions
+ ): Promise {
+ const { profile, provider } = options
+
+ await OutpostDeploymentVerifier.verify({
+ family: OutpostChainFamily.solana,
+ profile,
+ connection: provider.connection
+ })
+ return new SolanaOutpostClient(
+ options,
+ OutpostArtifactRegistry.resolve(profile)
+ )
+ }
+
+ private readonly liqsolCore: Program
+
+ private constructor(
+ private readonly options: SolanaOutpostClientOptions,
+ artifactSuite: OutpostArtifactSuite
+ ) {
+ const address =
+ options.profile.solana.programs[SolanaProgramName.liqsolCore].address
+
+ this.liqsolCore = new Program(
+ {
+ ...artifactSuite.solana.idls[SolanaProgramName.liqsolCore],
+ address
+ },
+ options.provider
+ )
+ this.reserves = new SolanaReserveClient(options.provider, this.liqsolCore)
+ this.swaps = new SolanaReserveSwapClient(options.provider, this.liqsolCore)
+ }
+
+ /** Reserve creation, cancellation, and reads for this verified outpost. */
+ readonly reserves: SolanaReserveClient
+
+ /** Reserve-swap writes and balance reads for this verified outpost. */
+ readonly swaps: SolanaReserveSwapClient
+
+ /** Provider verified against the configured Solana cluster. */
+ get provider(): SolanaOutpostClientOptions["provider"] {
+ return this.options.provider
+ }
+
+ /** Deployment profile used to verify and connect this client. */
+ get profile(): SolanaOutpostClientOptions["profile"] {
+ return this.options.profile
+ }
+
+ /** Return a generated Anchor program client by typed deployment name. */
+ program(name: T): SolanaProgramMap[T] {
+ const program = match(name as SolanaProgramName)
+ .with(SolanaProgramName.liqsolCore, () => this.liqsolCore)
+ .exhaustive()
+
+ return program as SolanaProgramMap[T]
+ }
+}
diff --git a/packages/sdk-outpost/src/clients/solana/SolanaReserveAddresses.ts b/packages/sdk-outpost/src/clients/solana/SolanaReserveAddresses.ts
new file mode 100644
index 0000000..464e2ad
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/solana/SolanaReserveAddresses.ts
@@ -0,0 +1,75 @@
+import { BN } from "@coral-xyz/anchor"
+import { PublicKey } from "@solana/web3.js"
+
+import {
+ assertReserveUnsigned64,
+ type OutpostReserveIdentity
+} from "../../reserves/index.js"
+
+const SolanaReserveSeed = {
+ outpostConfig: Buffer.from("outpost_config"),
+ outboundMessageBuffer: Buffer.from("outbound_message_buffer"),
+ reserve: Buffer.from("reserve"),
+ reserveVault: Buffer.from("reserve_vault")
+ } as const,
+ Unsigned64ByteLength = 8
+
+/** Canonical PDA derivation for Solana reserve lifecycle and swap clients. */
+export class SolanaReserveAddresses {
+ /** Bind reserve address derivation to one verified program deployment. */
+ constructor(private readonly programId: PublicKey) {}
+
+ /** Derive the singleton outpost configuration PDA. */
+ outpostConfig(): PublicKey {
+ return this.derive([SolanaReserveSeed.outpostConfig])
+ }
+
+ /** Derive the singleton outbound message-buffer PDA. */
+ outboundMessageBuffer(): PublicKey {
+ return this.derive([SolanaReserveSeed.outboundMessageBuffer])
+ }
+
+ /** Derive a reserve account PDA. */
+ reserve(identity: OutpostReserveIdentity): PublicKey {
+ return this.reserveAddress(SolanaReserveSeed.reserve, identity)
+ }
+
+ /** Derive a reserve custody-vault PDA. */
+ reserveVault(identity: OutpostReserveIdentity): PublicKey {
+ return this.reserveAddress(SolanaReserveSeed.reserveVault, identity)
+ }
+
+ /** Convert an SDK reserve integer to Anchor's u64 representation. */
+ unsigned64(
+ value: OutpostReserveIdentity["tokenCode"],
+ field: string
+ ): BN {
+ return new BN(assertReserveUnsigned64(value, field).toString())
+ }
+
+ private derive(seeds: Buffer[]): PublicKey {
+ return PublicKey.findProgramAddressSync(seeds, this.programId)[0]
+ }
+
+ private reserveAddress(
+ seed: Buffer,
+ identity: OutpostReserveIdentity
+ ): PublicKey {
+ return this.derive([
+ seed,
+ this.unsigned64Seed(identity.tokenCode, "tokenCode"),
+ this.unsigned64Seed(identity.reserveCode, "reserveCode")
+ ])
+ }
+
+ private unsigned64Seed(
+ value: OutpostReserveIdentity["tokenCode"],
+ field: string
+ ): Buffer {
+ return this.unsigned64(value, field).toArrayLike(
+ Buffer,
+ "le",
+ Unsigned64ByteLength
+ )
+ }
+}
diff --git a/packages/sdk-outpost/src/clients/solana/SolanaReserveClient.ts b/packages/sdk-outpost/src/clients/solana/SolanaReserveClient.ts
new file mode 100644
index 0000000..d52f8bd
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/solana/SolanaReserveClient.ts
@@ -0,0 +1,258 @@
+import {
+ type AnchorProvider,
+ type IdlAccounts,
+ type Program
+} from "@coral-xyz/anchor"
+import {
+ createAssociatedTokenAccountIdempotentInstruction,
+ getAssociatedTokenAddressSync,
+ TOKEN_PROGRAM_ID
+} from "@solana/spl-token"
+import {
+ PublicKey,
+ SYSVAR_RENT_PUBKEY,
+ SystemProgram,
+ Transaction,
+ type TransactionInstruction
+} from "@solana/web3.js"
+import { match } from "ts-pattern"
+
+import type { LiqsolCore } from "@wireio/outpost-solana-artifacts"
+import {
+ assertReserveCreateDefinition,
+ assertReserveUnsigned64,
+ OutpostReserveStatus,
+ type OutpostReserveIdentity,
+ type OutpostReserveSubmission,
+ type SolanaConfiguredReserveToken,
+ type SolanaReserveCreateRequest,
+ type SolanaReserveRecord
+} from "../../reserves/index.js"
+import { SolanaReserveAddresses } from "./SolanaReserveAddresses.js"
+
+const PublicKeyByteLength = 32,
+ NativeSolanaDecimals = 9,
+ NativeTokenMarker = new PublicKey(new Uint8Array(PublicKeyByteLength))
+
+type LiqsolCoreAccounts = IdlAccounts
+
+/** Reserve creation, cancellation, and reads for one verified Solana outpost. */
+export class SolanaReserveClient {
+ private readonly addresses: SolanaReserveAddresses
+
+ /** Bind reserve lifecycle operations to a generated Anchor program client. */
+ constructor(
+ private readonly provider: AnchorProvider,
+ private readonly program: Program
+ ) {
+ this.addresses = new SolanaReserveAddresses(program.programId)
+ }
+
+ /** List token routes configured by the deployed outpost. */
+ async getConfiguredTokens(): Promise {
+ const account = await this.provider.connection.getAccountInfo(
+ this.addresses.outpostConfig()
+ )
+ if (account == null) {
+ throw new Error("Solana outpost configuration is unavailable.")
+ }
+ const config = this.program.coder.accounts.decode<
+ LiqsolCoreAccounts["outpostConfig"]
+ >("outpostConfig", account.data),
+ precisionByTokenCode = new Map(
+ config.precisionByTokenCode.map(entry => [
+ entry.tokenCode.toString(),
+ entry.decimals
+ ])
+ )
+
+ return config.tokenAddressesByCode.map(entry => {
+ const tokenCode = entry.tokenCode.toString(),
+ isNative = entry.mint.equals(NativeTokenMarker),
+ configuredDecimals = precisionByTokenCode.get(tokenCode)
+ if (!isNative && configuredDecimals == null) {
+ throw new Error(
+ `Solana reserve token ${tokenCode} has no configured precision.`
+ )
+ }
+ return {
+ tokenCode: BigInt(tokenCode),
+ mint: entry.mint,
+ isNative,
+ decimals: isNative ? NativeSolanaDecimals : configuredDecimals
+ }
+ })
+ }
+
+ /** Derive the local outpost account for one reserve identity. */
+ deriveAddress(identity: OutpostReserveIdentity): PublicKey {
+ return this.addresses.reserve(identity)
+ }
+
+ /** Build reserve-creation instructions without signing or submitting. */
+ async createInstructions(
+ request: SolanaReserveCreateRequest
+ ): Promise {
+ assertReserveCreateDefinition(request)
+ assertReserveUnsigned64(request.externalTokenAmount, "externalTokenAmount")
+ if (request.mint.equals(NativeTokenMarker)) {
+ throw new Error(
+ "Native SOL reserve creation requires a real placeholder SPL mint and creator token account; the all-zero native marker is configuration metadata only."
+ )
+ }
+ const creator = this.assertWallet(),
+ {
+ creatorTokenAccount = getAssociatedTokenAddressSync(
+ request.mint,
+ creator,
+ false,
+ TOKEN_PROGRAM_ID
+ )
+ } = request,
+ createReserve = await this.program.methods
+ .createReserve(
+ this.addresses.unsigned64(request.tokenCode, "tokenCode"),
+ this.addresses.unsigned64(request.reserveCode, "reserveCode"),
+ this.addresses.unsigned64(
+ request.externalTokenAmount,
+ "externalTokenAmount"
+ ),
+ this.addresses.unsigned64(
+ request.requestedWireAmount,
+ "requestedWireAmount"
+ ),
+ request.connectorWeightBps,
+ request.name,
+ request.description,
+ request.isPrivate
+ )
+ .accounts({
+ creator,
+ config: this.addresses.outpostConfig(),
+ reserve: this.addresses.reserve(request),
+ reserveVault: this.addresses.reserveVault(request),
+ mint: request.mint,
+ creatorAta: creatorTokenAccount,
+ outboundMessageBuffer: this.addresses.outboundMessageBuffer(),
+ tokenProgram: TOKEN_PROGRAM_ID,
+ systemProgram: SystemProgram.programId,
+ rent: SYSVAR_RENT_PUBKEY
+ })
+ .instruction()
+
+ if (
+ request.ensureCreatorTokenAccount === false ||
+ request.creatorTokenAccount != null
+ ) {
+ return [createReserve]
+ }
+
+ return [
+ createAssociatedTokenAccountIdempotentInstruction(
+ creator,
+ creatorTokenAccount,
+ creator,
+ request.mint,
+ TOKEN_PROGRAM_ID
+ ),
+ createReserve
+ ]
+ }
+
+ /** Create a pending external reserve. */
+ async create(
+ request: SolanaReserveCreateRequest
+ ): Promise {
+ const transactionId = await this.provider.sendAndConfirm(
+ new Transaction().add(...(await this.createInstructions(request)))
+ )
+ return { transactionId }
+ }
+
+ /** Build a creator-authorized pending-reserve cancellation instruction. */
+ async cancelInstruction(
+ identity: OutpostReserveIdentity
+ ): Promise {
+ const creator = this.assertWallet()
+ return this.program.methods
+ .cancelCreateReserve(
+ this.addresses.unsigned64(identity.tokenCode, "tokenCode"),
+ this.addresses.unsigned64(identity.reserveCode, "reserveCode")
+ )
+ .accounts({
+ creator,
+ config: this.addresses.outpostConfig(),
+ reserve: this.addresses.reserve(identity),
+ outboundMessageBuffer: this.addresses.outboundMessageBuffer()
+ })
+ .instruction()
+ }
+
+ /** Request cancellation and refund of a pending reserve. */
+ async cancel(
+ identity: OutpostReserveIdentity
+ ): Promise {
+ const transactionId = await this.provider.sendAndConfirm(
+ new Transaction().add(await this.cancelInstruction(identity))
+ )
+ return { transactionId }
+ }
+
+ /** Read and normalize one local reserve account. */
+ async get(
+ identity: OutpostReserveIdentity
+ ): Promise {
+ const address = this.addresses.reserve(identity),
+ account = await this.provider.connection.getAccountInfo(address)
+ if (account == null) return null
+ const reserve = this.program.coder.accounts.decode<
+ LiqsolCoreAccounts["reserve"]
+ >("reserve", account.data)
+
+ return {
+ address,
+ vaultAddress: this.addresses.reserveVault(identity),
+ tokenCode: BigInt(reserve.tokenCode.toString()),
+ reserveCode: BigInt(reserve.reserveCode.toString()),
+ externalTokenAmount: BigInt(reserve.externalTokenAmount.toString()),
+ requestedWireAmount: BigInt(reserve.requestedWireAmount.toString()),
+ connectorWeightBps: reserve.connectorWeightBps,
+ status: match(reserve.status)
+ .when(
+ status => "pending" in status,
+ () => OutpostReserveStatus.pending
+ )
+ .when(
+ status => "active" in status,
+ () => OutpostReserveStatus.active
+ )
+ .when(
+ status => "cancelled" in status,
+ () => OutpostReserveStatus.cancelled
+ )
+ .otherwise(() => {
+ throw new Error("Unsupported Solana reserve status.")
+ }),
+ creator: reserve.creator,
+ name: this.decodeFixedUtf8(reserve.nameBytes, reserve.nameLen),
+ description: this.decodeFixedUtf8(
+ reserve.descriptionBytes,
+ reserve.descriptionLen
+ )
+ }
+ }
+
+ private assertWallet(): PublicKey {
+ const publicKey = this.provider.wallet.publicKey
+ if (publicKey == null) {
+ throw new Error("Solana reserve operation requires a connected wallet.")
+ }
+ return publicKey
+ }
+
+ private decodeFixedUtf8(value: ArrayLike, length: number): string {
+ return new TextDecoder().decode(
+ Uint8Array.from(value).slice(0, Number(length))
+ )
+ }
+}
diff --git a/packages/sdk-outpost/src/clients/solana/SolanaReserveSwapClient.ts b/packages/sdk-outpost/src/clients/solana/SolanaReserveSwapClient.ts
new file mode 100644
index 0000000..2622e6d
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/solana/SolanaReserveSwapClient.ts
@@ -0,0 +1,261 @@
+import { type AnchorProvider, type Program } from "@coral-xyz/anchor"
+import {
+ getAssociatedTokenAddressSync,
+ TOKEN_PROGRAM_ID
+} from "@solana/spl-token"
+import {
+ PublicKey,
+ SystemProgram,
+ Transaction,
+ type TransactionInstruction,
+ type VersionedTransactionResponse
+} from "@solana/web3.js"
+import type { LiqsolCore } from "@wireio/outpost-solana-artifacts"
+import { getBytes } from "ethers"
+
+import {
+ assertReserveSwapRequest,
+ type ReserveSwapRequest,
+ type ReserveSwapSubmission,
+ type SolanaSplReserveSwapRequest
+} from "../../reserves/index.js"
+import { SolanaReserveAddresses } from "./SolanaReserveAddresses.js"
+
+const ConfirmationCommitment = "confirmed",
+ ConfirmationPollIntervalMs = 1_500,
+ SolanaConfirmationStatus = {
+ confirmed: "confirmed",
+ finalized: "finalized"
+ } as const,
+ SwapDepositLog = /opp_outpost: SwapDeposit id=(\d+)\b/
+
+/** Reserve-swap writes and balance reads for one verified Solana outpost. */
+export class SolanaReserveSwapClient {
+ private readonly addresses: SolanaReserveAddresses
+
+ /** Create a Solana reserve-swap workflow bound to a verified deployment. */
+ constructor(
+ private readonly provider: AnchorProvider,
+ private readonly program: Program
+ ) {
+ this.addresses = new SolanaReserveAddresses(program.programId)
+ }
+
+ /** Build a native-SOL reserve-swap instruction without signing it. */
+ async createNativeInstruction(
+ request: ReserveSwapRequest
+ ): Promise {
+ assertReserveSwapRequest(request)
+ const user = this.assertWallet()
+ return this.program.methods
+ .requestSwap(...this.instructionArguments(request))
+ .accounts({
+ user,
+ config: this.addresses.outpostConfig(),
+ reserve: this.addresses.reserve({
+ tokenCode: request.sourceTokenCode,
+ reserveCode: request.sourceReserveCode
+ }),
+ outboundMessageBuffer: this.addresses.outboundMessageBuffer(),
+ systemProgram: SystemProgram.programId
+ })
+ .instruction()
+ }
+
+ /** Escrow native SOL and return the confirmed protocol deposit id. */
+ async requestNative(
+ request: ReserveSwapRequest
+ ): Promise {
+ return this.submit(await this.createNativeInstruction(request))
+ }
+
+ /** Build a classic SPL reserve-swap instruction without signing it. */
+ async createSplInstruction(
+ request: SolanaSplReserveSwapRequest
+ ): Promise {
+ assertReserveSwapRequest(request)
+ const user = this.assertWallet(),
+ {
+ userTokenAccount = getAssociatedTokenAddressSync(
+ request.mint,
+ user,
+ false,
+ TOKEN_PROGRAM_ID
+ )
+ } = request
+
+ return this.program.methods
+ .requestSwapSpl(...this.instructionArguments(request))
+ .accounts({
+ user,
+ config: this.addresses.outpostConfig(),
+ reserve: this.addresses.reserve({
+ tokenCode: request.sourceTokenCode,
+ reserveCode: request.sourceReserveCode
+ }),
+ reserveVault: this.addresses.reserveVault({
+ tokenCode: request.sourceTokenCode,
+ reserveCode: request.sourceReserveCode
+ }),
+ mint: request.mint,
+ userAta: userTokenAccount,
+ outboundMessageBuffer: this.addresses.outboundMessageBuffer(),
+ tokenProgram: TOKEN_PROGRAM_ID
+ })
+ .instruction()
+ }
+
+ /** Escrow a classic SPL token and return the confirmed protocol deposit id. */
+ async requestSpl(
+ request: SolanaSplReserveSwapRequest
+ ): Promise {
+ return this.submit(await this.createSplInstruction(request))
+ }
+
+ /** Read the native SOL balance of one account. */
+ async nativeBalance(owner = this.assertWallet()): Promise {
+ return BigInt(
+ await this.provider.connection.getBalance(
+ owner,
+ ConfirmationCommitment
+ )
+ )
+ }
+
+ /** Read a classic SPL token balance, returning zero when the ATA is absent. */
+ async splBalance(
+ mint: PublicKey,
+ owner = this.assertWallet()
+ ): Promise {
+ const tokenAccount = getAssociatedTokenAddressSync(
+ mint,
+ owner,
+ false,
+ TOKEN_PROGRAM_ID
+ ),
+ account = await this.provider.connection.getAccountInfo(
+ tokenAccount,
+ ConfirmationCommitment
+ )
+ if (account == null) return 0n
+ const balance = await this.provider.connection.getTokenAccountBalance(
+ tokenAccount,
+ ConfirmationCommitment
+ )
+ return BigInt(balance.value.amount)
+ }
+
+ private assertWallet(): PublicKey {
+ const publicKey = this.provider.wallet.publicKey
+ if (publicKey == null) {
+ throw new Error("Solana reserve swap requires a connected wallet.")
+ }
+ return publicKey
+ }
+
+ private instructionArguments(request: ReserveSwapRequest) {
+ return [
+ this.unsigned64(request.sourceTokenCode, "sourceTokenCode"),
+ this.unsigned64(request.sourceReserveCode, "sourceReserveCode"),
+ this.unsigned64(request.sourceAmount, "sourceAmount"),
+ this.unsigned64(request.targetChainCode, "targetChainCode"),
+ this.unsigned64(request.targetTokenCode, "targetTokenCode"),
+ this.unsigned64(request.targetReserveCode, "targetReserveCode"),
+ Buffer.from(getBytes(request.targetRecipient)),
+ this.unsigned64(request.targetAmount, "targetAmount"),
+ request.targetToleranceBps
+ ] as const
+ }
+
+ private unsigned64(
+ value: ReserveSwapRequest["sourceTokenCode"],
+ field: string
+ ) {
+ return this.addresses.unsigned64(value, field)
+ }
+
+ private async submit(
+ instruction: TransactionInstruction
+ ): Promise {
+ const connection = this.provider.connection,
+ latestBlockhash = await connection.getLatestBlockhash(
+ ConfirmationCommitment
+ ),
+ transaction = new Transaction({
+ feePayer: this.assertWallet(),
+ blockhash: latestBlockhash.blockhash,
+ lastValidBlockHeight: latestBlockhash.lastValidBlockHeight
+ }).add(instruction),
+ signedTransaction = await this.provider.wallet.signTransaction(
+ transaction
+ ),
+ transactionId = await connection.sendRawTransaction(
+ signedTransaction.serialize(),
+ { preflightCommitment: ConfirmationCommitment }
+ ),
+ confirmedTransaction = await this.waitForConfirmedTransaction(
+ transactionId,
+ latestBlockhash.lastValidBlockHeight
+ ),
+ sourceRequestId = SolanaReserveSwapClient.parseSourceRequestId(
+ confirmedTransaction.meta?.logMessages ?? []
+ )
+ return { transactionId, sourceRequestId }
+ }
+
+ /** Poll one submitted signature until Solana records success or a terminal error. */
+ private async waitForConfirmedTransaction(
+ transactionId: string,
+ lastValidBlockHeight: number
+ ): Promise {
+ const connection = this.provider.connection,
+ [statusResponse, blockHeight] = await Promise.all([
+ connection.getSignatureStatuses([transactionId], {
+ searchTransactionHistory: true
+ }),
+ connection.getBlockHeight(ConfirmationCommitment)
+ ]),
+ status = statusResponse.value[0]
+
+ if (status?.err != null) {
+ throw new Error(
+ `Solana reserve swap ${transactionId} failed: ${JSON.stringify(status.err)}`
+ )
+ }
+
+ const confirmed =
+ status?.confirmationStatus === SolanaConfirmationStatus.confirmed ||
+ status?.confirmationStatus === SolanaConfirmationStatus.finalized
+ if (confirmed) {
+ const confirmedTransaction = await connection.getTransaction(
+ transactionId,
+ {
+ commitment: ConfirmationCommitment,
+ maxSupportedTransactionVersion: 0
+ }
+ )
+ if (confirmedTransaction != null) return confirmedTransaction
+ } else if (status == null && blockHeight > lastValidBlockHeight) {
+ throw new Error(
+ `Solana reserve swap ${transactionId} expired before it was recorded on chain.`
+ )
+ }
+
+ await new Promise(resolve => setTimeout(resolve, ConfirmationPollIntervalMs))
+ return this.waitForConfirmedTransaction(
+ transactionId,
+ lastValidBlockHeight
+ )
+ }
+
+ /** Parse the canonical deposit id logged by `request_swap*`. */
+ static parseSourceRequestId(logMessages: readonly string[]): bigint {
+ const match = logMessages
+ .map(message => message.match(SwapDepositLog))
+ .find(candidate => candidate != null)
+ if (match == null) {
+ throw new Error("Confirmed Solana reserve swap did not log SwapDeposit.")
+ }
+ return BigInt(match[1])
+ }
+}
diff --git a/packages/sdk-outpost/src/clients/solana/Types.ts b/packages/sdk-outpost/src/clients/solana/Types.ts
new file mode 100644
index 0000000..00d258a
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/solana/Types.ts
@@ -0,0 +1,19 @@
+import type { AnchorProvider, Program } from "@coral-xyz/anchor"
+
+import type { OutpostDeploymentProfile } from "../../deployments/index.js"
+import { SolanaProgramName } from "../../deployments/index.js"
+import type { LiqsolCore } from "@wireio/outpost-solana-artifacts"
+
+/** Inputs required to connect a Solana outpost client. */
+export interface SolanaOutpostClientOptions {
+ /** Immutable deployment profile selected from the parent Wire chain. */
+ profile: OutpostDeploymentProfile
+ /** Anchor provider for the target Solana cluster. */
+ provider: AnchorProvider
+}
+
+/** Generated program clients keyed by their deployment identity. */
+export interface SolanaProgramMap {
+ /** liqSOL core program deployed for this network group. */
+ [SolanaProgramName.liqsolCore]: Program
+}
diff --git a/packages/sdk-outpost/src/clients/solana/index.ts b/packages/sdk-outpost/src/clients/solana/index.ts
new file mode 100644
index 0000000..3bd9d25
--- /dev/null
+++ b/packages/sdk-outpost/src/clients/solana/index.ts
@@ -0,0 +1,4 @@
+export * from "./SolanaReserveSwapClient.js"
+export * from "./SolanaReserveAddresses.js"
+export * from "./SolanaReserveClient.js"
+export * from "./Types.js"
diff --git a/packages/sdk-outpost/src/deployments/Schema.ts b/packages/sdk-outpost/src/deployments/Schema.ts
new file mode 100644
index 0000000..9a50a12
--- /dev/null
+++ b/packages/sdk-outpost/src/deployments/Schema.ts
@@ -0,0 +1,101 @@
+import { PublicKey } from "@solana/web3.js"
+import { getAddress } from "ethers"
+import { z } from "zod"
+
+import { EthereumContractName, SolanaProgramName } from "./Types.js"
+
+const Sha256Schema = z.string().regex(/^[0-9a-f]{64}$/),
+ WireChainIdSchema = z.string().regex(/^[0-9a-f]{64}$/),
+ EthereumAddressSchema = z.string().transform((value, context) => {
+ try {
+ return getAddress(value)
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : String(error)
+ context.addIssue({
+ code: "custom",
+ message: `Invalid Ethereum address: ${message}`
+ })
+ return z.NEVER
+ }
+ }),
+ SolanaAddressSchema = z.string().transform((value, context) => {
+ try {
+ return new PublicKey(value).toBase58()
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : String(error)
+ context.addIssue({
+ code: "custom",
+ message: `Invalid Solana address: ${message}`
+ })
+ return z.NEVER
+ }
+ })
+
+/** Compatibility and runtime identity for one deployed Ethereum contract. */
+export const EthereumContractDeploymentProfileSchema = z.object({
+ address: EthereumAddressSchema,
+ implementationAddress: EthereumAddressSchema,
+ abiSha256: Sha256Schema,
+ implementationCodeSha256: Sha256Schema
+})
+
+/** Compatibility and runtime identity for one deployed Solana program. */
+export const SolanaProgramDeploymentProfileSchema = z.object({
+ address: SolanaAddressSchema,
+ programDataAddress: SolanaAddressSchema,
+ idlSha256: Sha256Schema,
+ programDataSha256: Sha256Schema
+})
+
+/** Immutable compatibility profile for one Wire outpost deployment. */
+export const OutpostDeploymentProfileSchema = z
+ .object({
+ schemaVersion: z.literal(1),
+ id: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
+ deploymentChecksum: Sha256Schema,
+ wire: z.object({
+ chainId: WireChainIdSchema
+ }),
+ ethereum: z.object({
+ chainId: z.number().int().positive(),
+ contracts: z.object({
+ [EthereumContractName.BAR]:
+ EthereumContractDeploymentProfileSchema.optional(),
+ [EthereumContractName.OPP]: EthereumContractDeploymentProfileSchema,
+ [EthereumContractName.OPPInbound]:
+ EthereumContractDeploymentProfileSchema,
+ [EthereumContractName.OperatorRegistry]:
+ EthereumContractDeploymentProfileSchema,
+ [EthereumContractName.ReserveManager]:
+ EthereumContractDeploymentProfileSchema
+ })
+ }),
+ solana: z.object({
+ genesisHash: SolanaAddressSchema,
+ programs: z.object({
+ [SolanaProgramName.liqsolCore]: SolanaProgramDeploymentProfileSchema
+ })
+ })
+ })
+ .superRefine((profile, context) => {
+ const expectedId = `${profile.wire.chainId}-${profile.deploymentChecksum.slice(0, 12)}`
+ if (profile.id !== expectedId) {
+ context.addIssue({
+ code: "custom",
+ message: `Deployment profile id must be ${expectedId}`,
+ path: ["id"]
+ })
+ }
+ })
+
+/** Parsed, runtime-safe outpost deployment profile. */
+export type OutpostDeploymentProfile = z.infer<
+ typeof OutpostDeploymentProfileSchema
+>
+
+/** Validate an untrusted deployment profile at its JSON boundary. */
+export function parseOutpostDeploymentProfile(
+ value: unknown
+): OutpostDeploymentProfile {
+ return OutpostDeploymentProfileSchema.parse(value)
+}
diff --git a/packages/sdk-outpost/src/deployments/Types.ts b/packages/sdk-outpost/src/deployments/Types.ts
new file mode 100644
index 0000000..33abb78
--- /dev/null
+++ b/packages/sdk-outpost/src/deployments/Types.ts
@@ -0,0 +1,19 @@
+/** Supported external-chain client families. */
+export enum OutpostChainFamily {
+ ethereum = "ethereum",
+ solana = "solana"
+}
+
+/** Ethereum contracts owned by the current outpost deployment. */
+export enum EthereumContractName {
+ BAR = "BAR",
+ OPP = "OPP",
+ OPPInbound = "OPPInbound",
+ OperatorRegistry = "OperatorRegistry",
+ ReserveManager = "ReserveManager"
+}
+
+/** Solana programs owned by the current outpost deployment. */
+export enum SolanaProgramName {
+ liqsolCore = "liqsolCore"
+}
diff --git a/packages/sdk-outpost/src/deployments/index.ts b/packages/sdk-outpost/src/deployments/index.ts
new file mode 100644
index 0000000..29919be
--- /dev/null
+++ b/packages/sdk-outpost/src/deployments/index.ts
@@ -0,0 +1,2 @@
+export * from "./Schema.js"
+export * from "./Types.js"
diff --git a/packages/sdk-outpost/src/index.ts b/packages/sdk-outpost/src/index.ts
new file mode 100644
index 0000000..974408c
--- /dev/null
+++ b/packages/sdk-outpost/src/index.ts
@@ -0,0 +1,5 @@
+export * from "./clients/index.js"
+export * from "./artifacts/index.js"
+export * from "./deployments/index.js"
+export * from "./reserves/index.js"
+export * from "./verification/index.js"
diff --git a/packages/sdk-outpost/src/reserves/Types.ts b/packages/sdk-outpost/src/reserves/Types.ts
new file mode 100644
index 0000000..1595565
--- /dev/null
+++ b/packages/sdk-outpost/src/reserves/Types.ts
@@ -0,0 +1,155 @@
+import type { PublicKey } from "@solana/web3.js"
+import type { ReserveManagerLib } from "@wireio/outpost-ethereum-artifacts"
+import type { BigNumberish, BytesLike } from "ethers"
+
+/** Two-part identity shared by external-chain reserve custody clients. */
+export interface OutpostReserveIdentity {
+ /** Packed external token slug. */
+ tokenCode: BigNumberish
+ /** Packed reserve discriminator slug. */
+ reserveCode: BigNumberish
+}
+
+/** Portable reserve fields validated before any external wallet prompt. */
+export interface ReserveCreateDefinition extends OutpostReserveIdentity {
+ /** External-chain amount escrowed by the creator. */
+ externalTokenAmount: BigNumberish
+ /** Exact WIRE amount required to activate the reserve. */
+ requestedWireAmount: BigNumberish
+ /** Bancor connector weight in basis points. */
+ connectorWeightBps: number
+ /** User-facing reserve name. */
+ name: string
+ /** User-facing reserve description. */
+ description: string
+ /** Whether Wire restricts routing to a same-owner external counterpart. */
+ isPrivate: boolean
+}
+
+/** Ethereum reserve-creation request with the creator's AuthEx public key. */
+export interface EthereumReserveCreateRequest extends ReserveCreateDefinition {
+ /** Compressed secp256k1 public key that derives to the connected signer. */
+ creatorPubKey: BytesLike
+}
+
+/** Confirmed external reserve lifecycle submission. */
+export interface OutpostReserveSubmission {
+ /** External-chain transaction hash or signature. */
+ transactionId: string
+}
+
+/** Chain-neutral lifecycle states exposed by external reserve clients. */
+export enum OutpostReserveStatus {
+ pending = "pending",
+ active = "active",
+ cancelled = "cancelled"
+}
+
+/** Normalized Ethereum ReserveManager record. */
+export interface EthereumReserveRecord {
+ /** Packed external token slug. */
+ tokenCode: bigint
+ /** Packed reserve discriminator slug. */
+ reserveCode: bigint
+ /** Raw external amount committed at creation. */
+ externalTokenAmount: bigint
+ /** Exact WIRE amount requested from the matcher. */
+ requestedWireAmount: bigint
+ /** Connector weight in basis points. */
+ connectorWeightBps: number
+ /** Local outpost lifecycle state. */
+ status: OutpostReserveStatus
+ /** Ethereum creator address. */
+ creator: string
+ /** Whether the record exists. */
+ exists: boolean
+}
+
+/** Solana reserve-creation request for the permissionless outpost path. */
+export interface SolanaReserveCreateRequest extends ReserveCreateDefinition {
+ /** Custody mint for SPL reserves, or a real placeholder SPL mint for native SOL. */
+ mint: PublicKey
+ /** Creator token-account override. Defaults to the canonical ATA. */
+ creatorTokenAccount?: PublicKey
+ /** Add an idempotent ATA instruction when no override is supplied. */
+ ensureCreatorTokenAccount?: boolean
+}
+
+/** Normalized Solana outpost reserve account. */
+export interface SolanaReserveRecord {
+ /** Reserve account PDA. */
+ address: PublicKey
+ /** Reserve vault PDA. */
+ vaultAddress: PublicKey
+ /** Packed external token slug. */
+ tokenCode: bigint
+ /** Packed reserve discriminator slug. */
+ reserveCode: bigint
+ /** Raw external amount committed at creation. */
+ externalTokenAmount: bigint
+ /** Exact WIRE amount requested from the matcher. */
+ requestedWireAmount: bigint
+ /** Connector weight in basis points. */
+ connectorWeightBps: number
+ /** Local outpost lifecycle state. */
+ status: OutpostReserveStatus
+ /** Solana wallet that created and can cancel the reserve. */
+ creator: PublicKey
+ /** UTF-8 reserve display name. */
+ name: string
+ /** UTF-8 reserve description. */
+ description: string
+}
+
+/** Token route configured by the Solana outpost authority. */
+export interface SolanaConfiguredReserveToken {
+ /** Packed token slug used in reserve instructions. */
+ tokenCode: bigint
+ /** Configured SPL mint, or the all-zero native marker. */
+ mint: PublicKey
+ /** Whether the token route represents native SOL. */
+ isNative: boolean
+ /** Chain-side decimal precision used at the depot boundary. */
+ decimals: number
+}
+
+/** Permit signature accepted by Ethereum ReserveManager. */
+export type EthereumReservePermitSignature = ReserveManagerLib.PermitSigStruct
+
+/** Confirmed source-outpost submission used to correlate a swap with Wire. */
+export interface ReserveSwapSubmission {
+ /** External-chain transaction signature or hash. */
+ transactionId: string
+ /** Protocol deposit id copied into `sysio.uwrit::uwreqs.source_tx_id`. */
+ sourceRequestId: bigint
+}
+
+/** Reserve-swap request shared by Ethereum and Solana outposts. */
+export interface ReserveSwapRequest {
+ /** Packed source token slug. */
+ sourceTokenCode: BigNumberish
+ /** Packed source reserve discriminator. */
+ sourceReserveCode: BigNumberish
+ /** Escrowed source amount in external-chain base units. */
+ sourceAmount: BigNumberish
+ /** Packed destination chain slug. */
+ targetChainCode: BigNumberish
+ /** Packed destination token slug. */
+ targetTokenCode: BigNumberish
+ /** Packed destination reserve discriminator or WIRE sentinel. */
+ targetReserveCode: BigNumberish
+ /** Recipient bytes interpreted by the destination chain. */
+ targetRecipient: BytesLike
+ /** Current destination quote in depot units. */
+ targetAmount: BigNumberish
+ /** Maximum accepted quote drift in basis points. */
+ targetToleranceBps: number
+}
+
+/** Classic SPL source details for a Solana reserve swap. */
+export interface SolanaSplReserveSwapRequest extends ReserveSwapRequest {
+ /** SPL mint configured for the source token code. */
+ mint: PublicKey
+ /** Optional source token account; the wallet ATA is used when omitted. */
+ userTokenAccount?: PublicKey
+}
diff --git a/packages/sdk-outpost/src/reserves/Validation.ts b/packages/sdk-outpost/src/reserves/Validation.ts
new file mode 100644
index 0000000..a0c653f
--- /dev/null
+++ b/packages/sdk-outpost/src/reserves/Validation.ts
@@ -0,0 +1,125 @@
+import { getBigInt, getBytes, isBytesLike, toUtf8Bytes } from "ethers"
+
+import type {
+ EthereumReserveCreateRequest,
+ ReserveCreateDefinition,
+ ReserveSwapRequest
+} from "./Types.js"
+
+const MaximumUnsigned64 = 18_446_744_073_709_551_615n,
+ MinimumReserveValue = 1n,
+ MinimumConnectorWeightBps = 1,
+ MaximumConnectorWeightBps = 9999,
+ MinimumToleranceBps = 0,
+ MaximumToleranceBps = 10_000,
+ MaximumReserveNameBytes = 64,
+ MaximumReserveDescriptionBytes = 256,
+ CompressedSecp256k1PublicKeyBytes = 33,
+ CompressedSecp256k1PublicKeyPrefix = {
+ even: 2,
+ odd: 3
+ } as const,
+ InvalidCompressedSecp256k1PublicKeyMessage =
+ "creatorPubKey must be a 33-byte compressed secp256k1 public key."
+
+/** Validate one value against the positive unsigned 64-bit protocol boundary. */
+export function assertReserveUnsigned64(
+ value: ReserveSwapRequest["sourceTokenCode"],
+ field: string
+): bigint {
+ let parsed: bigint
+ try {
+ parsed = getBigInt(value)
+ } catch (error: unknown) {
+ throw new Error(`${field} must be an integer.`, { cause: error })
+ }
+ if (parsed < MinimumReserveValue || parsed > MaximumUnsigned64) {
+ throw new Error(`${field} must be between 1 and uint64 max.`)
+ }
+ return parsed
+}
+
+/** Validate portable reserve-creation fields before opening a wallet prompt. */
+export function assertReserveCreateDefinition(
+ definition: ReserveCreateDefinition
+): void {
+ assertReserveUnsigned64(definition.tokenCode, "tokenCode")
+ assertReserveUnsigned64(definition.reserveCode, "reserveCode")
+
+ let externalTokenAmount: bigint
+ try {
+ externalTokenAmount = getBigInt(definition.externalTokenAmount)
+ } catch (error: unknown) {
+ throw new Error("externalTokenAmount must be an integer.", { cause: error })
+ }
+ if (externalTokenAmount < MinimumReserveValue) {
+ throw new Error("externalTokenAmount must be greater than zero.")
+ }
+
+ assertReserveUnsigned64(definition.requestedWireAmount, "requestedWireAmount")
+ if (
+ !Number.isInteger(definition.connectorWeightBps) ||
+ definition.connectorWeightBps < MinimumConnectorWeightBps ||
+ definition.connectorWeightBps > MaximumConnectorWeightBps
+ ) {
+ throw new Error(
+ `connectorWeightBps must be an integer from ${MinimumConnectorWeightBps} to ${MaximumConnectorWeightBps}.`
+ )
+ }
+
+ const nameBytes = toUtf8Bytes(definition.name).length
+ if (nameBytes === 0 || nameBytes > MaximumReserveNameBytes) {
+ throw new Error(
+ `name must contain 1 to ${MaximumReserveNameBytes} UTF-8 bytes.`
+ )
+ }
+
+ const descriptionBytes = toUtf8Bytes(definition.description).length
+ if (descriptionBytes > MaximumReserveDescriptionBytes) {
+ throw new Error(
+ `description must contain at most ${MaximumReserveDescriptionBytes} UTF-8 bytes.`
+ )
+ }
+}
+
+/** Validate Ethereum-specific reserve creation fields. */
+export function assertEthereumReserveCreateRequest(
+ request: EthereumReserveCreateRequest
+): void {
+ assertReserveCreateDefinition(request)
+ if (!isBytesLike(request.creatorPubKey)) {
+ throw new Error(InvalidCompressedSecp256k1PublicKeyMessage)
+ }
+ const creatorPublicKey = getBytes(request.creatorPubKey)
+ if (
+ creatorPublicKey.length !== CompressedSecp256k1PublicKeyBytes ||
+ (creatorPublicKey[0] !== CompressedSecp256k1PublicKeyPrefix.even &&
+ creatorPublicKey[0] !== CompressedSecp256k1PublicKeyPrefix.odd)
+ ) {
+ throw new Error(InvalidCompressedSecp256k1PublicKeyMessage)
+ }
+}
+
+/** Validate portable reserve-swap fields before opening a wallet prompt. */
+export function assertReserveSwapRequest(request: ReserveSwapRequest): void {
+ assertReserveUnsigned64(request.sourceTokenCode, "sourceTokenCode")
+ assertReserveUnsigned64(request.sourceReserveCode, "sourceReserveCode")
+ assertReserveUnsigned64(request.sourceAmount, "sourceAmount")
+ assertReserveUnsigned64(request.targetChainCode, "targetChainCode")
+ assertReserveUnsigned64(request.targetTokenCode, "targetTokenCode")
+ assertReserveUnsigned64(request.targetReserveCode, "targetReserveCode")
+ assertReserveUnsigned64(request.targetAmount, "targetAmount")
+
+ if (
+ !Number.isInteger(request.targetToleranceBps) ||
+ request.targetToleranceBps < MinimumToleranceBps ||
+ request.targetToleranceBps > MaximumToleranceBps
+ ) {
+ throw new Error(
+ `targetToleranceBps must be between ${MinimumToleranceBps} and ${MaximumToleranceBps}.`
+ )
+ }
+ if (getBytes(request.targetRecipient).length === 0) {
+ throw new Error("targetRecipient is required.")
+ }
+}
diff --git a/packages/sdk-outpost/src/reserves/index.ts b/packages/sdk-outpost/src/reserves/index.ts
new file mode 100644
index 0000000..421334f
--- /dev/null
+++ b/packages/sdk-outpost/src/reserves/index.ts
@@ -0,0 +1,2 @@
+export * from "./Types.js"
+export * from "./Validation.js"
diff --git a/packages/sdk-outpost/src/verification/OutpostDeploymentVerifier.ts b/packages/sdk-outpost/src/verification/OutpostDeploymentVerifier.ts
new file mode 100644
index 0000000..bc0e941
--- /dev/null
+++ b/packages/sdk-outpost/src/verification/OutpostDeploymentVerifier.ts
@@ -0,0 +1,289 @@
+import { PublicKey } from "@solana/web3.js"
+import type { AccountInfo, Connection } from "@solana/web3.js"
+import type { BytesLike, Provider } from "ethers"
+import { dataSlice, getAddress, sha256 as ethersSha256 } from "ethers"
+import { match } from "ts-pattern"
+
+import {
+ CurrentOutpostArtifactSuite,
+ OutpostArtifactRegistry,
+ type OutpostArtifactSuite
+} from "../artifacts/Registry.js"
+import {
+ assertEthereumRuntimeSuiteCompatibility,
+ assertOutpostArtifactSuiteCompatibility,
+ assertSolanaProgramSuiteCompatibility
+} from "../artifacts/SuiteCompatibility.js"
+import {
+ EthereumContractName,
+ OutpostChainFamily,
+ SolanaProgramName,
+ type OutpostDeploymentProfile
+} from "../deployments/index.js"
+import type { OutpostDeploymentVerificationInput } from "./Types.js"
+
+const EmptyEthereumCode = "0x",
+ Eip1967ImplementationStorageSlot =
+ "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
+ Eip1967ImplementationAddressOffset = 12,
+ UpgradeableLoaderStateTagByteLength = 4,
+ SolanaPublicKeyByteLength = 32,
+ UpgradeableLoaderProgramDataAddressEnd =
+ UpgradeableLoaderStateTagByteLength + SolanaPublicKeyByteLength
+
+enum UpgradeableLoaderStateTag {
+ program = 2,
+ programData = 3
+}
+
+/** Canonical Solana upgradeable-loader program identity. */
+export const SolanaUpgradeableLoaderProgramId = new PublicKey(
+ "BPFLoaderUpgradeab1e11111111111111111111111"
+)
+
+function sha256(value: BytesLike): string {
+ return ethersSha256(value).slice(2)
+}
+
+function upgradeableLoaderStateTag(data: Uint8Array): number {
+ if (data.byteLength < UpgradeableLoaderStateTagByteLength) {
+ throw new Error("Solana upgradeable-loader account data is truncated")
+ }
+ return new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(
+ 0,
+ true
+ )
+}
+
+function assertSolanaUpgradeableLoaderAccount(
+ account: AccountInfo,
+ address: string,
+ label: string
+): void {
+ if (!account.owner.equals(SolanaUpgradeableLoaderProgramId)) {
+ throw new Error(
+ `${label} ${address} is not owned by the Solana upgradeable loader`
+ )
+ }
+}
+
+async function verifyEthereum(
+ profile: OutpostDeploymentProfile,
+ provider: Provider,
+ suite: OutpostArtifactSuite
+): Promise {
+ assertOutpostArtifactSuiteCompatibility(
+ profile,
+ OutpostChainFamily.ethereum,
+ suite
+ )
+
+ const network = await provider.getNetwork()
+ if (network.chainId !== BigInt(profile.ethereum.chainId)) {
+ throw new Error(
+ `Ethereum chain mismatch: expected ${profile.ethereum.chainId}, received ${network.chainId}`
+ )
+ }
+
+ await Promise.all(
+ Object.values(EthereumContractName).map(async contractName => {
+ const contract = profile.ethereum.contracts[contractName]
+ if (contract == null) return
+ const proxyCode = await provider.getCode(contract.address)
+
+ if (proxyCode === EmptyEthereumCode) {
+ throw new Error(
+ `Ethereum contract ${contractName} is not deployed at ${contract.address}`
+ )
+ }
+
+ const implementationWord = await provider.getStorage(
+ contract.address,
+ Eip1967ImplementationStorageSlot
+ ),
+ implementationAddress = getAddress(
+ dataSlice(implementationWord, Eip1967ImplementationAddressOffset)
+ )
+
+ if (implementationAddress !== contract.implementationAddress) {
+ throw new Error(
+ `Ethereum ${contractName} implementation mismatch: expected ${contract.implementationAddress}, received ${implementationAddress}`
+ )
+ }
+
+ const implementationCode = await provider.getCode(implementationAddress)
+ if (implementationCode === EmptyEthereumCode) {
+ throw new Error(
+ `Ethereum ${contractName} implementation is not deployed at ${implementationAddress}`
+ )
+ }
+
+ const implementationCodeSha256 = sha256(implementationCode)
+ if (implementationCodeSha256 !== contract.implementationCodeSha256) {
+ throw new Error(
+ `Ethereum ${contractName} implementation code mismatch: expected ${contract.implementationCodeSha256}, received ${implementationCodeSha256}`
+ )
+ }
+ assertEthereumRuntimeSuiteCompatibility(
+ contractName,
+ implementationCode,
+ suite
+ )
+ })
+ )
+}
+
+async function verifySolana(
+ profile: OutpostDeploymentProfile,
+ connection: Connection,
+ suite: OutpostArtifactSuite
+): Promise {
+ assertOutpostArtifactSuiteCompatibility(
+ profile,
+ OutpostChainFamily.solana,
+ suite
+ )
+
+ const genesisHash = await connection.getGenesisHash()
+ if (genesisHash !== profile.solana.genesisHash) {
+ throw new Error(
+ `Solana genesis mismatch: expected ${profile.solana.genesisHash}, received ${genesisHash}`
+ )
+ }
+
+ await Promise.all(
+ Object.values(SolanaProgramName).map(async programName => {
+ const program = profile.solana.programs[programName],
+ programAccount = await connection.getAccountInfo(
+ new PublicKey(program.address)
+ )
+
+ if (programAccount == null || !programAccount.executable) {
+ throw new Error(
+ `Solana program ${programName} is not executable at ${program.address}`
+ )
+ }
+ assertSolanaUpgradeableLoaderAccount(
+ programAccount,
+ program.address,
+ `Solana program ${programName}`
+ )
+
+ const programStateTag = upgradeableLoaderStateTag(programAccount.data)
+ if (programStateTag !== UpgradeableLoaderStateTag.program) {
+ throw new Error(
+ `Solana program ${programName} has invalid upgradeable-loader state ${programStateTag}`
+ )
+ }
+
+ if (
+ programAccount.data.byteLength < UpgradeableLoaderProgramDataAddressEnd
+ ) {
+ throw new Error(
+ `Solana program ${programName} upgradeable-loader data is truncated`
+ )
+ }
+
+ const programDataAddress = new PublicKey(
+ programAccount.data.subarray(
+ UpgradeableLoaderStateTagByteLength,
+ UpgradeableLoaderProgramDataAddressEnd
+ )
+ ).toBase58()
+ if (programDataAddress !== program.programDataAddress) {
+ throw new Error(
+ `Solana ${programName} ProgramData mismatch: expected ${program.programDataAddress}, received ${programDataAddress}`
+ )
+ }
+
+ const programDataAccount = await connection.getAccountInfo(
+ new PublicKey(programDataAddress)
+ )
+ if (programDataAccount == null) {
+ throw new Error(
+ `Solana ${programName} ProgramData is not deployed at ${programDataAddress}`
+ )
+ }
+ assertSolanaUpgradeableLoaderAccount(
+ programDataAccount,
+ programDataAddress,
+ `Solana ${programName} ProgramData`
+ )
+
+ const programDataStateTag = upgradeableLoaderStateTag(
+ programDataAccount.data
+ )
+ if (programDataStateTag !== UpgradeableLoaderStateTag.programData) {
+ throw new Error(
+ `Solana ${programName} ProgramData has invalid upgradeable-loader state ${programDataStateTag}`
+ )
+ }
+
+ const programDataSha256 = sha256(programDataAccount.data)
+ if (programDataSha256 !== program.programDataSha256) {
+ throw new Error(
+ `Solana ${programName} ProgramData mismatch: expected ${program.programDataSha256}, received ${programDataSha256}`
+ )
+ }
+ assertSolanaProgramSuiteCompatibility(
+ programName,
+ programDataAccount.data,
+ suite
+ )
+ })
+ )
+}
+
+/** Verify one candidate artifact suite against its live external chain. */
+async function verifyOutpostArtifactSuite(
+ input: OutpostDeploymentVerificationInput,
+ suite: OutpostArtifactSuite
+): Promise {
+ await match(input)
+ .with({ family: OutpostChainFamily.ethereum }, value =>
+ verifyEthereum(value.profile, value.provider, suite)
+ )
+ .with({ family: OutpostChainFamily.solana }, value =>
+ verifySolana(value.profile, value.connection, suite)
+ )
+ .exhaustive()
+}
+
+/** Verify compatible suites in registration order until one matches live code. */
+async function verifyOutpostArtifactSuiteCandidates(
+ input: OutpostDeploymentVerificationInput,
+ suites: readonly OutpostArtifactSuite[],
+ index = 0
+): Promise {
+ const suite = suites[index]
+ if (suite == null) {
+ assertOutpostArtifactSuiteCompatibility(
+ input.profile,
+ input.family,
+ CurrentOutpostArtifactSuite
+ )
+ throw new Error(
+ `No registered artifact suite matches deployment profile ${input.profile.id}`
+ )
+ }
+
+ try {
+ await verifyOutpostArtifactSuite(input, suite)
+ } catch (error: unknown) {
+ if (index + 1 >= suites.length) throw error
+ await verifyOutpostArtifactSuiteCandidates(input, suites, index + 1)
+ }
+}
+
+/** Cross-chain facade for exact outpost deployment-profile verification. */
+export namespace OutpostDeploymentVerifier {
+ /** Verify chain identity, interface compatibility, and exact live runtime identity. */
+ export async function verify(
+ input: OutpostDeploymentVerificationInput
+ ): Promise {
+ await verifyOutpostArtifactSuiteCandidates(
+ input,
+ OutpostArtifactRegistry.candidates(input.profile)
+ )
+ }
+}
diff --git a/packages/sdk-outpost/src/verification/Types.ts b/packages/sdk-outpost/src/verification/Types.ts
new file mode 100644
index 0000000..2ed775c
--- /dev/null
+++ b/packages/sdk-outpost/src/verification/Types.ts
@@ -0,0 +1,30 @@
+import type { Connection } from "@solana/web3.js"
+import type { Provider } from "ethers"
+
+import type { OutpostDeploymentProfile } from "../deployments/index.js"
+import { OutpostChainFamily } from "../deployments/index.js"
+
+/** Ethereum verification request for an outpost deployment profile. */
+export interface EthereumOutpostDeploymentVerificationInput {
+ /** External-chain family discriminator. */
+ family: OutpostChainFamily.ethereum
+ /** Immutable deployment profile to verify. */
+ profile: OutpostDeploymentProfile
+ /** Ethereum provider connected to the deployed contracts. */
+ provider: Provider
+}
+
+/** Solana verification request for an outpost deployment profile. */
+export interface SolanaOutpostDeploymentVerificationInput {
+ /** External-chain family discriminator. */
+ family: OutpostChainFamily.solana
+ /** Immutable deployment profile to verify. */
+ profile: OutpostDeploymentProfile
+ /** Solana connection targeting the deployed program. */
+ connection: Connection
+}
+
+/** Typed verification input accepted by the cross-chain verifier facade. */
+export type OutpostDeploymentVerificationInput =
+ | EthereumOutpostDeploymentVerificationInput
+ | SolanaOutpostDeploymentVerificationInput
diff --git a/packages/sdk-outpost/src/verification/index.ts b/packages/sdk-outpost/src/verification/index.ts
new file mode 100644
index 0000000..89cba26
--- /dev/null
+++ b/packages/sdk-outpost/src/verification/index.ts
@@ -0,0 +1,2 @@
+export * from "./OutpostDeploymentVerifier.js"
+export * from "./Types.js"
diff --git a/packages/sdk-outpost/tests/Fixtures.ts b/packages/sdk-outpost/tests/Fixtures.ts
new file mode 100644
index 0000000..9e593dc
--- /dev/null
+++ b/packages/sdk-outpost/tests/Fixtures.ts
@@ -0,0 +1,222 @@
+import { AnchorProvider, Wallet } from "@coral-xyz/anchor"
+import { Connection, Keypair, PublicKey } from "@solana/web3.js"
+import { readFileSync } from "node:fs"
+import { createRequire } from "node:module"
+import {
+ getAddress,
+ hexlify,
+ JsonRpcProvider,
+ Network,
+ sha256,
+ toBeHex,
+ zeroPadValue
+} from "ethers"
+
+import {
+ EthereumContractName,
+ OutpostArtifactManifests,
+ type OutpostDeploymentProfile,
+ SolanaProgramName,
+ SolanaUpgradeableLoaderProgramId,
+ parseOutpostDeploymentProfile
+} from "@wireio/sdk-outpost"
+
+const TestHash = "a".repeat(64),
+ TestWireChainId = "c".repeat(64),
+ TestSolanaGenesisHash = "5nBtmutQLrRKBUxNfHJPDjiW5u8id6QM9Hhjg1D1g1XH",
+ TestSolanaProgramAddress = "4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi",
+ TestSolanaProgramDataAddress = "8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR",
+ TestSolanaRpcUrl = "http://example.invalid",
+ UpgradeableLoaderStateTagByteLength = 4,
+ SolanaPublicKeyByteLength = 32,
+ ProgramAccountDataByteLength =
+ UpgradeableLoaderStateTagByteLength + SolanaPublicKeyByteLength,
+ SolanaProgramDataMetadataByteLength = 45,
+ SolanaProgramDataPaddingByteLength = 16,
+ ProgramStateTag = 2,
+ ProgramDataStateTag = 3,
+ TestEthereumProxyAddressBase = 100,
+ TestEthereumImplementationAddressBase = 200,
+ TestEthereumProxyCode = "0x01",
+ PackageRequire = createRequire(__filename),
+ SolanaProgramArtifact =
+ OutpostArtifactManifests.solana.programs[SolanaProgramName.liqsolCore],
+ SolanaProgramBinary = readFileSync(
+ PackageRequire.resolve(
+ `${OutpostArtifactManifests.solana.package.name}/${SolanaProgramArtifact.programBinaryPath}`
+ )
+ )
+
+/** Create one deterministic Ethereum address for a fixture index. */
+function createEthereumAddress(index: number): string {
+ return getAddress(zeroPadValue(toBeHex(index), 20))
+}
+
+/** Create deployment-substituted live code from one producer runtime template. */
+export function createEthereumImplementationCode(
+ contractName: EthereumContractName
+): string {
+ const artifact = OutpostArtifactManifests.ethereum.contracts[contractName],
+ runtimeCode = Buffer.from(
+ readFileSync(
+ PackageRequire.resolve(
+ `${OutpostArtifactManifests.ethereum.package.name}/${artifact.runtimeBytecodePath}`
+ )
+ )
+ )
+
+ artifact.runtimeLinkReferences.forEach(({ start, length }) =>
+ runtimeCode.fill(1, start, start + length)
+ )
+ artifact.runtimeImmutableReferences.forEach(({ start, length }) =>
+ runtimeCode.fill(2, start, start + length)
+ )
+ return hexlify(runtimeCode)
+}
+
+/** Encode the upgradeable-loader Program account for one ProgramData address. */
+export function createSolanaProgramAccountData(
+ programDataAddress: string
+): Buffer {
+ const data = Buffer.alloc(ProgramAccountDataByteLength)
+ data.writeUInt32LE(ProgramStateTag, 0)
+ new PublicKey(programDataAddress)
+ .toBuffer()
+ .copy(data, UpgradeableLoaderStateTagByteLength)
+ return data
+}
+
+/** Encode deterministic upgradeable-loader ProgramData account contents. */
+export function createSolanaProgramDataAccountData(): Buffer {
+ const data = Buffer.alloc(
+ SolanaProgramDataMetadataByteLength +
+ SolanaProgramBinary.length +
+ SolanaProgramDataPaddingByteLength
+ )
+ data.writeUInt32LE(ProgramDataStateTag, 0)
+ data.writeBigUInt64LE(1n, UpgradeableLoaderStateTagByteLength)
+ SolanaProgramBinary.copy(data, SolanaProgramDataMetadataByteLength)
+ return data
+}
+
+/** Create a valid profile aligned with the SDK's source-owned artifacts. */
+export function createOutpostDeploymentProfileFixture(): OutpostDeploymentProfile {
+ const ethereumContracts = Object.fromEntries(
+ Object.values(EthereumContractName).map((contractName, index) => [
+ contractName,
+ {
+ address: createEthereumAddress(TestEthereumProxyAddressBase + index),
+ implementationAddress: createEthereumAddress(
+ TestEthereumImplementationAddressBase + index
+ ),
+ abiSha256:
+ OutpostArtifactManifests.ethereum.contracts[contractName].abiSha256,
+ implementationCodeSha256: sha256(
+ createEthereumImplementationCode(contractName)
+ ).slice(2)
+ }
+ ])
+ ),
+ programData = createSolanaProgramDataAccountData(),
+ profile = {
+ schemaVersion: 1,
+ id: `${TestWireChainId}-${TestHash.slice(0, 12)}`,
+ deploymentChecksum: TestHash,
+ wire: { chainId: TestWireChainId },
+ ethereum: {
+ chainId: 31_337,
+ contracts: ethereumContracts
+ },
+ solana: {
+ genesisHash: TestSolanaGenesisHash,
+ programs: {
+ [SolanaProgramName.liqsolCore]: {
+ address: TestSolanaProgramAddress,
+ programDataAddress: TestSolanaProgramDataAddress,
+ idlSha256:
+ OutpostArtifactManifests.solana.programs[
+ SolanaProgramName.liqsolCore
+ ].idlSha256,
+ programDataSha256: sha256(programData).slice(2)
+ }
+ }
+ }
+ }
+
+ return parseOutpostDeploymentProfile(profile)
+}
+
+/** Create an Ethereum provider aligned with one deployment profile. */
+export function createEthereumProviderFixture(
+ profile: OutpostDeploymentProfile
+): JsonRpcProvider {
+ const provider = new JsonRpcProvider()
+ jest.spyOn(provider, "getNetwork").mockResolvedValue(
+ Network.from({
+ chainId: profile.ethereum.chainId,
+ name: "wire-outpost"
+ })
+ )
+ jest.spyOn(provider, "getCode").mockImplementation(async address => {
+ const implementation = Object.entries(profile.ethereum.contracts).find(
+ ([, deployment]) => deployment?.implementationAddress === address
+ )
+ if (implementation != null) {
+ return createEthereumImplementationCode(
+ implementation[0] as EthereumContractName
+ )
+ }
+ return Object.values(profile.ethereum.contracts).some(
+ deployment => deployment?.address === address
+ )
+ ? TestEthereumProxyCode
+ : "0x"
+ })
+ jest.spyOn(provider, "getStorage").mockImplementation(async address => {
+ const contract = Object.values(profile.ethereum.contracts).find(
+ deployment => deployment?.address === address
+ )
+ return zeroPadValue(
+ contract?.implementationAddress ??
+ profile.ethereum.contracts[EthereumContractName.OPP]
+ .implementationAddress,
+ 32
+ )
+ })
+ return provider
+}
+
+/** Create a Solana provider aligned with one deployment profile. */
+export function createSolanaProviderFixture(
+ profile: OutpostDeploymentProfile
+): AnchorProvider {
+ const connection = new Connection(TestSolanaRpcUrl),
+ provider = new AnchorProvider(connection, new Wallet(Keypair.generate())),
+ program = profile.solana.programs[SolanaProgramName.liqsolCore]
+
+ jest
+ .spyOn(connection, "getGenesisHash")
+ .mockResolvedValue(profile.solana.genesisHash)
+ jest.spyOn(connection, "getAccountInfo").mockImplementation(async address => {
+ if (address.equals(new PublicKey(program.address))) {
+ return {
+ data: createSolanaProgramAccountData(program.programDataAddress),
+ executable: true,
+ lamports: 1,
+ owner: SolanaUpgradeableLoaderProgramId,
+ rentEpoch: 0
+ }
+ }
+ if (address.equals(new PublicKey(program.programDataAddress))) {
+ return {
+ data: createSolanaProgramDataAccountData(),
+ executable: false,
+ lamports: 1,
+ owner: SolanaUpgradeableLoaderProgramId,
+ rentEpoch: 0
+ }
+ }
+ return null
+ })
+ return provider
+}
diff --git a/packages/sdk-outpost/tests/assets/Artifacts.test.ts b/packages/sdk-outpost/tests/assets/Artifacts.test.ts
new file mode 100644
index 0000000..1b5ed69
--- /dev/null
+++ b/packages/sdk-outpost/tests/assets/Artifacts.test.ts
@@ -0,0 +1,165 @@
+import type { Program } from "@coral-xyz/anchor"
+import {
+ BAR__factory,
+ IERC1155__factory,
+ OPP__factory,
+ OperatorRegistry__factory,
+ ReserveManager__factory
+} from "@wireio/outpost-ethereum-artifacts"
+import {
+ liqsolCoreIdl,
+ type LiqsolCore
+} from "@wireio/outpost-solana-artifacts"
+import { createHash } from "node:crypto"
+import { readFileSync } from "node:fs"
+import { createRequire } from "node:module"
+
+import {
+ EthereumContractName,
+ OutpostArtifactManifests,
+ OutpostChainFamily,
+ SolanaProgramName,
+ assertOutpostArtifactCompatibility
+} from "@wireio/sdk-outpost"
+
+import { createOutpostDeploymentProfileFixture } from "../Fixtures.js"
+
+const PackageRequire = createRequire(__filename)
+
+/** Return the SHA-256 digest for one installed producer-package file. */
+function installedPackageFileSha256(
+ packageName: string,
+ packagePath: string
+): string {
+ return createHash("sha256")
+ .update(
+ readFileSync(PackageRequire.resolve(`${packageName}/${packagePath}`))
+ )
+ .digest("hex")
+}
+
+describe("source-owned outpost artifacts", () => {
+ it("records exact producer package identity", () => {
+ expect(OutpostArtifactManifests.ethereum.package.name).toBe(
+ "@wireio/outpost-ethereum-artifacts"
+ )
+ expect(OutpostArtifactManifests.solana.package.name).toBe(
+ "@wireio/outpost-solana-artifacts"
+ )
+ expect(OutpostArtifactManifests.ethereum.package.version).toBe("0.3.0")
+ expect(OutpostArtifactManifests.solana.package.version).toBe("0.3.0")
+ expect(OutpostArtifactManifests.ethereum.source.revision).toBe(
+ "b90035b48414267d1b3ca183b88e2118a8c5b16e"
+ )
+ expect(OutpostArtifactManifests.solana.source.revision).toBe(
+ "217c4d6909cb658cd1bf3bcda570400947ed2893"
+ )
+ })
+
+ it("ships checksum-valid Ethereum runtimes and Solana program inputs", () => {
+ Object.values(OutpostArtifactManifests.ethereum.contracts).forEach(
+ artifact => {
+ expect(
+ installedPackageFileSha256(
+ OutpostArtifactManifests.ethereum.package.name,
+ artifact.runtimeBytecodePath
+ )
+ ).toBe(artifact.runtimeBytecodeSha256)
+ }
+ )
+
+ const solanaProgram =
+ OutpostArtifactManifests.solana.programs[SolanaProgramName.liqsolCore]
+ expect(
+ installedPackageFileSha256(
+ OutpostArtifactManifests.solana.package.name,
+ solanaProgram.idlPath
+ )
+ ).toBe(solanaProgram.idlSha256)
+ expect(
+ installedPackageFileSha256(
+ OutpostArtifactManifests.solana.package.name,
+ solanaProgram.programBinaryPath
+ )
+ ).toBe(solanaProgram.programBinarySha256)
+ })
+
+ it.each(Object.values(OutpostChainFamily))(
+ "accepts a runtime deployment aligned with %s artifacts",
+ family => {
+ expect(() =>
+ assertOutpostArtifactCompatibility(
+ createOutpostDeploymentProfileFixture(),
+ family
+ )
+ ).not.toThrow()
+ }
+ )
+
+ it("rejects a runtime deployment with an incompatible Ethereum ABI", () => {
+ const profile = createOutpostDeploymentProfileFixture()
+ profile.ethereum.contracts[EthereumContractName.ReserveManager].abiSha256 =
+ "f".repeat(64)
+
+ expect(() =>
+ assertOutpostArtifactCompatibility(profile, OutpostChainFamily.ethereum)
+ ).toThrow("Ethereum ReserveManager ABI interface mismatch")
+ })
+
+ it("rejects a runtime deployment with an incompatible Solana IDL", () => {
+ const profile = createOutpostDeploymentProfileFixture()
+ profile.solana.programs[SolanaProgramName.liqsolCore].idlSha256 =
+ "f".repeat(64)
+
+ expect(() =>
+ assertOutpostArtifactCompatibility(profile, OutpostChainFamily.solana)
+ ).toThrow("Solana liqsolCore IDL interface mismatch")
+ })
+
+ it("generates the callable swap and collateral surfaces", () => {
+ const accountNames: Array["account"]> = [
+ "outpostConfig",
+ "reserve"
+ ]
+ expect(OPP__factory.abi.length).toBeGreaterThan(0)
+ expect(
+ OPP__factory.createInterface().getFunction("addAttestation")
+ ).toBeDefined()
+ expect(
+ ReserveManager__factory.createInterface().getFunction("requestSwap")
+ ).toBeDefined()
+ expect(
+ ReserveManager__factory.createInterface().getFunction(
+ "requestSwapErc20WithApproval"
+ )
+ ).toBeDefined()
+ expect(
+ OperatorRegistry__factory.createInterface().getFunction("deposit")
+ ).toBeDefined()
+ expect(
+ OperatorRegistry__factory.createInterface().getFunction("commit")
+ ).toBeDefined()
+ expect(
+ BAR__factory.createInterface().getFunction("wireNodesContract")
+ ).toBeDefined()
+ expect(
+ BAR__factory.createInterface().getFunction("commitNode")
+ ).toBeDefined()
+ expect(
+ IERC1155__factory.createInterface().getFunction("balanceOf")
+ ).toBeDefined()
+ expect(
+ IERC1155__factory.createInterface().getFunction("setApprovalForAll")
+ ).toBeDefined()
+ expect(
+ liqsolCoreIdl.instructions.map(instruction => instruction.name)
+ ).toEqual(
+ expect.arrayContaining([
+ "requestSwap",
+ "requestSwapSpl",
+ "commitUnderwrite"
+ ])
+ )
+ expect(accountNames).toEqual(["outpostConfig", "reserve"])
+ })
+})
diff --git a/packages/sdk-outpost/tests/assets/Registry.test.ts b/packages/sdk-outpost/tests/assets/Registry.test.ts
new file mode 100644
index 0000000..b3f0024
--- /dev/null
+++ b/packages/sdk-outpost/tests/assets/Registry.test.ts
@@ -0,0 +1,48 @@
+import { ReserveManager__factory } from "@wireio/outpost-ethereum-artifacts"
+import { liqsolCoreIdl } from "@wireio/outpost-solana-artifacts"
+
+import {
+ CurrentOutpostArtifactSuite,
+ OutpostArtifactRegistry
+} from "@wireio/sdk-outpost/artifacts/Registry"
+import { EthereumContractName, SolanaProgramName } from "@wireio/sdk-outpost"
+
+import { createOutpostDeploymentProfileFixture } from "../Fixtures.js"
+
+const MismatchedInterfaceDigest = "f".repeat(64)
+
+describe("OutpostArtifactRegistry", () => {
+ it("resolves the exact installed Ethereum and Solana package pair", () => {
+ const suite = OutpostArtifactRegistry.resolve(
+ createOutpostDeploymentProfileFixture()
+ )
+
+ expect(suite).toBe(CurrentOutpostArtifactSuite)
+ expect(suite.ethereum.factories[EthereumContractName.ReserveManager]).toBe(
+ ReserveManager__factory
+ )
+ expect(suite.solana.idls[SolanaProgramName.liqsolCore]).toBe(liqsolCoreIdl)
+ })
+
+ it("rejects an unregistered Ethereum interface", () => {
+ const profile = createOutpostDeploymentProfileFixture()
+ profile.ethereum.contracts[EthereumContractName.ReserveManager].abiSha256 =
+ MismatchedInterfaceDigest
+
+ expect(OutpostArtifactRegistry.candidates(profile)).toEqual([])
+ expect(() => OutpostArtifactRegistry.resolve(profile)).toThrow(
+ "No registered artifact suite matches deployment profile"
+ )
+ })
+
+ it("rejects an unregistered Solana interface", () => {
+ const profile = createOutpostDeploymentProfileFixture()
+ profile.solana.programs[SolanaProgramName.liqsolCore].idlSha256 =
+ MismatchedInterfaceDigest
+
+ expect(OutpostArtifactRegistry.candidates(profile)).toEqual([])
+ expect(() => OutpostArtifactRegistry.resolve(profile)).toThrow(
+ "No registered artifact suite matches deployment profile"
+ )
+ })
+})
diff --git a/packages/sdk-outpost/tests/clients/OutpostClient.test.ts b/packages/sdk-outpost/tests/clients/OutpostClient.test.ts
new file mode 100644
index 0000000..7c73b14
--- /dev/null
+++ b/packages/sdk-outpost/tests/clients/OutpostClient.test.ts
@@ -0,0 +1,43 @@
+import {
+ OutpostChainFamily,
+ OutpostClient,
+ type EthereumOutpostClient,
+ type SolanaOutpostClient
+} from "@wireio/sdk-outpost"
+import {
+ createEthereumProviderFixture,
+ createOutpostDeploymentProfileFixture,
+ createSolanaProviderFixture
+} from "../Fixtures.js"
+
+describe("OutpostClient", () => {
+ it("preserves the precise Ethereum client type", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ client = await OutpostClient.create({
+ family: OutpostChainFamily.ethereum,
+ options: {
+ profile,
+ connection: createEthereumProviderFixture(profile)
+ }
+ }),
+ typedClient: EthereumOutpostClient = client
+
+ expect(typedClient.profile).toBe(profile)
+ expect(typedClient.reserves).toBeDefined()
+ })
+
+ it("preserves the precise Solana client type", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ client = await OutpostClient.create({
+ family: OutpostChainFamily.solana,
+ options: {
+ profile,
+ provider: createSolanaProviderFixture(profile)
+ }
+ }),
+ typedClient: SolanaOutpostClient = client
+
+ expect(typedClient.profile).toBe(profile)
+ expect(typedClient.reserves).toBeDefined()
+ })
+})
diff --git a/packages/sdk-outpost/tests/clients/ethereum/EthereumNodeOwnerClient.test.ts b/packages/sdk-outpost/tests/clients/ethereum/EthereumNodeOwnerClient.test.ts
new file mode 100644
index 0000000..7fa806c
--- /dev/null
+++ b/packages/sdk-outpost/tests/clients/ethereum/EthereumNodeOwnerClient.test.ts
@@ -0,0 +1,188 @@
+import type { BAR, IERC1155 } from "@wireio/outpost-ethereum-artifacts"
+import { IERC1155__factory } from "@wireio/outpost-ethereum-artifacts"
+import { KeyType, Name, PublicKey } from "@wireio/sdk-core"
+import {
+ JsonRpcProvider,
+ Wallet,
+ ZeroAddress,
+ getBytes,
+ type EventLog,
+ type TransactionReceipt,
+ type TransactionResponse
+} from "ethers"
+
+import {
+ EthereumNodeOwnerClient,
+ EthereumNodeOwnerTier
+} from "@wireio/sdk-outpost"
+
+const BarAddress = "0x18E317A7D70d8fBf8e6E893616b52390EbBdb629",
+ TokenContractAddress = "0x5c74c94173F05dA1720953407cbb920F3DF9f887",
+ CommitTransactionHash = `0x${"11".repeat(32)}`,
+ ApprovalTransactionHash = `0x${"22".repeat(32)}`,
+ TestPrivateKey = `0x${"33".repeat(32)}`,
+ WireAccountName = "nodeowner",
+ WireAccount = Name.from(WireAccountName),
+ TransactionReceiptFixture = {
+ logs: [],
+ status: 1
+ } as unknown as TransactionReceipt
+
+/** Create a confirmed transaction fixture with optional parsed logs. */
+function transactionFixture(
+ hash: string,
+ logs: readonly EventLog[] = []
+): TransactionResponse {
+ return {
+ hash,
+ wait: jest.fn(async () => ({ ...TransactionReceiptFixture, logs }))
+ } as unknown as TransactionResponse
+}
+
+/** Create generated BAR and IERC-1155 fixtures for one node-owner flow. */
+function contractFixtures(
+ approved = true,
+ tokenContractAddress = TokenContractAddress
+) {
+ const wallet = new Wallet(TestPrivateKey),
+ committedEvent = {
+ eventName: "NodeCommitted",
+ args: [
+ wallet.address,
+ BigInt(EthereumNodeOwnerTier.T2),
+ TokenContractAddress,
+ WireAccountName
+ ]
+ } as unknown as EventLog,
+ commitTransaction = transactionFixture(CommitTransactionHash, [
+ committedEvent
+ ]),
+ approvalTransaction = transactionFixture(ApprovalTransactionHash),
+ bar = {
+ target: BarAddress,
+ getAddress: jest.fn(async () => BarAddress),
+ wireNodesContract: jest.fn(async () => tokenContractAddress),
+ commitNode: jest.fn(async () => commitTransaction)
+ } as unknown as BAR,
+ token = {
+ balanceOf: jest.fn(async (_owner: string, tokenId: number) =>
+ BigInt(tokenId === EthereumNodeOwnerTier.T2 ? 1 : 0)
+ ),
+ isApprovedForAll: jest.fn(async () => approved),
+ setApprovalForAll: jest.fn(async () => approvalTransaction)
+ } as unknown as IERC1155
+
+ jest.spyOn(IERC1155__factory, "connect").mockReturnValue(token)
+ return { wallet, bar, token, commitTransaction, approvalTransaction }
+}
+
+/** Return an sdk-core public-key input aligned with the EVM test signer. */
+function wirePublicKey(wallet: Wallet): PublicKey {
+ return PublicKey.from({
+ type: KeyType.K1,
+ compressed: getBytes(wallet.signingKey.compressedPublicKey)
+ })
+}
+
+afterEach(() => jest.restoreAllMocks())
+
+describe("EthereumNodeOwnerClient", () => {
+ it("reads only owned tiers from BAR's canonical WireNodes contract", async () => {
+ const { wallet, bar, token } = contractFixtures(),
+ client = new EthereumNodeOwnerClient(bar, wallet)
+
+ await expect(client.ownedSlots(wallet.address)).resolves.toEqual([
+ {
+ tokenId: EthereumNodeOwnerTier.T2,
+ balance: 1n,
+ tokenContractAddress: TokenContractAddress
+ }
+ ])
+ expect(token.balanceOf).toHaveBeenCalledTimes(3)
+ })
+
+ it("commits through BAR without an unnecessary approval", async () => {
+ const { wallet, bar, token, commitTransaction } = contractFixtures(),
+ client = new EthereumNodeOwnerClient(bar, wallet),
+ submission = await client.commit({
+ tokenId: EthereumNodeOwnerTier.T2,
+ wireAccountName: WireAccount,
+ wirePublicKey: wirePublicKey(wallet),
+ depositorPublicKey: wallet.signingKey.publicKey
+ })
+
+ expect(submission).toEqual({
+ transactionId: CommitTransactionHash,
+ approvalTransactionId: undefined,
+ committed: {
+ owner: wallet.address,
+ tokenId: EthereumNodeOwnerTier.T2,
+ tokenContractAddress: TokenContractAddress,
+ wireAccountName: WireAccountName
+ }
+ })
+ expect(token.setApprovalForAll).not.toHaveBeenCalled()
+ expect(bar.commitNode).toHaveBeenCalledWith(
+ EthereumNodeOwnerTier.T2,
+ WireAccountName,
+ expect.objectContaining({ keyType: 1 }),
+ getBytes(wallet.signingKey.publicKey)
+ )
+ expect(commitTransaction.wait).toHaveBeenCalledWith(1)
+ })
+
+ it("confirms ERC-1155 approval before committing when required", async () => {
+ const { wallet, bar, token, approvalTransaction } = contractFixtures(false),
+ client = new EthereumNodeOwnerClient(bar, wallet)
+
+ await expect(
+ client.commit({
+ tokenId: EthereumNodeOwnerTier.T2,
+ wireAccountName: WireAccount,
+ wirePublicKey: wirePublicKey(wallet),
+ depositorPublicKey: wallet.signingKey.publicKey
+ })
+ ).resolves.toEqual(
+ expect.objectContaining({
+ approvalTransactionId: ApprovalTransactionHash
+ })
+ )
+ expect(token.setApprovalForAll).toHaveBeenCalledWith(BarAddress, true)
+ expect(approvalTransaction.wait).toHaveBeenCalledWith(1)
+ })
+
+ it("rejects provider-only writes and depositor keys from another signer", async () => {
+ const { wallet, bar } = contractFixtures(),
+ providerClient = new EthereumNodeOwnerClient(
+ bar,
+ new JsonRpcProvider()
+ ),
+ signerClient = new EthereumNodeOwnerClient(bar, wallet),
+ otherWallet = Wallet.createRandom(),
+ request = {
+ tokenId: EthereumNodeOwnerTier.T2,
+ wireAccountName: WireAccount,
+ wirePublicKey: wirePublicKey(wallet),
+ depositorPublicKey: wallet.signingKey.publicKey
+ }
+
+ await expect(providerClient.commit(request)).rejects.toThrow(
+ "requires a connected signer"
+ )
+ await expect(
+ signerClient.commit({
+ ...request,
+ depositorPublicKey: otherWallet.signingKey.publicKey
+ })
+ ).rejects.toThrow("does not belong to the EVM signer")
+ expect(bar.commitNode).not.toHaveBeenCalled()
+ })
+
+ it("fails closed when BAR has no canonical WireNodes contract", async () => {
+ const { wallet, bar } = contractFixtures(true, ZeroAddress)
+
+ await expect(
+ new EthereumNodeOwnerClient(bar, wallet).canonicalTokenContractAddress()
+ ).rejects.toThrow("no canonical WireNodes contract")
+ })
+})
diff --git a/packages/sdk-outpost/tests/clients/ethereum/EthereumOutpostClient.test.ts b/packages/sdk-outpost/tests/clients/ethereum/EthereumOutpostClient.test.ts
new file mode 100644
index 0000000..7ee3a83
--- /dev/null
+++ b/packages/sdk-outpost/tests/clients/ethereum/EthereumOutpostClient.test.ts
@@ -0,0 +1,224 @@
+import {
+ getBytes,
+ hexlify,
+ Network,
+ sha256,
+ Wallet,
+ zeroPadValue,
+ type EventLog
+} from "ethers"
+
+import {
+ EthereumContractName,
+ EthereumReserveClient,
+ EthereumReserveSwapClient,
+ OutpostChainFamily,
+ OutpostClient,
+ type EthereumOutpostClient,
+ type EthereumOutpostClientOptions,
+ type ReserveSwapRequest
+} from "@wireio/sdk-outpost"
+import {
+ createEthereumImplementationCode,
+ createEthereumProviderFixture,
+ createOutpostDeploymentProfileFixture
+} from "../../Fixtures.js"
+
+/** Create the Ethereum client through the package's only public facade. */
+function createEthereumClient(
+ options: EthereumOutpostClientOptions
+): Promise {
+ return OutpostClient.create({
+ family: OutpostChainFamily.ethereum,
+ options
+ })
+}
+
+describe("EthereumOutpostClient", () => {
+ it("submits native reserve swaps with estimated gas headroom", async () => {
+ const request: ReserveSwapRequest = {
+ sourceTokenCode: 1,
+ sourceReserveCode: 2,
+ sourceAmount: 3,
+ targetChainCode: 4,
+ targetTokenCode: 5,
+ targetReserveCode: 6,
+ targetRecipient: new Uint8Array([7]),
+ targetAmount: 8,
+ targetToleranceBps: 500
+ },
+ wait = jest.fn().mockResolvedValue({
+ logs: [{ eventName: "SwapDeposit", args: [42n] } as unknown as EventLog]
+ }),
+ requestSwap = Object.assign(
+ jest.fn().mockResolvedValue({ hash: "0xabc", wait }),
+ {
+ staticCall: jest.fn().mockResolvedValue(null),
+ estimateGas: jest.fn().mockResolvedValue(100_000n)
+ }
+ ),
+ reserveManager = {
+ requestSwap
+ } as unknown as ConstructorParameters<
+ typeof EthereumReserveSwapClient
+ >[0],
+ client = new EthereumReserveSwapClient(
+ reserveManager,
+ Wallet.createRandom()
+ )
+
+ await expect(client.requestNative(request)).resolves.toEqual({
+ transactionId: "0xabc",
+ sourceRequestId: 42n
+ })
+ expect(requestSwap).toHaveBeenCalledWith(
+ request.sourceTokenCode,
+ request.sourceReserveCode,
+ request.targetChainCode,
+ request.targetTokenCode,
+ request.targetReserveCode,
+ request.targetRecipient,
+ request.targetAmount,
+ request.targetToleranceBps,
+ {
+ value: request.sourceAmount,
+ gasLimit: 125_000n
+ }
+ )
+ expect(wait).toHaveBeenCalledWith(1)
+ })
+
+ it("adds 25% gas headroom to reserve swap submissions", () => {
+ expect(EthereumReserveSwapClient.addSubmissionGasHeadroom(789_767)).toEqual(
+ 987_209n
+ )
+ expect(EthereumReserveSwapClient.addSubmissionGasHeadroom(1)).toEqual(2n)
+ })
+
+ it("resets nonzero ERC-20 allowances before increasing them", () => {
+ expect(EthereumReserveSwapClient.approvalAmounts(2, 3)).toEqual([0n, 3n])
+ expect(EthereumReserveSwapClient.approvalAmounts(0, 3)).toEqual([3n])
+ expect(EthereumReserveSwapClient.approvalAmounts(3, 3)).toEqual([])
+ })
+
+ it("verifies a profile and returns a generated contract type", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createEthereumProviderFixture(profile),
+ client = await createEthereumClient({
+ profile,
+ connection: provider
+ }),
+ reserveManager = client.contract(EthereumContractName.ReserveManager)
+
+ expect(reserveManager.target).toBe(
+ profile.ethereum.contracts[EthereumContractName.ReserveManager].address
+ )
+ expect(client.reserves).toBeInstanceOf(EthereumReserveClient)
+ expect(client.swaps).toBeInstanceOf(EthereumReserveSwapClient)
+ expect(provider.getCode).toHaveBeenCalledTimes(
+ Object.values(EthereumContractName).length * 2
+ )
+ expect(provider.getStorage).toHaveBeenCalledTimes(
+ Object.values(EthereumContractName).length
+ )
+ })
+
+ it("keeps existing Ethereum clients usable when BAR is not deployed", async () => {
+ const profile = createOutpostDeploymentProfileFixture()
+ delete profile.ethereum.contracts[EthereumContractName.BAR]
+ const provider = createEthereumProviderFixture(profile),
+ client = await createEthereumClient({
+ profile,
+ connection: provider
+ })
+
+ expect(client.reserves).toBeInstanceOf(EthereumReserveClient)
+ expect(client.swaps).toBeInstanceOf(EthereumReserveSwapClient)
+ expect(() => client.nodeOwners).toThrow("has no BAR identity")
+ expect(provider.getCode).toHaveBeenCalledTimes(
+ (Object.values(EthereumContractName).length - 1) * 2
+ )
+ })
+
+ it("parses the protocol deposit id from a confirmed receipt", () => {
+ const events = [
+ { eventName: "SwapDeposit", args: [42n] } as unknown as EventLog
+ ]
+
+ expect(EthereumReserveSwapClient.parseSourceRequestId(events)).toBe(42n)
+ expect(() => EthereumReserveSwapClient.parseSourceRequestId([])).toThrow(
+ "did not emit SwapDeposit"
+ )
+ })
+
+ it("rejects the wrong Ethereum chain", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createEthereumProviderFixture(profile)
+ jest
+ .spyOn(provider, "getNetwork")
+ .mockResolvedValue(Network.from({ chainId: 1, name: "mainnet" }))
+
+ await expect(
+ createEthereumClient({ profile, connection: provider })
+ ).rejects.toThrow("Ethereum chain mismatch")
+ })
+
+ it("rejects a configured proxy without bytecode", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createEthereumProviderFixture(profile)
+ jest.spyOn(provider, "getCode").mockResolvedValue("0x")
+
+ await expect(
+ createEthereumClient({ profile, connection: provider })
+ ).rejects.toThrow("is not deployed")
+ })
+
+ it("rejects an implementation address mismatch", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createEthereumProviderFixture(profile)
+ jest
+ .spyOn(provider, "getStorage")
+ .mockResolvedValue(zeroPadValue("0x01", 32))
+
+ await expect(
+ createEthereumClient({ profile, connection: provider })
+ ).rejects.toThrow("implementation mismatch")
+ })
+
+ it("rejects an implementation code mismatch", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createEthereumProviderFixture(profile)
+ profile.ethereum.contracts[
+ EthereumContractName.ReserveManager
+ ].implementationCodeSha256 = "f".repeat(64)
+
+ await expect(
+ createEthereumClient({ profile, connection: provider })
+ ).rejects.toThrow("implementation code mismatch")
+ })
+
+ it("rejects live code from another producer runtime", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ contract = profile.ethereum.contracts[EthereumContractName.OPP],
+ incompatibleCodeBytes = getBytes(
+ createEthereumImplementationCode(EthereumContractName.OPP)
+ )
+ incompatibleCodeBytes[0] ^= 1
+ const incompatibleCode = hexlify(incompatibleCodeBytes),
+ incompatibleCodeSha256 = sha256(incompatibleCode).slice(2)
+ contract.implementationCodeSha256 = incompatibleCodeSha256
+ const provider = createEthereumProviderFixture(profile),
+ getCode = (provider.getCode as jest.Mock).getMockImplementation()
+ jest
+ .spyOn(provider, "getCode")
+ .mockImplementation(async address =>
+ address === contract.implementationAddress
+ ? incompatibleCode
+ : getCode(address)
+ )
+
+ await expect(
+ createEthereumClient({ profile, connection: provider })
+ ).rejects.toThrow("artifact runtime")
+ })
+})
diff --git a/packages/sdk-outpost/tests/clients/ethereum/EthereumReserveClient.test.ts b/packages/sdk-outpost/tests/clients/ethereum/EthereumReserveClient.test.ts
new file mode 100644
index 0000000..c63a11d
--- /dev/null
+++ b/packages/sdk-outpost/tests/clients/ethereum/EthereumReserveClient.test.ts
@@ -0,0 +1,245 @@
+import type { ReserveManager } from "@wireio/outpost-ethereum-artifacts"
+import {
+ AbiCoder,
+ AbstractSigner,
+ JsonRpcProvider,
+ Wallet,
+ ZeroAddress,
+ ZeroHash,
+ type Provider,
+ type TransactionReceipt,
+ type TransactionRequest,
+ type TransactionResponse,
+ type TypedDataDomain,
+ type TypedDataField
+} from "ethers"
+
+import {
+ EthereumReserveClient,
+ OutpostReserveStatus,
+ type EthereumReserveCreateRequest
+} from "@wireio/sdk-outpost"
+
+const ReserveTransactionHash = `0x${"11".repeat(32)}`,
+ ApprovalTransactionHash = `0x${"22".repeat(32)}`,
+ ReserveManagerAddress = "0x18E317A7D70d8fBf8e6E893616b52390EbBdb629",
+ TokenAddress = "0x5c74c94173F05dA1720953407cbb920F3DF9f887",
+ CreatorAddress = "0x7412BC256355ABD22dD53De3a38E8995b5d4c1D1",
+ TransactionReceiptFixture = {
+ logs: [],
+ status: 1
+ } as unknown as TransactionReceipt
+
+const request: EthereumReserveCreateRequest = {
+ tokenCode: 1,
+ reserveCode: 2,
+ externalTokenAmount: 3,
+ requestedWireAmount: 4,
+ connectorWeightBps: 5_000,
+ name: "Private ETH reserve",
+ description: "Same-owner external routing",
+ isPrivate: true,
+ creatorPubKey: `0x02${"11".repeat(32)}`
+}
+
+/** Create one v6 transaction response fixture. */
+function transactionFixture(
+ hash = ReserveTransactionHash,
+ wait: TransactionResponse["wait"] = jest.fn(
+ async (): Promise => TransactionReceiptFixture
+ )
+): TransactionResponse {
+ return { hash, wait } as unknown as TransactionResponse
+}
+
+/** Create one callable v6 contract method with a static preflight. */
+function contractMethodFixture(transaction: TransactionResponse) {
+ return Object.assign(
+ jest.fn(async () => transaction),
+ {
+ staticCall: jest.fn(async (): Promise => undefined)
+ }
+ )
+}
+
+/** Create the ReserveManager contract surface exercised by this client. */
+function reserveManagerFixture(configuredTokenAddress = TokenAddress) {
+ const transaction = transactionFixture(),
+ createReserve = contractMethodFixture(transaction),
+ createErc20WithApproval = contractMethodFixture(transaction),
+ createErc20WithPermit = contractMethodFixture(transaction),
+ reserveManager = {
+ target: ReserveManagerAddress,
+ getAddress: jest.fn(async () => ReserveManagerAddress),
+ create_reserve: createReserve,
+ requestReserveCreateErc20WithApproval: createErc20WithApproval,
+ requestReserveCreateErc20WithPermit: createErc20WithPermit,
+ cancel_create_reserve: jest.fn(async () => transaction),
+ tokenAddressesByCode: jest.fn(async () => configuredTokenAddress),
+ getReserve: jest.fn(async () => ({
+ tokenCode: 1n,
+ reserveCode: 2n,
+ externalTokenAmount: 3n,
+ requestedWireAmount: 4n,
+ connectorWeightBps: 5_000n,
+ status: 1n,
+ creator: CreatorAddress,
+ exists: true
+ }))
+ } as unknown as ReserveManager
+
+ return { reserveManager, transaction }
+}
+
+/** Create a static v6 provider that confirms the mocked ERC-20 transaction. */
+function erc20Provider(): JsonRpcProvider {
+ const provider = new JsonRpcProvider(undefined, 31_337, {
+ staticNetwork: true
+ })
+ jest
+ .spyOn(provider, "getTransactionReceipt")
+ .mockResolvedValue(TransactionReceiptFixture)
+ return provider
+}
+
+/** Minimal ethers v6 signer used to observe ERC-20 calls and submissions. */
+class Erc20Signer extends AbstractSigner {
+ readonly approval = transactionFixture(ApprovalTransactionHash)
+
+ constructor(provider: Provider = erc20Provider()) {
+ super(provider)
+ }
+
+ async getAddress(): Promise {
+ return CreatorAddress
+ }
+
+ async signMessage(): Promise {
+ return "0x"
+ }
+
+ async signTransaction(): Promise {
+ return "0x"
+ }
+
+ async signTypedData(
+ _domain: TypedDataDomain,
+ _types: Record>,
+ _value: Record
+ ): Promise {
+ return "0x"
+ }
+
+ connect(provider: Provider): Erc20Signer {
+ return new Erc20Signer(provider)
+ }
+
+ async call(): Promise {
+ return AbiCoder.defaultAbiCoder().encode(["uint256"], [0])
+ }
+
+ async sendTransaction(
+ _transaction: TransactionRequest
+ ): Promise {
+ return this.approval
+ }
+}
+
+describe("EthereumReserveClient", () => {
+ it("creates a native pending reserve after static preflight", async () => {
+ const { reserveManager, transaction } = reserveManagerFixture(),
+ client = new EthereumReserveClient(reserveManager, Wallet.createRandom())
+
+ await expect(client.createNative(request)).resolves.toEqual({
+ transactionId: ReserveTransactionHash
+ })
+ expect(reserveManager.create_reserve.staticCall).toHaveBeenCalledTimes(1)
+ expect(reserveManager.create_reserve).toHaveBeenCalledTimes(1)
+ expect(transaction.wait).toHaveBeenCalledWith(1)
+ })
+
+ it("approves an ERC-20 before creating its pending reserve", async () => {
+ const { reserveManager } = reserveManagerFixture(),
+ signer = new Erc20Signer(),
+ client = new EthereumReserveClient(reserveManager, signer)
+
+ const submission = await client.createErc20WithApproval(
+ request,
+ TokenAddress
+ )
+ expect(submission).toEqual({ transactionId: ReserveTransactionHash })
+ expect(signer.provider.getTransactionReceipt).toHaveBeenCalledWith(
+ ApprovalTransactionHash
+ )
+ expect(
+ reserveManager.requestReserveCreateErc20WithApproval
+ ).toHaveBeenCalledTimes(1)
+ })
+
+ it("creates an ERC-20 reserve with a supplied permit", async () => {
+ const { reserveManager } = reserveManagerFixture(),
+ client = new EthereumReserveClient(reserveManager, Wallet.createRandom())
+
+ await expect(
+ client.createErc20WithPermit(request, {
+ deadline: 100,
+ v: 27,
+ r: ZeroHash,
+ s: ZeroHash
+ })
+ ).resolves.toEqual({ transactionId: ReserveTransactionHash })
+ expect(
+ reserveManager.requestReserveCreateErc20WithPermit.staticCall
+ ).toHaveBeenCalledTimes(1)
+ })
+
+ it("cancels a pending reserve and normalizes active local state", async () => {
+ const { reserveManager } = reserveManagerFixture(),
+ client = new EthereumReserveClient(reserveManager, Wallet.createRandom())
+
+ await expect(
+ client.cancel({ tokenCode: 1, reserveCode: 2 })
+ ).resolves.toEqual({ transactionId: ReserveTransactionHash })
+ await expect(client.get({ tokenCode: 1, reserveCode: 2 })).resolves.toEqual(
+ expect.objectContaining({
+ tokenCode: 1n,
+ reserveCode: 2n,
+ status: OutpostReserveStatus.active,
+ creator: CreatorAddress,
+ exists: true
+ })
+ )
+ })
+
+ it("requires a signer and a configured ERC-20 route", async () => {
+ const { reserveManager } = reserveManagerFixture(),
+ provider = new JsonRpcProvider(),
+ providerClient = new EthereumReserveClient(reserveManager, provider)
+
+ await expect(providerClient.createNative(request)).rejects.toThrow(
+ "requires a connected signer"
+ )
+
+ const { reserveManager: unconfiguredReserveManager } =
+ reserveManagerFixture(ZeroAddress),
+ signerClient = new EthereumReserveClient(
+ unconfiguredReserveManager,
+ new Erc20Signer()
+ )
+ await expect(signerClient.createErc20WithApproval(request)).rejects.toThrow(
+ "No ERC-20 address is configured"
+ )
+ })
+
+ it("rejects an ERC-20 address that differs from the configured route", async () => {
+ const { reserveManager } = reserveManagerFixture(),
+ signer = new Erc20Signer(),
+ client = new EthereumReserveClient(reserveManager, signer),
+ differentTokenAddress = Wallet.createRandom().address
+
+ await expect(
+ client.createErc20WithApproval(request, differentTokenAddress)
+ ).rejects.toThrow("does not match the configured route")
+ expect(signer.provider.getTransactionReceipt).not.toHaveBeenCalled()
+ })
+})
diff --git a/packages/sdk-outpost/tests/clients/solana/SolanaOutpostClient.test.ts b/packages/sdk-outpost/tests/clients/solana/SolanaOutpostClient.test.ts
new file mode 100644
index 0000000..55319dd
--- /dev/null
+++ b/packages/sdk-outpost/tests/clients/solana/SolanaOutpostClient.test.ts
@@ -0,0 +1,279 @@
+import { Keypair, PublicKey, SystemProgram } from "@solana/web3.js"
+import { sha256 } from "ethers"
+
+import {
+ OutpostChainFamily,
+ OutpostClient,
+ type ReserveSwapRequest,
+ SolanaProgramName,
+ SolanaReserveClient,
+ SolanaReserveSwapClient,
+ SolanaUpgradeableLoaderProgramId,
+ type SolanaOutpostClient,
+ type SolanaOutpostClientOptions
+} from "@wireio/sdk-outpost"
+import {
+ createOutpostDeploymentProfileFixture,
+ createSolanaProgramAccountData,
+ createSolanaProgramDataAccountData,
+ createSolanaProviderFixture
+} from "../../Fixtures.js"
+
+const WrongProgramDataAddress = "SysvarRent111111111111111111111111111111111"
+const SubmittedSignature = "3".repeat(64)
+const SolanaProgramDataMetadataByteLength = 45
+
+/** Create the Solana client through the package's only public facade. */
+function createSolanaClient(
+ options: SolanaOutpostClientOptions
+): Promise {
+ return OutpostClient.create({
+ family: OutpostChainFamily.solana,
+ options
+ })
+}
+
+const reserveSwapRequest: ReserveSwapRequest = {
+ sourceTokenCode: 1,
+ sourceReserveCode: 2,
+ sourceAmount: 3,
+ targetChainCode: 4,
+ targetTokenCode: 5,
+ targetReserveCode: 6,
+ targetRecipient: Uint8Array.from([7]),
+ targetAmount: 8,
+ targetToleranceBps: 500
+}
+
+function unsigned64Seed(value: bigint): Buffer {
+ const seed = Buffer.alloc(8)
+ seed.writeBigUInt64LE(value)
+ return seed
+}
+
+describe("SolanaOutpostClient", () => {
+ it("verifies a profile and returns its runtime program address", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createSolanaProviderFixture(profile),
+ client = await createSolanaClient({ profile, provider }),
+ program = client.program(SolanaProgramName.liqsolCore)
+
+ expect(program.programId.toBase58()).toBe(
+ profile.solana.programs[SolanaProgramName.liqsolCore].address
+ )
+ expect(client.reserves).toBeInstanceOf(SolanaReserveClient)
+ expect(client.swaps).toBeInstanceOf(SolanaReserveSwapClient)
+ })
+
+ it("parses the protocol deposit id from confirmed program logs", () => {
+ expect(
+ SolanaReserveSwapClient.parseSourceRequestId([
+ "Program log: opp_outpost: SwapDeposit id=42 hash=abc"
+ ])
+ ).toBe(42n)
+ expect(() => SolanaReserveSwapClient.parseSourceRequestId([])).toThrow(
+ "did not log SwapDeposit"
+ )
+ })
+
+ it("derives native reserve accounts from the deployed program seeds", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createSolanaProviderFixture(profile),
+ client = await createSolanaClient({ profile, provider }),
+ instruction = await client.swaps.createNativeInstruction(
+ reserveSwapRequest
+ ),
+ programAddress = new PublicKey(
+ profile.solana.programs[SolanaProgramName.liqsolCore].address
+ ),
+ expectedReserve = PublicKey.findProgramAddressSync(
+ [
+ Buffer.from("reserve"),
+ unsigned64Seed(1n),
+ unsigned64Seed(2n)
+ ],
+ programAddress
+ )[0]
+
+ expect(instruction.keys[2].pubkey.equals(expectedReserve)).toBe(true)
+ })
+
+ it("derives SPL reserve vaults from the deployed program seeds", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createSolanaProviderFixture(profile),
+ client = await createSolanaClient({ profile, provider }),
+ instruction = await client.swaps.createSplInstruction({
+ ...reserveSwapRequest,
+ mint: Keypair.generate().publicKey
+ }),
+ programAddress = new PublicKey(
+ profile.solana.programs[SolanaProgramName.liqsolCore].address
+ ),
+ expectedVault = PublicKey.findProgramAddressSync(
+ [
+ Buffer.from("reserve_vault"),
+ unsigned64Seed(1n),
+ unsigned64Seed(2n)
+ ],
+ programAddress
+ )[0]
+
+ expect(instruction.keys[3].pubkey.equals(expectedVault)).toBe(true)
+ })
+
+ it("polls a submitted reserve swap until Solana confirms it", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createSolanaProviderFixture(profile),
+ client = await createSolanaClient({ profile, provider }),
+ instruction = SystemProgram.transfer({
+ fromPubkey: provider.wallet.publicKey,
+ toPubkey: provider.wallet.publicKey,
+ lamports: 1
+ })
+ jest.spyOn(client.swaps, "createNativeInstruction").mockResolvedValue(instruction)
+ jest.spyOn(provider.connection, "getLatestBlockhash").mockResolvedValue({
+ blockhash: Keypair.generate().publicKey.toBase58(),
+ lastValidBlockHeight: 100
+ })
+ jest.spyOn(provider.connection, "sendRawTransaction").mockResolvedValue(SubmittedSignature)
+ jest.spyOn(provider.connection, "getSignatureStatuses").mockResolvedValue({
+ context: { slot: 10 },
+ value: [{ slot: 10, confirmations: 1, err: null, confirmationStatus: "confirmed" }]
+ })
+ jest.spyOn(provider.connection, "getBlockHeight").mockResolvedValue(10)
+ jest.spyOn(provider.connection, "getTransaction").mockResolvedValue({
+ meta: { logMessages: ["Program log: opp_outpost: SwapDeposit id=42 hash=abc"] }
+ } as never)
+
+ await expect(client.swaps.requestNative(reserveSwapRequest)).resolves.toEqual({
+ transactionId: SubmittedSignature,
+ sourceRequestId: 42n
+ })
+ })
+
+ it("reports an explicit on-chain reserve swap failure", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createSolanaProviderFixture(profile),
+ client = await createSolanaClient({ profile, provider }),
+ instruction = SystemProgram.transfer({
+ fromPubkey: provider.wallet.publicKey,
+ toPubkey: provider.wallet.publicKey,
+ lamports: 1
+ })
+ jest.spyOn(client.swaps, "createNativeInstruction").mockResolvedValue(instruction)
+ jest.spyOn(provider.connection, "getLatestBlockhash").mockResolvedValue({
+ blockhash: Keypair.generate().publicKey.toBase58(),
+ lastValidBlockHeight: 100
+ })
+ jest.spyOn(provider.connection, "sendRawTransaction").mockResolvedValue(SubmittedSignature)
+ jest.spyOn(provider.connection, "getSignatureStatuses").mockResolvedValue({
+ context: { slot: 10 },
+ value: [{ slot: 10, confirmations: 1, err: { InstructionError: [2, "Custom"] }, confirmationStatus: "confirmed" }]
+ })
+ jest.spyOn(provider.connection, "getBlockHeight").mockResolvedValue(10)
+
+ await expect(client.swaps.requestNative(reserveSwapRequest)).rejects.toThrow(
+ `Solana reserve swap ${SubmittedSignature} failed`
+ )
+ })
+
+ it("rejects the wrong Solana cluster", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createSolanaProviderFixture(profile)
+ jest
+ .spyOn(provider.connection, "getGenesisHash")
+ .mockResolvedValue("9".repeat(32))
+
+ await expect(
+ createSolanaClient({ profile, provider })
+ ).rejects.toThrow("Solana genesis mismatch")
+ })
+
+ it("rejects a configured program that is not executable", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createSolanaProviderFixture(profile)
+ jest.spyOn(provider.connection, "getAccountInfo").mockResolvedValue(null)
+
+ await expect(
+ createSolanaClient({ profile, provider })
+ ).rejects.toThrow("is not executable")
+ })
+
+ it("rejects a Program account pointing at another ProgramData account", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createSolanaProviderFixture(profile)
+ jest.spyOn(provider.connection, "getAccountInfo").mockResolvedValue({
+ data: createSolanaProgramAccountData(WrongProgramDataAddress),
+ executable: true,
+ lamports: 1,
+ owner: SolanaUpgradeableLoaderProgramId,
+ rentEpoch: 0
+ })
+
+ await expect(
+ createSolanaClient({ profile, provider })
+ ).rejects.toThrow("ProgramData mismatch")
+ })
+
+ it("rejects a ProgramData code mismatch", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createSolanaProviderFixture(profile),
+ program = profile.solana.programs[SolanaProgramName.liqsolCore]
+ program.programDataSha256 = "f".repeat(64)
+ jest
+ .spyOn(provider.connection, "getAccountInfo")
+ .mockImplementation(async address =>
+ address.equals(new PublicKey(program.address))
+ ? {
+ data: createSolanaProgramAccountData(program.programDataAddress),
+ executable: true,
+ lamports: 1,
+ owner: SolanaUpgradeableLoaderProgramId,
+ rentEpoch: 0
+ }
+ : {
+ data: createSolanaProgramDataAccountData(),
+ executable: false,
+ lamports: 1,
+ owner: SolanaUpgradeableLoaderProgramId,
+ rentEpoch: 0
+ }
+ )
+
+ await expect(
+ createSolanaClient({ profile, provider })
+ ).rejects.toThrow("ProgramData mismatch")
+ })
+
+ it("rejects ProgramData executable bytes from another producer binary", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ program = profile.solana.programs[SolanaProgramName.liqsolCore],
+ incompatibleProgramData = createSolanaProgramDataAccountData()
+ incompatibleProgramData[SolanaProgramDataMetadataByteLength] ^= 1
+ program.programDataSha256 = sha256(incompatibleProgramData).slice(2)
+ const provider = createSolanaProviderFixture(profile)
+ jest
+ .spyOn(provider.connection, "getAccountInfo")
+ .mockImplementation(async address =>
+ address.equals(new PublicKey(program.address))
+ ? {
+ data: createSolanaProgramAccountData(program.programDataAddress),
+ executable: true,
+ lamports: 1,
+ owner: SolanaUpgradeableLoaderProgramId,
+ rentEpoch: 0
+ }
+ : {
+ data: incompatibleProgramData,
+ executable: false,
+ lamports: 1,
+ owner: SolanaUpgradeableLoaderProgramId,
+ rentEpoch: 0
+ }
+ )
+
+ await expect(
+ createSolanaClient({ profile, provider })
+ ).rejects.toThrow("artifact program mismatch")
+ })
+})
diff --git a/packages/sdk-outpost/tests/clients/solana/SolanaReserveAddresses.test.ts b/packages/sdk-outpost/tests/clients/solana/SolanaReserveAddresses.test.ts
new file mode 100644
index 0000000..83d0a95
--- /dev/null
+++ b/packages/sdk-outpost/tests/clients/solana/SolanaReserveAddresses.test.ts
@@ -0,0 +1,66 @@
+import { Keypair, PublicKey } from "@solana/web3.js"
+
+import { SolanaReserveAddresses } from "@wireio/sdk-outpost"
+
+const Unsigned64ByteLength = 8
+
+function unsigned64Seed(value: bigint): Buffer {
+ const seed = Buffer.alloc(Unsigned64ByteLength)
+ seed.writeBigUInt64LE(value)
+ return seed
+}
+
+describe("SolanaReserveAddresses", () => {
+ it("derives every reserve lifecycle PDA from the deployed program", () => {
+ const programId = Keypair.generate().publicKey,
+ addresses = new SolanaReserveAddresses(programId),
+ identity = { tokenCode: 1n, reserveCode: 2n },
+ expectedReserve = PublicKey.findProgramAddressSync(
+ [
+ Buffer.from("reserve"),
+ unsigned64Seed(1n),
+ unsigned64Seed(2n)
+ ],
+ programId
+ )[0],
+ expectedVault = PublicKey.findProgramAddressSync(
+ [
+ Buffer.from("reserve_vault"),
+ unsigned64Seed(1n),
+ unsigned64Seed(2n)
+ ],
+ programId
+ )[0]
+
+ expect(addresses.reserve(identity).equals(expectedReserve)).toBe(true)
+ expect(addresses.reserveVault(identity).equals(expectedVault)).toBe(true)
+ expect(
+ addresses
+ .outpostConfig()
+ .equals(
+ PublicKey.findProgramAddressSync(
+ [Buffer.from("outpost_config")],
+ programId
+ )[0]
+ )
+ ).toBe(true)
+ expect(
+ addresses
+ .outboundMessageBuffer()
+ .equals(
+ PublicKey.findProgramAddressSync(
+ [Buffer.from("outbound_message_buffer")],
+ programId
+ )[0]
+ )
+ ).toBe(true)
+ })
+
+ it("rejects reserve identity values outside the protocol u64 range", () => {
+ const addresses = new SolanaReserveAddresses(Keypair.generate().publicKey)
+
+ expect(() => addresses.unsigned64(0, "tokenCode")).toThrow(
+ "tokenCode must be between 1 and uint64 max."
+ )
+ })
+})
diff --git a/packages/sdk-outpost/tests/clients/solana/SolanaReserveClient.test.ts b/packages/sdk-outpost/tests/clients/solana/SolanaReserveClient.test.ts
new file mode 100644
index 0000000..fd11618
--- /dev/null
+++ b/packages/sdk-outpost/tests/clients/solana/SolanaReserveClient.test.ts
@@ -0,0 +1,190 @@
+import { BN, Program } from "@coral-xyz/anchor"
+import {
+ liqsolCoreIdl,
+ type LiqsolCore
+} from "@wireio/outpost-solana-artifacts"
+import { ASSOCIATED_TOKEN_PROGRAM_ID } from "@solana/spl-token"
+import { Keypair, PublicKey } from "@solana/web3.js"
+
+import {
+ OutpostReserveStatus,
+ type SolanaReserveCreateRequest,
+ SolanaReserveClient
+} from "@wireio/sdk-outpost"
+import {
+ createOutpostDeploymentProfileFixture,
+ createSolanaProviderFixture
+} from "../../Fixtures.js"
+
+const SubmittedSignature = "4".repeat(64),
+ AccountData = Buffer.alloc(8),
+ NativeTokenMarker = new PublicKey(new Uint8Array(32))
+
+const request: SolanaReserveCreateRequest = {
+ tokenCode: 1,
+ reserveCode: 2,
+ externalTokenAmount: 3,
+ requestedWireAmount: 4,
+ connectorWeightBps: 5_000,
+ name: "Private USDC reserve",
+ description: "Same-owner external routing",
+ isPrivate: true,
+ mint: Keypair.generate().publicKey
+}
+
+function clientFixture() {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createSolanaProviderFixture(profile),
+ address = profile.solana.programs.liqsolCore.address,
+ program = new Program(
+ { ...liqsolCoreIdl, address },
+ provider
+ ),
+ client = new SolanaReserveClient(provider, program)
+
+ return { client, program, provider }
+}
+
+describe("SolanaReserveClient", () => {
+ it("builds permissionless reserve creation with an idempotent creator ATA", async () => {
+ const { client, program, provider } = clientFixture(),
+ instructions = await client.createInstructions(request)
+
+ expect(instructions).toHaveLength(2)
+ expect(
+ instructions[0].programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID)
+ ).toBe(true)
+ expect(instructions[1].programId.equals(program.programId)).toBe(true)
+ expect(
+ instructions[1].keys.some(key =>
+ key.pubkey.equals(provider.wallet.publicKey)
+ )
+ ).toBe(true)
+ })
+
+ it("uses an explicit creator token account without adding an ATA instruction", async () => {
+ const { client } = clientFixture(),
+ instructions = await client.createInstructions({
+ ...request,
+ creatorTokenAccount: Keypair.generate().publicKey
+ })
+
+ expect(instructions).toHaveLength(1)
+ })
+
+ it("submits reserve creation and pending cancellation through the provider", async () => {
+ const { client, provider } = clientFixture(),
+ sendAndConfirm = jest
+ .spyOn(provider, "sendAndConfirm")
+ .mockResolvedValue(SubmittedSignature)
+
+ await expect(client.create(request)).resolves.toEqual({
+ transactionId: SubmittedSignature
+ })
+ await expect(
+ client.cancel({ tokenCode: 1, reserveCode: 2 })
+ ).resolves.toEqual({ transactionId: SubmittedSignature })
+ expect(sendAndConfirm).toHaveBeenCalledTimes(2)
+ })
+
+ it("normalizes configured token routes and a local reserve record", async () => {
+ const { client, program, provider } = clientFixture(),
+ splMint = Keypair.generate().publicKey,
+ creator = Keypair.generate().publicKey
+
+ jest.spyOn(provider.connection, "getAccountInfo").mockResolvedValue({
+ data: AccountData,
+ executable: false,
+ lamports: 1,
+ owner: program.programId,
+ rentEpoch: 0
+ })
+ jest
+ .spyOn(program.coder.accounts, "decode")
+ .mockImplementation(accountName =>
+ (accountName === "outpostConfig"
+ ? {
+ tokenAddressesByCode: [
+ { tokenCode: new BN(1), mint: NativeTokenMarker },
+ { tokenCode: new BN(2), mint: splMint }
+ ],
+ precisionByTokenCode: [
+ { tokenCode: new BN(2), decimals: 6 }
+ ]
+ }
+ : {
+ tokenCode: new BN(2),
+ reserveCode: new BN(3),
+ externalTokenAmount: new BN(4),
+ requestedWireAmount: new BN(5),
+ connectorWeightBps: 5_000,
+ status: { active: {} },
+ creator,
+ nameLen: 4,
+ nameBytes: [...Buffer.from("USDC"), ...new Uint8Array(60)],
+ descriptionLen: 7,
+ descriptionBytes: [
+ ...Buffer.from("Private"),
+ ...new Uint8Array(249)
+ ]
+ }) as never
+ )
+
+ await expect(client.getConfiguredTokens()).resolves.toEqual([
+ {
+ tokenCode: 1n,
+ mint: NativeTokenMarker,
+ isNative: true,
+ decimals: 9
+ },
+ { tokenCode: 2n, mint: splMint, isNative: false, decimals: 6 }
+ ])
+ await expect(
+ client.get({ tokenCode: 2, reserveCode: 3 })
+ ).resolves.toEqual(
+ expect.objectContaining({
+ tokenCode: 2n,
+ reserveCode: 3n,
+ status: OutpostReserveStatus.active,
+ creator,
+ name: "USDC",
+ description: "Private"
+ })
+ )
+ })
+
+ it("returns null for an absent account and rejects invalid creation values", async () => {
+ const { client, provider } = clientFixture()
+ jest.spyOn(provider.connection, "getAccountInfo").mockResolvedValue(null)
+
+ await expect(
+ client.get({ tokenCode: 1, reserveCode: 2 })
+ ).resolves.toBeNull()
+ await expect(
+ client.createInstructions({ ...request, externalTokenAmount: 0 })
+ ).rejects.toThrow("externalTokenAmount must be greater than zero.")
+ await expect(
+ client.createInstructions({ ...request, mint: NativeTokenMarker })
+ ).rejects.toThrow("requires a real placeholder SPL mint")
+ })
+
+ it("rejects a configured SPL reserve route without chain precision", async () => {
+ const { client, program, provider } = clientFixture(),
+ splMint = Keypair.generate().publicKey
+ jest.spyOn(provider.connection, "getAccountInfo").mockResolvedValue({
+ data: AccountData,
+ executable: false,
+ lamports: 1,
+ owner: program.programId,
+ rentEpoch: 0
+ })
+ jest.spyOn(program.coder.accounts, "decode").mockReturnValue({
+ tokenAddressesByCode: [{ tokenCode: new BN(2), mint: splMint }],
+ precisionByTokenCode: []
+ } as never)
+
+ await expect(client.getConfiguredTokens()).rejects.toThrow(
+ "Solana reserve token 2 has no configured precision."
+ )
+ })
+})
diff --git a/packages/sdk-outpost/tests/deployments/Schema.test.ts b/packages/sdk-outpost/tests/deployments/Schema.test.ts
new file mode 100644
index 0000000..868ea69
--- /dev/null
+++ b/packages/sdk-outpost/tests/deployments/Schema.test.ts
@@ -0,0 +1,63 @@
+import {
+ EthereumContractName,
+ parseOutpostDeploymentProfile
+} from "@wireio/sdk-outpost"
+import { createOutpostDeploymentProfileFixture } from "../Fixtures.js"
+
+describe("OutpostDeploymentProfileSchema", () => {
+ it("parses a valid profile with its Wire chain identity", () => {
+ const profile = parseOutpostDeploymentProfile(
+ createOutpostDeploymentProfileFixture()
+ )
+
+ expect(profile.id).toBe(
+ `${profile.wire.chainId}-${profile.deploymentChecksum.slice(0, 12)}`
+ )
+ expect(
+ profile.ethereum.contracts[EthereumContractName.ReserveManager].address
+ ).toBe(
+ createOutpostDeploymentProfileFixture().ethereum.contracts[
+ EthereumContractName.ReserveManager
+ ].address
+ )
+ })
+
+ it("rejects an invalid contract address", () => {
+ const fixture = createOutpostDeploymentProfileFixture()
+ fixture.ethereum.contracts.OPP.address = "not-an-address"
+
+ expect(() => parseOutpostDeploymentProfile(fixture)).toThrow(
+ "Invalid Ethereum address"
+ )
+ })
+
+ it("preserves schema-v1 profiles when BAR is not deployed", () => {
+ const profile = createOutpostDeploymentProfileFixture()
+ delete profile.ethereum.contracts[EthereumContractName.BAR]
+
+ expect(
+ parseOutpostDeploymentProfile(profile).ethereum.contracts[
+ EthereumContractName.BAR
+ ]
+ ).toBeUndefined()
+ })
+
+ it("rejects an invalid Solana ProgramData address", () => {
+ const fixture = createOutpostDeploymentProfileFixture()
+ fixture.solana.programs.liqsolCore.programDataAddress =
+ "not-a-program-data-address"
+
+ expect(() => parseOutpostDeploymentProfile(fixture)).toThrow(
+ "Invalid Solana address"
+ )
+ })
+
+ it("rejects an environment-specific profile id", () => {
+ const fixture = createOutpostDeploymentProfileFixture()
+ fixture.id = "named-environment"
+
+ expect(() => parseOutpostDeploymentProfile(fixture)).toThrow(
+ "Deployment profile id must be"
+ )
+ })
+})
diff --git a/packages/sdk-outpost/tests/reserves/Validation.test.ts b/packages/sdk-outpost/tests/reserves/Validation.test.ts
new file mode 100644
index 0000000..781fed4
--- /dev/null
+++ b/packages/sdk-outpost/tests/reserves/Validation.test.ts
@@ -0,0 +1,97 @@
+import {
+ assertEthereumReserveCreateRequest,
+ assertReserveCreateDefinition,
+ assertReserveSwapRequest,
+ assertReserveUnsigned64,
+ type EthereumReserveCreateRequest,
+ type ReserveCreateDefinition,
+ type ReserveSwapRequest
+} from "@wireio/sdk-outpost"
+
+const InvalidConnectorWeights = [0, 10_000]
+
+const reserveDefinition: ReserveCreateDefinition = {
+ tokenCode: 1,
+ reserveCode: 2,
+ externalTokenAmount: 3,
+ requestedWireAmount: 4,
+ connectorWeightBps: 5_000,
+ name: "Private ETH reserve",
+ description: "Same-owner external routing",
+ isPrivate: true
+}
+
+const ethereumRequest: EthereumReserveCreateRequest = {
+ ...reserveDefinition,
+ creatorPubKey: `0x02${"11".repeat(32)}`
+}
+
+const request: ReserveSwapRequest = {
+ sourceTokenCode: 1,
+ sourceReserveCode: 2,
+ sourceAmount: 3,
+ targetChainCode: 4,
+ targetTokenCode: 5,
+ targetReserveCode: 6,
+ targetRecipient: Uint8Array.from([7]),
+ targetAmount: 8,
+ targetToleranceBps: 500
+}
+
+describe("reserve swap validation", () => {
+ it("accepts portable and Ethereum reserve creation requests", () => {
+ expect(() => assertReserveCreateDefinition(reserveDefinition)).not.toThrow()
+ expect(() =>
+ assertEthereumReserveCreateRequest(ethereumRequest)
+ ).not.toThrow()
+ })
+
+ it("rejects invalid reserve creation metadata and creator keys", () => {
+ InvalidConnectorWeights.forEach(connectorWeightBps =>
+ expect(() =>
+ assertReserveCreateDefinition({
+ ...reserveDefinition,
+ connectorWeightBps
+ })
+ ).toThrow("connectorWeightBps")
+ )
+ expect(() =>
+ assertReserveCreateDefinition({ ...reserveDefinition, name: "" })
+ ).toThrow("name must contain")
+ expect(() =>
+ assertEthereumReserveCreateRequest({
+ ...ethereumRequest,
+ creatorPubKey: "0x02"
+ })
+ ).toThrow("33-byte compressed")
+ expect(() =>
+ assertEthereumReserveCreateRequest({
+ ...ethereumRequest,
+ creatorPubKey: `0x04${"11".repeat(32)}`
+ })
+ ).toThrow("33-byte compressed")
+ expect(() =>
+ assertEthereumReserveCreateRequest({
+ ...ethereumRequest,
+ creatorPubKey: "not-hex"
+ })
+ ).toThrow("33-byte compressed")
+ })
+
+ it("accepts a portable positive request", () => {
+ expect(() => assertReserveSwapRequest(request)).not.toThrow()
+ expect(assertReserveUnsigned64(8, "value")).toBe(8n)
+ })
+
+ it("rejects an empty recipient and values outside uint64", () => {
+ expect(() =>
+ assertReserveSwapRequest({
+ ...request,
+ targetRecipient: new Uint8Array()
+ })
+ ).toThrow("targetRecipient is required")
+ expect(() => assertReserveUnsigned64(0, "value")).toThrow(
+ "between 1 and uint64 max"
+ )
+ })
+})
diff --git a/packages/sdk-outpost/tests/verification/OutpostDeploymentVerifier.test.ts b/packages/sdk-outpost/tests/verification/OutpostDeploymentVerifier.test.ts
new file mode 100644
index 0000000..a3e0c5d
--- /dev/null
+++ b/packages/sdk-outpost/tests/verification/OutpostDeploymentVerifier.test.ts
@@ -0,0 +1,43 @@
+import {
+ OutpostChainFamily,
+ OutpostDeploymentVerifier,
+ SolanaUpgradeableLoaderProgramId
+} from "@wireio/sdk-outpost"
+import {
+ createEthereumProviderFixture,
+ createOutpostDeploymentProfileFixture,
+ createSolanaProviderFixture
+} from "../Fixtures.js"
+
+describe("OutpostDeploymentVerifier", () => {
+ it("verifies an Ethereum deployment through the generic facade", async () => {
+ const profile = createOutpostDeploymentProfileFixture()
+
+ await expect(
+ OutpostDeploymentVerifier.verify({
+ family: OutpostChainFamily.ethereum,
+ profile,
+ provider: createEthereumProviderFixture(profile)
+ })
+ ).resolves.toBeUndefined()
+ })
+
+ it("verifies a Solana deployment through the generic facade", async () => {
+ const profile = createOutpostDeploymentProfileFixture(),
+ provider = createSolanaProviderFixture(profile)
+
+ await expect(
+ OutpostDeploymentVerifier.verify({
+ family: OutpostChainFamily.solana,
+ profile,
+ connection: provider.connection
+ })
+ ).resolves.toBeUndefined()
+ })
+
+ it("exports the canonical Solana upgradeable-loader identity", () => {
+ expect(SolanaUpgradeableLoaderProgramId.toBase58()).toBe(
+ "BPFLoaderUpgradeab1e11111111111111111111111"
+ )
+ })
+})
diff --git a/packages/sdk-outpost/tsconfig.cjs.jest.json b/packages/sdk-outpost/tsconfig.cjs.jest.json
new file mode 100644
index 0000000..9a5a5f8
--- /dev/null
+++ b/packages/sdk-outpost/tsconfig.cjs.jest.json
@@ -0,0 +1,20 @@
+{
+ "extends": "../../etc/tsconfig/tsconfig.base.jest.json",
+ "compilerOptions": {
+ "rootDir": "tests",
+ "outDir": "lib/test-cjs",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "ignoreDeprecations": "6.0",
+ "composite": true,
+ "incremental": true,
+ "strict": true,
+ "noImplicitAny": true,
+ "paths": {
+ "@wireio/sdk-outpost": ["./src"],
+ "@wireio/sdk-outpost/*": ["./src/*"]
+ }
+ },
+ "references": [{ "path": "./tsconfig.cjs.json" }],
+ "include": ["tests"]
+}
diff --git a/packages/sdk-outpost/tsconfig.cjs.json b/packages/sdk-outpost/tsconfig.cjs.json
new file mode 100644
index 0000000..b15a1fa
--- /dev/null
+++ b/packages/sdk-outpost/tsconfig.cjs.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../etc/tsconfig/tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "lib/cjs",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "ignoreDeprecations": "6.0",
+ "strict": true,
+ "noImplicitAny": true
+ },
+ "include": ["src"]
+}
diff --git a/packages/sdk-outpost/tsconfig.esm.json b/packages/sdk-outpost/tsconfig.esm.json
new file mode 100644
index 0000000..35ab683
--- /dev/null
+++ b/packages/sdk-outpost/tsconfig.esm.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../etc/tsconfig/tsconfig.base.esm.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "lib/esm",
+ "strict": true,
+ "noImplicitAny": true
+ },
+ "include": ["src"]
+}
diff --git a/packages/sdk-outpost/tsconfig.json b/packages/sdk-outpost/tsconfig.json
new file mode 100644
index 0000000..75e4065
--- /dev/null
+++ b/packages/sdk-outpost/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../etc/tsconfig/tsconfig.base.json",
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.esm.json" },
+ { "path": "./tsconfig.cjs.json" },
+ { "path": "./tsconfig.cjs.jest.json" }
+ ]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9c96d40..43ed7c9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,15 +11,11 @@ overrides:
'@aws-sdk/client-ssm': 3.1102.0
'@aws-sdk/client-sts': 3.1102.0
-pnpmfileChecksum: sha256-aSegbCvK5TyBUeMcgQLjFgZgcFmq1CMqHT+lQb93Qks=
+pnpmfileChecksum: sha256-8SxVsi62NYbViQ0DPEJl+ttUXrF7HLpNiC5CJ8NzUT4=
importers:
.:
- dependencies:
- '@wireio/opp-typescript-models':
- specifier: ^1.0.26
- version: 1.0.48
devDependencies:
'@eslint/js':
specifier: ^10.0.1
@@ -56,7 +52,7 @@ importers:
version: 30.4.2(@types/node@25.5.0)(ts-node@10.9.2(@types/node@25.5.0)(typescript@6.0.2))
jest-environment-jsdom:
specifier: ^30.3.0
- version: 30.4.1
+ version: 30.4.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)
jest-junit:
specifier: ^17.0.0
version: 17.0.0
@@ -102,7 +98,7 @@ importers:
version: 6.0.1(webpack-dev-server@6.0.0)(webpack@5.104.1)
webpack-dev-server:
specifier: ^6.0.0
- version: 6.0.0(tslib@2.8.1)(webpack-cli@6.0.1)(webpack@5.104.1)
+ version: 6.0.0(bufferutil@4.1.0)(tslib@2.8.1)(utf-8-validate@6.0.6)(webpack-cli@6.0.1)(webpack@5.104.1)
packages/sdk-core:
dependencies:
@@ -139,9 +135,6 @@ importers:
'@noble/curves':
specifier: 1.9.7
version: 1.9.7
- '@wireio/opp-typescript-models':
- specifier: ^1.0.26
- version: 1.0.48
'@wireio/shared':
specifier: workspace:*
version: link:../shared
@@ -192,6 +185,43 @@ importers:
specifier: 6.0.2
version: 6.0.2
+ packages/sdk-outpost:
+ dependencies:
+ '@coral-xyz/anchor':
+ specifier: ^0.32.1
+ version: 0.32.1(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6)
+ '@solana/spl-token':
+ specifier: ^0.3.11
+ version: 0.3.11(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@6.0.2)(utf-8-validate@6.0.6)
+ '@solana/web3.js':
+ specifier: ^1.98.4
+ version: 1.98.4(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6)
+ '@wireio/outpost-ethereum-artifacts':
+ specifier: 0.3.0
+ version: 0.3.0(ethers@6.17.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))
+ '@wireio/outpost-solana-artifacts':
+ specifier: 0.3.0
+ version: 0.3.0
+ '@wireio/sdk-core':
+ specifier: workspace:*
+ version: link:../sdk-core
+ ethers:
+ specifier: ^6.15.0
+ version: 6.17.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
+ ts-pattern:
+ specifier: ^5.9.0
+ version: 5.9.0
+ zod:
+ specifier: ^4.4.3
+ version: 4.4.3
+ devDependencies:
+ rpc-websockets:
+ specifier: 9.3.8
+ version: 9.3.8
+ typescript:
+ specifier: 6.0.2
+ version: 6.0.2
+
packages/shared:
dependencies:
'@3fv/prelude-ts':
@@ -339,6 +369,9 @@ packages:
'@3fv/prelude-ts@0.8.42':
resolution: {integrity: sha512-a+d8lRjkUlzY7cyqYbClWKmEj1gAYa6ELeuXnhouJikIii9WUpI5aKIGsk19n3edNd5uzvGrf4FsI7uqNqFM+g==}
+ '@adraffy/ens-normalize@1.11.1':
+ resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==}
+
'@asamuzakjp/css-color@3.2.0':
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
@@ -563,6 +596,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/runtime@7.29.7':
+ resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/template@7.29.7':
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'}
@@ -578,6 +615,20 @@ packages:
'@bcoe/v8-coverage@0.2.3':
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
+ '@coral-xyz/anchor-errors@0.31.1':
+ resolution: {integrity: sha512-NhNEku4F3zzUSBtrYz84FzYWm48+9OvmT1Hhnwr6GnPQry2dsEqH/ti/7ASjjpoFTWRnPXrjAIT1qM6Isop+LQ==}
+ engines: {node: '>=10'}
+
+ '@coral-xyz/anchor@0.32.1':
+ resolution: {integrity: sha512-zAyxFtfeje2FbMA1wzgcdVs7Hng/MijPKpRijoySPCicnvcTQs/+dnPZ/cR+LcXM9v9UYSyW81uRNYZtN5G4yg==}
+ engines: {node: '>=17'}
+
+ '@coral-xyz/borsh@0.31.1':
+ resolution: {integrity: sha512-9N8AU9F0ubriKfNE3g1WF0/4dtlGXoBN/hd1PvbNBamBNwRgHxH4P+o3Zt7rSEloW1HUs6LfZEchlx9fW7POYw==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@solana/web3.js': ^1.69.0
+
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
@@ -1003,10 +1054,17 @@ packages:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
+ '@noble/curves@1.2.0':
+ resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==}
+
'@noble/curves@1.9.7':
resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==}
engines: {node: ^14.21.3 || >=16}
+ '@noble/hashes@1.3.2':
+ resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==}
+ engines: {node: '>= 16'}
+
'@noble/hashes@1.4.0':
resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==}
engines: {node: '>= 16'}
@@ -1060,9 +1118,6 @@ packages:
resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==}
engines: {node: ^14.18.0 || >=16.0.0}
- '@protobuf-ts/runtime@2.11.1':
- resolution: {integrity: sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==}
-
'@reduxjs/toolkit@2.12.0':
resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==}
peerDependencies:
@@ -1107,12 +1162,94 @@ packages:
resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==}
engines: {node: '>=18.0.0'}
+ '@solana/buffer-layout-utils@0.2.0':
+ resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==}
+ engines: {node: '>= 10'}
+
+ '@solana/buffer-layout@4.0.1':
+ resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==}
+ engines: {node: '>=5.10'}
+
+ '@solana/codecs-core@2.0.0-rc.1':
+ resolution: {integrity: sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==}
+ peerDependencies:
+ typescript: '>=5'
+
+ '@solana/codecs-core@2.3.0':
+ resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==}
+ engines: {node: '>=20.18.0'}
+ peerDependencies:
+ typescript: '>=5.3.3'
+
+ '@solana/codecs-data-structures@2.0.0-rc.1':
+ resolution: {integrity: sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==}
+ peerDependencies:
+ typescript: '>=5'
+
+ '@solana/codecs-numbers@2.0.0-rc.1':
+ resolution: {integrity: sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==}
+ peerDependencies:
+ typescript: '>=5'
+
+ '@solana/codecs-numbers@2.3.0':
+ resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==}
+ engines: {node: '>=20.18.0'}
+ peerDependencies:
+ typescript: '>=5.3.3'
+
+ '@solana/codecs-strings@2.0.0-rc.1':
+ resolution: {integrity: sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==}
+ peerDependencies:
+ fastestsmallesttextencoderdecoder: ^1.0.22
+ typescript: '>=5'
+
+ '@solana/codecs@2.0.0-rc.1':
+ resolution: {integrity: sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==}
+ peerDependencies:
+ typescript: '>=5'
+
+ '@solana/errors@2.0.0-rc.1':
+ resolution: {integrity: sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==}
+ hasBin: true
+ peerDependencies:
+ typescript: '>=5'
+
+ '@solana/errors@2.3.0':
+ resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==}
+ engines: {node: '>=20.18.0'}
+ hasBin: true
+ peerDependencies:
+ typescript: '>=5.3.3'
+
+ '@solana/options@2.0.0-rc.1':
+ resolution: {integrity: sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==}
+ peerDependencies:
+ typescript: '>=5'
+
+ '@solana/spl-token-metadata@0.1.6':
+ resolution: {integrity: sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==}
+ engines: {node: '>=16'}
+ peerDependencies:
+ '@solana/web3.js': ^1.95.3
+
+ '@solana/spl-token@0.3.11':
+ resolution: {integrity: sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==}
+ engines: {node: '>=16'}
+ peerDependencies:
+ '@solana/web3.js': ^1.88.0
+
+ '@solana/web3.js@1.98.4':
+ resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==}
+
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@standard-schema/utils@0.3.0':
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+ '@swc/helpers@0.5.23':
+ resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
+
'@tsconfig/node10@1.0.12':
resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==}
@@ -1227,9 +1364,15 @@ packages:
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
+ '@types/node@12.20.55':
+ resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
+
'@types/node@22.20.0':
resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==}
+ '@types/node@22.7.5':
+ resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==}
+
'@types/node@25.5.0':
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
@@ -1265,6 +1408,12 @@ packages:
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
+ '@types/uuid@10.0.0':
+ resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
+
+ '@types/ws@7.4.7':
+ resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==}
+
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
@@ -1526,8 +1675,13 @@ packages:
webpack-dev-server:
optional: true
- '@wireio/opp-typescript-models@1.0.48':
- resolution: {integrity: sha512-3HDC88AohYBBMuVdSqdI5tIRHPZWWTOaS+yQ/Xs1sCvhKuqlpF3KnYLcgIiXX3x6IC87dfo55xBfHPFcWpthmw==}
+ '@wireio/outpost-ethereum-artifacts@0.3.0':
+ resolution: {integrity: sha512-z//qpsX/OLOBBl/TRP854WbhZy79P/PuP2gfME7tN1kwChIoytrniXyVJ3tMvuMb9rvtjb5JuYfejG0lp5cEWA==}
+ peerDependencies:
+ ethers: ^6.15.0
+
+ '@wireio/outpost-solana-artifacts@0.3.0':
+ resolution: {integrity: sha512-zAiH9cI5pp2gSKFTsQp9qqjvlAGL49XK+oZjjOeQ41SW9gwXXWatMc10GuvmwpolfBeRyrIqiTfP7s0W9fNjjw==}
'@xtuc/ieee754@1.2.0':
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
@@ -1563,10 +1717,17 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
+ aes-js@4.0.0-beta.5:
+ resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==}
+
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
+ agentkeepalive@4.6.0:
+ resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
+ engines: {node: '>= 8.0.0'}
+
ajv-formats@2.1.1:
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies:
@@ -1661,6 +1822,12 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
+ base-x@3.0.11:
+ resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==}
+
+ base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
baseline-browser-mapping@2.10.42:
resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==}
engines: {node: '>=6.0.0'}
@@ -1669,6 +1836,16 @@ packages:
batch@0.6.1:
resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==}
+ bigint-buffer@1.1.5:
+ resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==}
+ engines: {node: '>= 10.0.0'}
+
+ bignumber.js@9.3.1:
+ resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
+
+ bindings@1.5.0:
+ resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
+
bluebird@3.7.2:
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
@@ -1688,6 +1865,9 @@ packages:
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+ borsh@0.7.0:
+ resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==}
+
bowser@2.14.1:
resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==}
@@ -1717,12 +1897,26 @@ packages:
resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==}
engines: {node: '>= 6'}
+ bs58@4.0.1:
+ resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==}
+
bser@2.1.1:
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+ buffer-layout@1.2.2:
+ resolution: {integrity: sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==}
+ engines: {node: '>=4.5'}
+
+ buffer@6.0.3:
+ resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+
+ bufferutil@4.1.0:
+ resolution: {integrity: sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==}
+ engines: {node: '>=6.14.2'}
+
bundle-name@4.1.0:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
engines: {node: '>=18'}
@@ -1765,6 +1959,10 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
+ chalk@5.6.2:
+ resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
+ engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
char-regex@1.0.2:
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
engines: {node: '>=10'}
@@ -1821,6 +2019,10 @@ packages:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
+ commander@14.0.3:
+ resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
+ engines: {node: '>=20'}
+
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@@ -1880,6 +2082,9 @@ packages:
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
+ cross-fetch@3.2.0:
+ resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
+
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -1978,6 +2183,10 @@ packages:
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
engines: {node: '>=12'}
+ delay@5.0.0:
+ resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==}
+ engines: {node: '>=10'}
+
depd@1.1.2:
resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
engines: {node: '>= 0.6'}
@@ -2081,6 +2290,12 @@ packages:
resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
engines: {node: '>= 0.4'}
+ es6-promise@4.2.8:
+ resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
+
+ es6-promisify@5.0.0:
+ resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==}
+
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -2161,6 +2376,13 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
+ ethers@6.17.0:
+ resolution: {integrity: sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==}
+ engines: {node: '>=14.0.0'}
+
+ eventemitter3@4.0.7:
+ resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+
eventemitter3@5.0.4:
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
@@ -2184,6 +2406,10 @@ packages:
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
engines: {node: '>= 18'}
+ eyes@0.1.8:
+ resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==}
+ engines: {node: '> 0.1.90'}
+
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -2193,6 +2419,9 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+ fast-stable-stringify@1.0.0:
+ resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==}
+
fast-uri@3.1.3:
resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==}
@@ -2200,6 +2429,9 @@ packages:
resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
engines: {node: '>= 4.9.1'}
+ fastestsmallesttextencoderdecoder@1.0.22:
+ resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==}
+
fb-watchman@2.0.2:
resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
@@ -2216,6 +2448,9 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
+ file-uri-to-path@1.0.0:
+ resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
+
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@@ -2403,6 +2638,9 @@ packages:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
+ humanize-ms@1.2.1:
+ resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
+
husky@9.1.7:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'}
@@ -2426,6 +2664,9 @@ packages:
peerDependencies:
postcss: ^8.1.0
+ ieee754@1.2.1:
+ resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -2539,6 +2780,11 @@ packages:
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
engines: {node: '>=0.10.0'}
+ isomorphic-ws@4.0.1:
+ resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==}
+ peerDependencies:
+ ws: '*'
+
istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
@@ -2562,6 +2808,11 @@ packages:
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
+ jayson@4.3.0:
+ resolution: {integrity: sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==}
+ engines: {node: '>=8'}
+ hasBin: true
+
jest-changed-files@30.4.1:
resolution: {integrity: sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
@@ -2749,6 +3000,9 @@ packages:
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+ json-stringify-safe@5.0.1:
+ resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
+
json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
@@ -2938,6 +3192,19 @@ packages:
no-case@3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
+ node-fetch@2.7.0:
+ resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+
+ node-gyp-build@4.8.4:
+ resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
+ hasBin: true
+
node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
@@ -3244,6 +3511,9 @@ packages:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
+ rpc-websockets@9.3.8:
+ resolution: {integrity: sha512-7r+fm4tSJmLf9GvZfL1DJ1SJwpagpp6AazqM0FUaeV7CA+7+NYINSk1syWa4tU/6OF2CyBicLtzENGmXRJH6wQ==}
+
rrweb-cssom@0.8.0:
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
@@ -3383,6 +3653,12 @@ packages:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
+ stream-chain@2.2.5:
+ resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==}
+
+ stream-json@1.9.1:
+ resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==}
+
string-length@4.0.2:
resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
engines: {node: '>=10'}
@@ -3421,6 +3697,13 @@ packages:
peerDependencies:
webpack: ^5.27.0
+ superstruct@0.15.5:
+ resolution: {integrity: sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==}
+
+ superstruct@2.0.2:
+ resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==}
+ engines: {node: '>=14.0.0'}
+
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -3496,6 +3779,9 @@ packages:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
engines: {node: '>=8'}
+ text-encoding-utf-8@1.0.2:
+ resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==}
+
thingies@2.6.0:
resolution: {integrity: sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==}
engines: {node: '>=10.18'}
@@ -3531,10 +3817,16 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
+ toml@3.0.0:
+ resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==}
+
tough-cookie@5.1.2:
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
engines: {node: '>=16'}
+ tr46@0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
tr46@5.1.1:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
@@ -3617,6 +3909,9 @@ packages:
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
+ tslib@2.7.0:
+ resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==}
+
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -3664,6 +3959,9 @@ packages:
engines: {node: '>=0.8.0'}
hasBin: true
+ undici-types@6.19.8:
+ resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
+
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@@ -3691,16 +3989,29 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ utf-8-validate@6.0.6:
+ resolution: {integrity: sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==}
+ engines: {node: '>=6.14.2'}
+
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
utila@0.4.0:
resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==}
+ uuid@11.1.1:
+ resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==}
+ hasBin: true
+
uuid@14.0.1:
resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==}
hasBin: true
+ uuid@8.3.2:
+ resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
+ deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
+ hasBin: true
+
v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
@@ -3723,6 +4034,9 @@ packages:
resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==}
engines: {node: '>=10.13.0'}
+ webidl-conversions@3.0.1:
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
@@ -3794,6 +4108,9 @@ packages:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
+ whatwg-url@5.0.0:
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -3824,6 +4141,18 @@ packages:
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ ws@7.5.13:
+ resolution: {integrity: sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==}
+ engines: {node: '>=8.3.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: ^5.0.2
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
ws@8.21.0:
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
engines: {node: '>=10.0.0'}
@@ -3877,6 +4206,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
+ zod@4.4.3:
+ resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
+
snapshots:
'@3fv/guard@1.4.39':
@@ -3889,6 +4221,8 @@ snapshots:
hamt_plus: 1.0.2
list: 2.0.19
+ '@adraffy/ens-normalize@1.11.1': {}
+
'@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)
@@ -4218,6 +4552,8 @@ snapshots:
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.29.7
+ '@babel/runtime@7.29.7': {}
+
'@babel/template@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
@@ -4243,6 +4579,35 @@ snapshots:
'@bcoe/v8-coverage@0.2.3': {}
+ '@coral-xyz/anchor-errors@0.31.1': {}
+
+ '@coral-xyz/anchor@0.32.1(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6)':
+ dependencies:
+ '@coral-xyz/anchor-errors': 0.31.1
+ '@coral-xyz/borsh': 0.31.1(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6))
+ '@noble/hashes': 1.8.0
+ '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6)
+ bn.js: 5.2.4
+ bs58: 4.0.1
+ buffer-layout: 1.2.2
+ camelcase: 6.3.0
+ cross-fetch: 3.2.0
+ eventemitter3: 4.0.7
+ pako: 2.2.0
+ superstruct: 0.15.5
+ toml: 3.0.0
+ transitivePeerDependencies:
+ - bufferutil
+ - encoding
+ - typescript
+ - utf-8-validate
+
+ '@coral-xyz/borsh@0.31.1(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6))':
+ dependencies:
+ '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6)
+ bn.js: 5.2.4
+ buffer-layout: 1.2.2
+
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
@@ -4507,7 +4872,7 @@ snapshots:
'@jest/console@30.4.1':
dependencies:
'@jest/types': 30.4.1
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
chalk: 4.1.2
jest-message-util: 30.4.1
jest-util: 30.4.1
@@ -4521,7 +4886,7 @@ snapshots:
'@jest/test-result': 30.4.1
'@jest/transform': 30.4.1
'@jest/types': 30.4.1
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 4.4.0
@@ -4529,7 +4894,7 @@ snapshots:
fast-json-stable-stringify: 2.1.0
graceful-fs: 4.2.11
jest-changed-files: 30.4.1
- jest-config: 30.4.2(@types/node@25.5.0)(ts-node@10.9.2(@types/node@25.5.0)(typescript@6.0.2))
+ jest-config: 30.4.2(@types/node@22.20.0)(ts-node@10.9.2(@types/node@25.5.0)(typescript@6.0.2))
jest-haste-map: 30.4.1
jest-message-util: 30.4.1
jest-regex-util: 30.4.0
@@ -4551,22 +4916,22 @@ snapshots:
'@jest/diff-sequences@30.4.0': {}
- '@jest/environment-jsdom-abstract@30.4.1(jsdom@26.1.0)':
+ '@jest/environment-jsdom-abstract@30.4.1(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))':
dependencies:
'@jest/environment': 30.4.1
'@jest/fake-timers': 30.4.1
'@jest/types': 30.4.1
'@types/jsdom': 21.1.7
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
jest-mock: 30.4.1
jest-util: 30.4.1
- jsdom: 26.1.0
+ jsdom: 26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
'@jest/environment@30.4.1':
dependencies:
'@jest/fake-timers': 30.4.1
'@jest/types': 30.4.1
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
jest-mock: 30.4.1
'@jest/expect-utils@30.4.1':
@@ -4584,7 +4949,7 @@ snapshots:
dependencies:
'@jest/types': 30.4.1
'@sinonjs/fake-timers': 15.4.0
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
jest-message-util: 30.4.1
jest-mock: 30.4.1
jest-util: 30.4.1
@@ -4602,7 +4967,7 @@ snapshots:
'@jest/pattern@30.4.0':
dependencies:
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
jest-regex-util: 30.4.0
'@jest/reporters@30.4.1':
@@ -4613,7 +4978,7 @@ snapshots:
'@jest/transform': 30.4.1
'@jest/types': 30.4.1
'@jridgewell/trace-mapping': 0.3.31
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
chalk: 4.1.2
collect-v8-coverage: 1.0.3
exit-x: 0.2.2
@@ -4689,7 +5054,7 @@ snapshots:
'@jest/schemas': 30.4.1
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
'@types/yargs': 17.0.35
chalk: 4.1.2
@@ -4858,10 +5223,16 @@ snapshots:
'@tybys/wasm-util': 0.10.3
optional: true
+ '@noble/curves@1.2.0':
+ dependencies:
+ '@noble/hashes': 1.3.2
+
'@noble/curves@1.9.7':
dependencies:
'@noble/hashes': 1.8.0
+ '@noble/hashes@1.3.2': {}
+
'@noble/hashes@1.4.0': {}
'@noble/hashes@1.8.0': {}
@@ -4965,8 +5336,6 @@ snapshots:
'@pkgr/core@0.3.6': {}
- '@protobuf-ts/runtime@2.11.1': {}
-
'@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)':
dependencies:
'@standard-schema/spec': 1.1.0
@@ -5022,10 +5391,146 @@ snapshots:
dependencies:
tslib: 2.8.1
+ '@solana/buffer-layout-utils@0.2.0(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6)':
+ dependencies:
+ '@solana/buffer-layout': 4.0.1
+ '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6)
+ bigint-buffer: 1.1.5
+ bignumber.js: 9.3.1
+ transitivePeerDependencies:
+ - bufferutil
+ - encoding
+ - typescript
+ - utf-8-validate
+
+ '@solana/buffer-layout@4.0.1':
+ dependencies:
+ buffer: 6.0.3
+
+ '@solana/codecs-core@2.0.0-rc.1(typescript@6.0.2)':
+ dependencies:
+ '@solana/errors': 2.0.0-rc.1(typescript@6.0.2)
+ typescript: 6.0.2
+
+ '@solana/codecs-core@2.3.0(typescript@6.0.2)':
+ dependencies:
+ '@solana/errors': 2.3.0(typescript@6.0.2)
+ typescript: 6.0.2
+
+ '@solana/codecs-data-structures@2.0.0-rc.1(typescript@6.0.2)':
+ dependencies:
+ '@solana/codecs-core': 2.0.0-rc.1(typescript@6.0.2)
+ '@solana/codecs-numbers': 2.0.0-rc.1(typescript@6.0.2)
+ '@solana/errors': 2.0.0-rc.1(typescript@6.0.2)
+ typescript: 6.0.2
+
+ '@solana/codecs-numbers@2.0.0-rc.1(typescript@6.0.2)':
+ dependencies:
+ '@solana/codecs-core': 2.0.0-rc.1(typescript@6.0.2)
+ '@solana/errors': 2.0.0-rc.1(typescript@6.0.2)
+ typescript: 6.0.2
+
+ '@solana/codecs-numbers@2.3.0(typescript@6.0.2)':
+ dependencies:
+ '@solana/codecs-core': 2.3.0(typescript@6.0.2)
+ '@solana/errors': 2.3.0(typescript@6.0.2)
+ typescript: 6.0.2
+
+ '@solana/codecs-strings@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@6.0.2)':
+ dependencies:
+ '@solana/codecs-core': 2.0.0-rc.1(typescript@6.0.2)
+ '@solana/codecs-numbers': 2.0.0-rc.1(typescript@6.0.2)
+ '@solana/errors': 2.0.0-rc.1(typescript@6.0.2)
+ fastestsmallesttextencoderdecoder: 1.0.22
+ typescript: 6.0.2
+
+ '@solana/codecs@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@6.0.2)':
+ dependencies:
+ '@solana/codecs-core': 2.0.0-rc.1(typescript@6.0.2)
+ '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@6.0.2)
+ '@solana/codecs-numbers': 2.0.0-rc.1(typescript@6.0.2)
+ '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@6.0.2)
+ '@solana/options': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@6.0.2)
+ typescript: 6.0.2
+ transitivePeerDependencies:
+ - fastestsmallesttextencoderdecoder
+
+ '@solana/errors@2.0.0-rc.1(typescript@6.0.2)':
+ dependencies:
+ chalk: 5.6.2
+ commander: 12.1.0
+ typescript: 6.0.2
+
+ '@solana/errors@2.3.0(typescript@6.0.2)':
+ dependencies:
+ chalk: 5.6.2
+ commander: 14.0.3
+ typescript: 6.0.2
+
+ '@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@6.0.2)':
+ dependencies:
+ '@solana/codecs-core': 2.0.0-rc.1(typescript@6.0.2)
+ '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@6.0.2)
+ '@solana/codecs-numbers': 2.0.0-rc.1(typescript@6.0.2)
+ '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@6.0.2)
+ '@solana/errors': 2.0.0-rc.1(typescript@6.0.2)
+ typescript: 6.0.2
+ transitivePeerDependencies:
+ - fastestsmallesttextencoderdecoder
+
+ '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@6.0.2)':
+ dependencies:
+ '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@6.0.2)
+ '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6)
+ transitivePeerDependencies:
+ - fastestsmallesttextencoderdecoder
+ - typescript
+
+ '@solana/spl-token@0.3.11(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@6.0.2)(utf-8-validate@6.0.6)':
+ dependencies:
+ '@solana/buffer-layout': 4.0.1
+ '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6)
+ '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@6.0.2)
+ '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6)
+ buffer: 6.0.3
+ transitivePeerDependencies:
+ - bufferutil
+ - encoding
+ - fastestsmallesttextencoderdecoder
+ - typescript
+ - utf-8-validate
+
+ '@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@6.0.2)(utf-8-validate@6.0.6)':
+ dependencies:
+ '@babel/runtime': 7.29.7
+ '@noble/curves': 1.9.7
+ '@noble/hashes': 1.8.0
+ '@solana/buffer-layout': 4.0.1
+ '@solana/codecs-numbers': 2.3.0(typescript@6.0.2)
+ agentkeepalive: 4.6.0
+ bn.js: 5.2.4
+ borsh: 0.7.0
+ bs58: 4.0.1
+ buffer: 6.0.3
+ fast-stable-stringify: 1.0.0
+ jayson: 4.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
+ node-fetch: 2.7.0
+ rpc-websockets: 9.3.8
+ superstruct: 2.0.2
+ transitivePeerDependencies:
+ - bufferutil
+ - encoding
+ - typescript
+ - utf-8-validate
+
'@standard-schema/spec@1.1.0': {}
'@standard-schema/utils@0.3.0': {}
+ '@swc/helpers@0.5.23':
+ dependencies:
+ tslib: 2.8.1
+
'@tsconfig/node10@1.0.12': {}
'@tsconfig/node12@1.0.11': {}
@@ -5153,7 +5658,7 @@ snapshots:
'@types/jsdom@21.1.7':
dependencies:
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
'@types/tough-cookie': 4.0.5
parse5: 7.3.0
@@ -5165,10 +5670,16 @@ snapshots:
'@types/ms@2.1.0': {}
+ '@types/node@12.20.55': {}
+
'@types/node@22.20.0':
dependencies:
undici-types: 6.21.0
+ '@types/node@22.7.5':
+ dependencies:
+ undici-types: 6.19.8
+
'@types/node@25.5.0':
dependencies:
undici-types: 7.18.2
@@ -5204,6 +5715,12 @@ snapshots:
'@types/use-sync-external-store@0.0.6': {}
+ '@types/uuid@10.0.0': {}
+
+ '@types/ws@7.4.7':
+ dependencies:
+ '@types/node': 22.20.0
+
'@types/ws@8.18.1':
dependencies:
'@types/node': 22.20.0
@@ -5468,16 +5985,18 @@ snapshots:
webpack: 5.104.1(webpack-cli@6.0.1)
webpack-cli: 6.0.1(webpack-dev-server@6.0.0)(webpack@5.104.1)
optionalDependencies:
- webpack-dev-server: 6.0.0(tslib@2.8.1)(webpack-cli@6.0.1)(webpack@5.104.1)
+ webpack-dev-server: 6.0.0(bufferutil@4.1.0)(tslib@2.8.1)(utf-8-validate@6.0.6)(webpack-cli@6.0.1)(webpack@5.104.1)
'@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.104.1)':
dependencies:
webpack: 5.104.1(postcss@8.5.16)(webpack-cli@6.0.1)
webpack-cli: 6.0.1(webpack@5.104.1)
- '@wireio/opp-typescript-models@1.0.48':
+ '@wireio/outpost-ethereum-artifacts@0.3.0(ethers@6.17.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))':
dependencies:
- '@protobuf-ts/runtime': 2.11.1
+ ethers: 6.17.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
+
+ '@wireio/outpost-solana-artifacts@0.3.0': {}
'@xtuc/ieee754@1.2.0': {}
@@ -5507,8 +6026,14 @@ snapshots:
acorn@8.17.0: {}
+ aes-js@4.0.0-beta.5: {}
+
agent-base@7.1.4: {}
+ agentkeepalive@4.6.0:
+ dependencies:
+ humanize-ms: 1.2.1
+
ajv-formats@2.1.1(ajv@8.20.0):
optionalDependencies:
ajv: 8.20.0
@@ -5623,10 +6148,26 @@ snapshots:
balanced-match@4.0.4: {}
+ base-x@3.0.11:
+ dependencies:
+ safe-buffer: 5.2.1
+
+ base64-js@1.5.1: {}
+
baseline-browser-mapping@2.10.42: {}
batch@0.6.1: {}
+ bigint-buffer@1.1.5:
+ dependencies:
+ bindings: 1.5.0
+
+ bignumber.js@9.3.1: {}
+
+ bindings@1.5.0:
+ dependencies:
+ file-uri-to-path: 1.0.0
+
bluebird@3.7.2: {}
bn.js@4.12.4: {}
@@ -5654,6 +6195,12 @@ snapshots:
boolbase@1.0.0: {}
+ borsh@0.7.0:
+ dependencies:
+ bn.js: 5.2.4
+ bs58: 4.0.1
+ text-encoding-utf-8: 1.0.2
+
bowser@2.14.1: {}
brace-expansion@1.1.15:
@@ -5687,12 +6234,28 @@ snapshots:
dependencies:
fast-json-stable-stringify: 2.1.0
+ bs58@4.0.1:
+ dependencies:
+ base-x: 3.0.11
+
bser@2.1.1:
dependencies:
node-int64: 0.4.0
buffer-from@1.1.2: {}
+ buffer-layout@1.2.2: {}
+
+ buffer@6.0.3:
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+
+ bufferutil@4.1.0:
+ dependencies:
+ node-gyp-build: 4.8.4
+ optional: true
+
bundle-name@4.1.0:
dependencies:
run-applescript: 7.1.0
@@ -5729,6 +6292,8 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
+ chalk@5.6.2: {}
+
char-regex@1.0.2: {}
chokidar@5.0.0:
@@ -5773,6 +6338,8 @@ snapshots:
commander@12.1.0: {}
+ commander@14.0.3: {}
+
commander@2.20.3: {}
commander@8.3.0: {}
@@ -5829,6 +6396,12 @@ snapshots:
create-require@1.1.1: {}
+ cross-fetch@3.2.0:
+ dependencies:
+ node-fetch: 2.7.0
+ transitivePeerDependencies:
+ - encoding
+
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -5903,6 +6476,8 @@ snapshots:
define-lazy-prop@3.0.0: {}
+ delay@5.0.0: {}
+
depd@1.1.2: {}
depd@2.0.0: {}
@@ -5997,6 +6572,12 @@ snapshots:
dependencies:
es-errors: 1.3.0
+ es6-promise@4.2.8: {}
+
+ es6-promisify@5.0.0:
+ dependencies:
+ es6-promise: 4.2.8
+
escalade@3.2.0: {}
escape-html@1.0.3: {}
@@ -6084,6 +6665,21 @@ snapshots:
etag@1.8.1: {}
+ ethers@6.17.0(bufferutil@4.1.0)(utf-8-validate@6.0.6):
+ dependencies:
+ '@adraffy/ens-normalize': 1.11.1
+ '@noble/curves': 1.2.0
+ '@noble/hashes': 1.3.2
+ '@types/node': 22.7.5
+ aes-js: 4.0.0-beta.5
+ tslib: 2.7.0
+ ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
+ eventemitter3@4.0.7: {}
+
eventemitter3@5.0.4: {}
events@3.3.0: {}
@@ -6144,16 +6740,22 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ eyes@0.1.8: {}
+
fast-deep-equal@3.1.3: {}
fast-json-stable-stringify@2.1.0: {}
fast-levenshtein@2.0.6: {}
+ fast-stable-stringify@1.0.0: {}
+
fast-uri@3.1.3: {}
fastest-levenshtein@1.0.16: {}
+ fastestsmallesttextencoderdecoder@1.0.22: {}
+
fb-watchman@2.0.2:
dependencies:
bser: 2.1.1
@@ -6166,6 +6768,8 @@ snapshots:
dependencies:
flat-cache: 4.0.1
+ file-uri-to-path@1.0.0: {}
+
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@@ -6383,6 +6987,10 @@ snapshots:
human-signals@2.1.0: {}
+ humanize-ms@1.2.1:
+ dependencies:
+ ms: 2.1.3
+
husky@9.1.7: {}
hyperdyperid@1.2.0: {}
@@ -6399,6 +7007,8 @@ snapshots:
dependencies:
postcss: 8.5.16
+ ieee754@1.2.1: {}
+
ignore@5.3.2: {}
ignore@7.0.6: {}
@@ -6473,6 +7083,10 @@ snapshots:
isobject@3.0.1: {}
+ isomorphic-ws@4.0.1(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)):
+ dependencies:
+ ws: 7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)
+
istanbul-lib-coverage@3.2.2: {}
istanbul-lib-instrument@6.0.3:
@@ -6510,6 +7124,24 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
+ jayson@4.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6):
+ dependencies:
+ '@types/connect': 3.4.38
+ '@types/node': 12.20.55
+ '@types/ws': 7.4.7
+ commander: 2.20.3
+ delay: 5.0.0
+ es6-promisify: 5.0.0
+ eyes: 0.1.8
+ isomorphic-ws: 4.0.1(ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6))
+ json-stringify-safe: 5.0.1
+ stream-json: 1.9.1
+ uuid: 8.3.2
+ ws: 7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6)
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
jest-changed-files@30.4.1:
dependencies:
execa: 5.1.1
@@ -6522,7 +7154,7 @@ snapshots:
'@jest/expect': 30.4.1
'@jest/test-result': 30.4.1
'@jest/types': 30.4.1
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
chalk: 4.1.2
co: 4.6.0
dedent: 1.7.2
@@ -6561,6 +7193,38 @@ snapshots:
- supports-color
- ts-node
+ jest-config@30.4.2(@types/node@22.20.0)(ts-node@10.9.2(@types/node@25.5.0)(typescript@6.0.2)):
+ dependencies:
+ '@babel/core': 7.29.7
+ '@jest/get-type': 30.1.0
+ '@jest/pattern': 30.4.0
+ '@jest/test-sequencer': 30.4.1
+ '@jest/types': 30.4.1
+ babel-jest: 30.4.1(@babel/core@7.29.7)
+ chalk: 4.1.2
+ ci-info: 4.4.0
+ deepmerge: 4.3.1
+ glob: 10.5.0
+ graceful-fs: 4.2.11
+ jest-circus: 30.4.2
+ jest-docblock: 30.4.0
+ jest-environment-node: 30.4.1
+ jest-regex-util: 30.4.0
+ jest-resolve: 30.4.1
+ jest-runner: 30.4.2
+ jest-util: 30.4.1
+ jest-validate: 30.4.1
+ parse-json: 5.2.0
+ pretty-format: 30.4.1
+ slash: 3.0.0
+ strip-json-comments: 3.1.1
+ optionalDependencies:
+ '@types/node': 22.20.0
+ ts-node: 10.9.2(@types/node@25.5.0)(typescript@6.0.2)
+ transitivePeerDependencies:
+ - babel-plugin-macros
+ - supports-color
+
jest-config@30.4.2(@types/node@25.5.0)(ts-node@10.9.2(@types/node@25.5.0)(typescript@6.0.2)):
dependencies:
'@babel/core': 7.29.7
@@ -6612,11 +7276,11 @@ snapshots:
jest-util: 30.4.1
pretty-format: 30.4.1
- jest-environment-jsdom@30.4.1:
+ jest-environment-jsdom@30.4.1(bufferutil@4.1.0)(utf-8-validate@6.0.6):
dependencies:
'@jest/environment': 30.4.1
- '@jest/environment-jsdom-abstract': 30.4.1(jsdom@26.1.0)
- jsdom: 26.1.0
+ '@jest/environment-jsdom-abstract': 30.4.1(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))
+ jsdom: 26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -6627,7 +7291,7 @@ snapshots:
'@jest/environment': 30.4.1
'@jest/fake-timers': 30.4.1
'@jest/types': 30.4.1
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
jest-mock: 30.4.1
jest-util: 30.4.1
jest-validate: 30.4.1
@@ -6635,7 +7299,7 @@ snapshots:
jest-haste-map@30.4.1:
dependencies:
'@jest/types': 30.4.1
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
anymatch: 3.1.3
fb-watchman: 2.0.2
graceful-fs: 4.2.11
@@ -6682,7 +7346,7 @@ snapshots:
jest-mock@30.4.1:
dependencies:
'@jest/types': 30.4.1
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
jest-util: 30.4.1
jest-pnp-resolver@1.2.3(jest-resolve@30.4.1):
@@ -6716,7 +7380,7 @@ snapshots:
'@jest/test-result': 30.4.1
'@jest/transform': 30.4.1
'@jest/types': 30.4.1
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
chalk: 4.1.2
emittery: 0.13.1
exit-x: 0.2.2
@@ -6745,7 +7409,7 @@ snapshots:
'@jest/test-result': 30.4.1
'@jest/transform': 30.4.1
'@jest/types': 30.4.1
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
chalk: 4.1.2
cjs-module-lexer: 2.2.0
collect-v8-coverage: 1.0.3
@@ -6792,7 +7456,7 @@ snapshots:
jest-util@30.4.1:
dependencies:
'@jest/types': 30.4.1
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
chalk: 4.1.2
ci-info: 4.4.0
graceful-fs: 4.2.11
@@ -6811,7 +7475,7 @@ snapshots:
dependencies:
'@jest/test-result': 30.4.1
'@jest/types': 30.4.1
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
ansi-escapes: 4.3.2
chalk: 4.1.2
emittery: 0.13.1
@@ -6826,7 +7490,7 @@ snapshots:
jest-worker@30.4.1:
dependencies:
- '@types/node': 25.5.0
+ '@types/node': 22.20.0
'@ungap/structured-clone': 1.3.2
jest-util: 30.4.1
merge-stream: 2.0.0
@@ -6856,7 +7520,7 @@ snapshots:
argparse: 1.0.10
esprima: 4.0.1
- jsdom@26.1.0:
+ jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6):
dependencies:
cssstyle: 4.6.0
data-urls: 5.0.0
@@ -6876,7 +7540,7 @@ snapshots:
whatwg-encoding: 3.1.1
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
- ws: 8.21.0
+ ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
xml-name-validator: 5.0.0
transitivePeerDependencies:
- bufferutil
@@ -6895,6 +7559,8 @@ snapshots:
json-stable-stringify-without-jsonify@1.0.1: {}
+ json-stringify-safe@5.0.1: {}
+
json5@2.2.3: {}
keyv@4.5.4:
@@ -7055,6 +7721,13 @@ snapshots:
lower-case: 2.0.2
tslib: 2.8.1
+ node-fetch@2.7.0:
+ dependencies:
+ whatwg-url: 5.0.0
+
+ node-gyp-build@4.8.4:
+ optional: true
+
node-int64@0.4.0: {}
node-releases@2.0.50: {}
@@ -7344,6 +8017,19 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ rpc-websockets@9.3.8:
+ dependencies:
+ '@swc/helpers': 0.5.23
+ '@types/uuid': 10.0.0
+ '@types/ws': 8.18.1
+ buffer: 6.0.3
+ eventemitter3: 5.0.4
+ uuid: 11.1.1
+ ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
+ optionalDependencies:
+ bufferutil: 4.1.0
+ utf-8-validate: 6.0.6
+
rrweb-cssom@0.8.0: {}
run-applescript@7.1.0: {}
@@ -7493,6 +8179,12 @@ snapshots:
statuses@2.0.2: {}
+ stream-chain@2.2.5: {}
+
+ stream-json@1.9.1:
+ dependencies:
+ stream-chain: 2.2.5
+
string-length@4.0.2:
dependencies:
char-regex: 1.0.2
@@ -7528,6 +8220,10 @@ snapshots:
dependencies:
webpack: 5.104.1(postcss@8.5.16)(webpack-cli@6.0.1)
+ superstruct@0.15.5: {}
+
+ superstruct@2.0.2: {}
+
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@@ -7577,6 +8273,8 @@ snapshots:
glob: 7.2.3
minimatch: 3.1.5
+ text-encoding-utf-8@1.0.2: {}
+
thingies@2.6.0(tslib@2.8.1):
dependencies:
tslib: 2.8.1
@@ -7604,10 +8302,14 @@ snapshots:
toidentifier@1.0.1: {}
+ toml@3.0.0: {}
+
tough-cookie@5.1.2:
dependencies:
tldts: 6.1.86
+ tr46@0.0.3: {}
+
tr46@5.1.1:
dependencies:
punycode: 2.3.1
@@ -7679,6 +8381,8 @@ snapshots:
tslib@1.14.1: {}
+ tslib@2.7.0: {}
+
tslib@2.8.1: {}
tsyringe@4.10.0:
@@ -7719,6 +8423,8 @@ snapshots:
uglify-js@3.19.3:
optional: true
+ undici-types@6.19.8: {}
+
undici-types@6.21.0: {}
undici-types@7.18.2: {}
@@ -7766,12 +8472,21 @@ snapshots:
dependencies:
react: 19.2.7
+ utf-8-validate@6.0.6:
+ dependencies:
+ node-gyp-build: 4.8.4
+ optional: true
+
util-deprecate@1.0.2: {}
utila@0.4.0: {}
+ uuid@11.1.1: {}
+
uuid@14.0.1: {}
+ uuid@8.3.2: {}
+
v8-compile-cache-lib@3.0.1: {}
v8-to-istanbul@9.3.0:
@@ -7794,6 +8509,8 @@ snapshots:
dependencies:
graceful-fs: 4.2.11
+ webidl-conversions@3.0.1: {}
+
webidl-conversions@7.0.0: {}
webpack-cli@6.0.1(webpack-dev-server@6.0.0)(webpack@5.104.1):
@@ -7813,7 +8530,7 @@ snapshots:
webpack: 5.104.1(webpack-cli@6.0.1)
webpack-merge: 6.0.1
optionalDependencies:
- webpack-dev-server: 6.0.0(tslib@2.8.1)(webpack-cli@6.0.1)(webpack@5.104.1)
+ webpack-dev-server: 6.0.0(bufferutil@4.1.0)(tslib@2.8.1)(utf-8-validate@6.0.6)(webpack-cli@6.0.1)(webpack@5.104.1)
webpack-cli@6.0.1(webpack@5.104.1):
dependencies:
@@ -7844,7 +8561,7 @@ snapshots:
transitivePeerDependencies:
- tslib
- webpack-dev-server@6.0.0(tslib@2.8.1)(webpack-cli@6.0.1)(webpack@5.104.1):
+ webpack-dev-server@6.0.0(bufferutil@4.1.0)(tslib@2.8.1)(utf-8-validate@6.0.6)(webpack-cli@6.0.1)(webpack@5.104.1):
dependencies:
'@types/bonjour': 3.5.13
'@types/connect-history-api-fallback': 1.5.4
@@ -7870,7 +8587,7 @@ snapshots:
serve-index: 1.9.2
tinyglobby: 0.2.17
webpack-dev-middleware: 8.0.3(tslib@2.8.1)(webpack@5.104.1)
- ws: 8.21.0
+ ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
optionalDependencies:
webpack: 5.104.1(webpack-cli@6.0.1)
webpack-cli: 6.0.1(webpack-dev-server@6.0.0)(webpack@5.104.1)
@@ -7985,6 +8702,11 @@ snapshots:
tr46: 5.1.1
webidl-conversions: 7.0.0
+ whatwg-url@5.0.0:
+ dependencies:
+ tr46: 0.0.3
+ webidl-conversions: 3.0.1
+
which@2.0.2:
dependencies:
isexe: 2.0.0
@@ -8014,7 +8736,15 @@ snapshots:
imurmurhash: 0.1.4
signal-exit: 4.1.0
- ws@8.21.0: {}
+ ws@7.5.13(bufferutil@4.1.0)(utf-8-validate@6.0.6):
+ optionalDependencies:
+ bufferutil: 4.1.0
+ utf-8-validate: 6.0.6
+
+ ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6):
+ optionalDependencies:
+ bufferutil: 4.1.0
+ utf-8-validate: 6.0.6
wsl-utils@0.3.1:
dependencies:
@@ -8056,3 +8786,5 @@ snapshots:
yn@3.1.1: {}
yocto-queue@0.1.0: {}
+
+ zod@4.4.3: {}
diff --git a/tsconfig.json b/tsconfig.json
index 4e7e765..670cf71 100755
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -26,6 +26,9 @@
{
"path": "./packages/sdk-core/tsconfig.json"
},
+ {
+ "path": "./packages/sdk-outpost/tsconfig.json"
+ },
{
"path": "./packages/wallet-ext-sdk/tsconfig.json"
},
@@ -36,4 +39,4 @@
"path": "./examples/web-logging-example/tsconfig.json"
}
]
-}
\ No newline at end of file
+}