From b7f76034551092c94ea5292dce499d7a77fc42e6 Mon Sep 17 00:00:00 2001 From: JoE11-y Date: Sun, 12 Apr 2026 14:27:38 +0100 Subject: [PATCH] feat: add stellar support to backend relayer and update unit and e2e tests --- apps/backend-relayer/.env.example | 7 +- .../docs/auth-sep10-upgrade.md | 111 ++++ apps/backend-relayer/package.json | 4 + .../migration.sql | 3 + apps/backend-relayer/prisma/schema.prisma | 2 + .../adapters/chain-adapter.abstract.ts} | 20 +- .../adapters/evm-chain-adapter.ts} | 74 ++- .../adapters/stellar-chain-adapter.ts | 196 ++++++ .../chain-adapters/chain-adapter.module.ts | 13 + .../chain-adapters/chain-adapter.service.ts | 25 + .../viem => chain-adapters}/types.ts | 54 +- apps/backend-relayer/src/libs/configs.ts | 12 + .../src/modules/ads/ad.module.ts | 4 +- .../src/modules/ads/ad.service.ts | 14 +- .../src/modules/ads/dto/ad.dto.ts | 8 +- .../src/modules/auth/auth.controller.spec.ts | 89 ++- .../src/modules/auth/auth.controller.ts | 4 +- .../src/modules/auth/auth.module.ts | 4 +- .../src/modules/auth/auth.service.ts | 230 ++----- .../src/modules/auth/dto/auth.dto.ts | 117 +++- .../src/modules/auth/evm/evm-auth.service.ts | 145 +++++ .../auth/stellar/stellar-auth.service.spec.ts | 129 ++++ .../auth/stellar/stellar-auth.service.ts | 168 +++++ .../src/modules/auth/username.util.ts | 14 + .../src/modules/chains/dto/chain.dto.ts | 10 +- .../src/modules/faucet/faucet.module.ts | 4 +- .../src/modules/faucet/faucet.service.ts | 6 +- .../src/modules/trades/trade.module.ts | 4 +- .../src/modules/trades/trade.service.ts | 24 +- .../providers/chain/chain-provider.service.ts | 25 - .../src/providers/chain/chain.module.ts | 11 - .../src/providers/stellar/stellar.module.ts | 10 + .../src/providers/stellar/stellar.service.ts | 599 ++++++++++++++++++ .../src/providers/stellar/utils/address.ts | 49 ++ .../src/providers/stellar/utils/eip712.ts | 68 ++ .../src/providers/stellar/utils/signing.ts | 251 ++++++++ .../providers/stellar/wasm/ad_manager.wasm | Bin 0 -> 45557 bytes .../stellar/wasm/merkle_manager.wasm | Bin 0 -> 29512 bytes .../providers/stellar/wasm/order_portal.wasm | Bin 0 -> 36207 bytes .../providers/stellar/wasm/test_token.wasm | Bin 0 -> 67376 bytes .../src/providers/stellar/wasm/verifier.wasm | Bin 0 -> 28044 bytes .../providers/viem/{ => ethers}/localnet.ts | 0 .../src/providers/viem/ethers/typedData.ts | 2 +- .../src/providers/viem/viem.service.ts | 4 +- apps/backend-relayer/test/e2e/ads.e2e-spec.ts | 149 ++--- .../backend-relayer/test/e2e/auth.e2e-spec.ts | 23 +- .../e2e/health.e2e-spec.ts} | 2 +- apps/backend-relayer/test/e2e/jest-e2e.json | 2 + .../test/e2e/trade-e2e-spec.ts | 270 +++----- .../eth-hedera-e2e-integration.ts | 582 ----------------- .../eth-stellar.e2e-integration.ts | 404 ++++++++++++ .../test/integrations/jest-e2e.json | 1 + .../backend-relayer/test/setups/create-app.ts | 9 +- .../{contract-actions.ts => evm-actions.ts} | 2 +- ...racts.json => evm-deployed-contracts.json} | 0 .../{contract-setup.ts => evm-setup.ts} | 64 +- .../setups/hedera-deployed-contracts.json | 11 - .../test/setups/jest-e2e.setup.ts | 9 + .../test/setups/jest-integrations.setup.ts | 24 +- .../test/setups/jest.teardown.ts | 7 + .../test/setups/mock-chain-adapter.ts | 264 ++++++++ apps/backend-relayer/test/setups/seed.ts | 64 +- .../test/setups/stellar-actions.ts | 260 ++++++++ .../test/setups/stellar-setup.ts | 300 +++++++++ apps/backend-relayer/test/setups/utils.ts | 95 +-- apps/backend-relayer/tsconfig.json | 1 + pnpm-lock.yaml | 316 +++++---- 67 files changed, 3891 insertions(+), 1482 deletions(-) create mode 100644 apps/backend-relayer/docs/auth-sep10-upgrade.md create mode 100644 apps/backend-relayer/prisma/migrations/20260412132246_extend_token_kind_for_stellar/migration.sql rename apps/backend-relayer/src/{providers/chain/chain-provider.abstract.ts => chain-adapters/adapters/chain-adapter.abstract.ts} (84%) rename apps/backend-relayer/src/{providers/chain/evm-chain-provider.ts => chain-adapters/adapters/evm-chain-adapter.ts} (55%) create mode 100644 apps/backend-relayer/src/chain-adapters/adapters/stellar-chain-adapter.ts create mode 100644 apps/backend-relayer/src/chain-adapters/chain-adapter.module.ts create mode 100644 apps/backend-relayer/src/chain-adapters/chain-adapter.service.ts rename apps/backend-relayer/src/{providers/viem => chain-adapters}/types.ts (77%) create mode 100644 apps/backend-relayer/src/modules/auth/evm/evm-auth.service.ts create mode 100644 apps/backend-relayer/src/modules/auth/stellar/stellar-auth.service.spec.ts create mode 100644 apps/backend-relayer/src/modules/auth/stellar/stellar-auth.service.ts create mode 100644 apps/backend-relayer/src/modules/auth/username.util.ts delete mode 100644 apps/backend-relayer/src/providers/chain/chain-provider.service.ts delete mode 100644 apps/backend-relayer/src/providers/chain/chain.module.ts create mode 100644 apps/backend-relayer/src/providers/stellar/stellar.module.ts create mode 100644 apps/backend-relayer/src/providers/stellar/stellar.service.ts create mode 100644 apps/backend-relayer/src/providers/stellar/utils/address.ts create mode 100644 apps/backend-relayer/src/providers/stellar/utils/eip712.ts create mode 100644 apps/backend-relayer/src/providers/stellar/utils/signing.ts create mode 100644 apps/backend-relayer/src/providers/stellar/wasm/ad_manager.wasm create mode 100644 apps/backend-relayer/src/providers/stellar/wasm/merkle_manager.wasm create mode 100644 apps/backend-relayer/src/providers/stellar/wasm/order_portal.wasm create mode 100755 apps/backend-relayer/src/providers/stellar/wasm/test_token.wasm create mode 100644 apps/backend-relayer/src/providers/stellar/wasm/verifier.wasm rename apps/backend-relayer/src/providers/viem/{ => ethers}/localnet.ts (100%) rename apps/backend-relayer/{src/app.e2e.spec.ts => test/e2e/health.e2e-spec.ts} (92%) delete mode 100644 apps/backend-relayer/test/integrations/eth-hedera-e2e-integration.ts create mode 100644 apps/backend-relayer/test/integrations/eth-stellar.e2e-integration.ts rename apps/backend-relayer/test/setups/{contract-actions.ts => evm-actions.ts} (99%) rename apps/backend-relayer/test/setups/{eth-deployed-contracts.json => evm-deployed-contracts.json} (100%) rename apps/backend-relayer/test/setups/{contract-setup.ts => evm-setup.ts} (68%) delete mode 100644 apps/backend-relayer/test/setups/hedera-deployed-contracts.json create mode 100644 apps/backend-relayer/test/setups/mock-chain-adapter.ts create mode 100644 apps/backend-relayer/test/setups/stellar-actions.ts create mode 100644 apps/backend-relayer/test/setups/stellar-setup.ts diff --git a/apps/backend-relayer/.env.example b/apps/backend-relayer/.env.example index 0c1998f..df6278a 100644 --- a/apps/backend-relayer/.env.example +++ b/apps/backend-relayer/.env.example @@ -8,4 +8,9 @@ NODE_ENV="" PORT="" SECRET_KEY="" SIGN_DOMAIN=proof-bridge.vercel.app -SIGN_URI=https://proof-bridge.vercel.app \ No newline at end of file +SIGN_URI=https://proof-bridge.vercel.app +# Stellar SEP-10 server signing key. Generate with: Keypair.random().secret() +STELLAR_AUTH_SECRET="" +STELLAR_RPC_URL="" +STELLAR_NETWORK_PASSPHRASE="" +STELLAR_ADMIN_SECRET="" \ No newline at end of file diff --git a/apps/backend-relayer/docs/auth-sep10-upgrade.md b/apps/backend-relayer/docs/auth-sep10-upgrade.md new file mode 100644 index 0000000..34f680d --- /dev/null +++ b/apps/backend-relayer/docs/auth-sep10-upgrade.md @@ -0,0 +1,111 @@ +# Stellar Auth — SEP-10 Upgrade Path + +Status: **not implemented** — captured here for a future iteration. + +Link: https://developers.stellar.org/docs/build/apps/wallet/sep10 + +## Where we are today + +The relayer already speaks **SEP-10 Layer 1** (the challenge transaction format). What we do **not** expose is the canonical Layer 2 HTTP surface and discovery metadata that third-party Stellar SDKs expect. + +Current flow (manual, frontend-driven): + +1. `POST /v1/auth/challenge` with `{ chainKind: "STELLAR", address }` + - Server builds the SEP-10 challenge via `WebAuth.buildChallengeTx` in `StellarAuthService.buildChallenge`. + - Returns `{ transaction, networkPassphrase, address, expiresAt }`. +2. Frontend hands the XDR to the user's wallet (Freighter, Albedo, Lobstr, wallet-kit, etc.) and asks it to co-sign. +3. `POST /v1/auth/login` with `{ chainKind: "STELLAR", transaction: }` + - Server verifies with `WebAuth.readChallengeTx` + `WebAuth.verifyChallengeTxSigners`, records the tx hash in `AuthNonce` for replay protection, upserts the user, returns JWTs. + +This is sufficient for our own frontend because it owns both sides of the fetch. It is **not** sufficient for SDKs that expect to auto-discover the auth endpoint. + +## What canonical SEP-10 Layer 2 looks like + +Two HTTP endpoints, both commonly mounted at `/auth` on the server's public domain: + +- `GET /auth?account=&home_domain=&client_domain=` → `{ transaction, network_passphrase }` +- `POST /auth` with `{ transaction, client_domain? }` → `{ token }` + +Plus a discovery document: + +- `GET /.well-known/stellar.toml` exposing at minimum: + ```toml + NETWORK_PASSPHRASE = "Test SDF Network ; September 2015" + WEB_AUTH_ENDPOINT = "https://api.proofbridge.xyz/v1/auth/sep10" + SIGNING_KEY = "G... (public key of STELLAR_AUTH_SECRET)" + ACCOUNTS = ["G..."] # same as SIGNING_KEY, for SDKs that look here + ``` + +With those in place, a caller can do: + +```ts +const anchor = wallet.anchor({ homeDomain: 'proofbridge.xyz' }); +const auth = await anchor.sep10(); +const token = await auth.authenticate({ accountKp }); +``` + +…and the SDK handles the GET/POST dance itself. No bespoke frontend code required. + +## Trigger for the upgrade + +Adopt Layer 2 when **any** of these becomes true: + +- A third-party client (partner integration, wallet-kit flow) needs to authenticate against the relayer without hand-rolling the fetch. +- We publish a public SDK that wraps the relayer and want it to be drop-in with the Stellar ecosystem's expectations. +- We add `client_domain` attribution (proving *which* app/dapp is logging the user in, not just which wallet). + +Until then, manual fetch is fine and avoids the TOML operational overhead. + +## Implementation sketch + +Nothing about `StellarAuthService` needs to change — its `buildChallenge` / `verifyLogin` are already the right primitives. The upgrade is purely a thin HTTP layer. + +1. **New controller** `sep10.controller.ts`: + ```ts + @Controller('/v1/auth/sep10') + export class Sep10Controller { + constructor(private readonly stellarAuth: StellarAuthService, + private readonly auth: AuthService) {} + + @Get() + get(@Query('account') account: string, + @Query('home_domain') homeDomain?: string, + @Query('client_domain') clientDomain?: string) { + const { transaction, networkPassphrase } = + this.stellarAuth.buildChallenge(account, { homeDomain, clientDomain }); + return { transaction, network_passphrase: networkPassphrase }; + } + + @Post() + async post(@Body() body: { transaction: string; client_domain?: string }) { + const user = await this.stellarAuth.verifyLogin(body.transaction, { + clientDomain: body.client_domain, + }); + const { tokens } = await this.auth.issueTokensForUser(user); + return { token: tokens.access }; // SEP-10 returns a single JWT + } + } + ``` + Note the response shape: `{ token }` (singular, access only). Refresh is not part of SEP-10 — clients re-auth when it expires. Our existing `/v1/auth/login` can keep returning the `{ access, refresh }` pair for our own frontend. + +2. **stellar.toml controller** serving `GET /.well-known/stellar.toml` as `text/plain`. Values come from `env.stellar.authSecret` (public key derived once at boot) and `env.stellar.networkPassphrase`. + +3. **Optional `client_domain` support** in `StellarAuthService.buildChallenge`: + - Add a second ManageData op with key `client_domain` and value = the requested domain. + - On verify, fetch `https:///.well-known/stellar.toml`, resolve its `SIGNING_KEY`, and require that key to co-sign the challenge alongside the user. + - Lean on `StellarTomlResolver` from `@stellar/stellar-sdk` — no need to roll our own fetch. + +4. **Replay protection** stays as-is (tx-hash → `AuthNonce`). The challenge format change does not affect uniqueness. + +5. **Tests**: extend `stellar-auth.service.spec.ts` with a `clientDomain` branch; add a controller spec for `sep10.controller.ts` mirroring `auth.controller.spec.ts` shape. + +## Non-goals + +- We are **not** becoming an anchor. SEP-10 is the only Stellar SEP we plan to speak; SEP-6/24/31 etc. are out of scope. +- We are **not** deprecating `/v1/auth/login` when Layer 2 lands. The two coexist: `/sep10` for ecosystem tooling, `/login` for our own frontend's richer `{ user, tokens }` response. + +## References + +- SEP-10: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md +- `@stellar/stellar-sdk` `WebAuth` helpers (already used in `StellarAuthService`). +- `@stellar/typescript-wallet-sdk` — the SDK most likely to exercise Layer 2 against us. diff --git a/apps/backend-relayer/package.json b/apps/backend-relayer/package.json index e1e37f9..9082ad2 100644 --- a/apps/backend-relayer/package.json +++ b/apps/backend-relayer/package.json @@ -35,6 +35,7 @@ "@node-rs/argon2": "^2.0.2", "@noir-lang/noir_js": "1.0.0-beta.9", "@prisma/client": "6.16.1", + "@stellar/stellar-sdk": "^15.0.1", "@types/morgan": "^1.9.10", "@zkpassport/poseidon2": "^0.6.2", "abstract-level": "^3.1.0", @@ -99,6 +100,9 @@ "transform": { "^.+\\.(t|j)s$": "ts-jest" }, + "transformIgnorePatterns": [ + "node_modules/(?!(?:\\.pnpm/)?@noble)" + ], "collectCoverageFrom": [ "**/*.(t|j)s" ], diff --git a/apps/backend-relayer/prisma/migrations/20260412132246_extend_token_kind_for_stellar/migration.sql b/apps/backend-relayer/prisma/migrations/20260412132246_extend_token_kind_for_stellar/migration.sql new file mode 100644 index 0000000..b7901f9 --- /dev/null +++ b/apps/backend-relayer/prisma/migrations/20260412132246_extend_token_kind_for_stellar/migration.sql @@ -0,0 +1,3 @@ +-- AlterEnum +ALTER TYPE "public"."TokenKind" ADD VALUE 'SAC'; +ALTER TYPE "public"."TokenKind" ADD VALUE 'SEP41'; diff --git a/apps/backend-relayer/prisma/schema.prisma b/apps/backend-relayer/prisma/schema.prisma index e8ac2df..d4c46e1 100644 --- a/apps/backend-relayer/prisma/schema.prisma +++ b/apps/backend-relayer/prisma/schema.prisma @@ -45,6 +45,8 @@ model AuthNonce { enum TokenKind { NATIVE ERC20 + SAC + SEP41 } enum ChainKind { diff --git a/apps/backend-relayer/src/providers/chain/chain-provider.abstract.ts b/apps/backend-relayer/src/chain-adapters/adapters/chain-adapter.abstract.ts similarity index 84% rename from apps/backend-relayer/src/providers/chain/chain-provider.abstract.ts rename to apps/backend-relayer/src/chain-adapters/adapters/chain-adapter.abstract.ts index 76d9d9d..4368be1 100644 --- a/apps/backend-relayer/src/providers/chain/chain-provider.abstract.ts +++ b/apps/backend-relayer/src/chain-adapters/adapters/chain-adapter.abstract.ts @@ -1,4 +1,5 @@ import { + ChainAddress, T_CloseAdRequest, T_CloseAdRequestContractDetails, T_CreatFundAdRequest, @@ -16,13 +17,10 @@ import { T_UnlockOrderContractDetails, T_WithdrawFromAdRequest, T_WithdrawFromAdRequestContractDetails, -} from '../viem/types'; +} from '../types'; -// Chain-agnostic contract surface. Each underlying chain family (EVM via -// ViemService, Stellar via StellarService, …) implements this. ad/trade/faucet -// services never depend on a concrete implementation — they go through -// ChainProviderService.forChain(chain.kind). -export abstract class ChainProvider { +// Chain-agnostic contract surface +export abstract class ChainAdapter { abstract getCreateAdRequestContractDetails( data: T_CreateAdRequest, ): Promise; @@ -85,20 +83,20 @@ export abstract class ChainProvider { abstract mintToken(data: { chainId: string; - tokenAddress: `0x${string}`; - receiver: `0x${string}`; + tokenAddress: ChainAddress; + receiver: ChainAddress; }): Promise<{ txHash: string }>; abstract checkTokenBalance(data: { chainId: string; - tokenAddress: `0x${string}`; - account: `0x${string}`; + tokenAddress: ChainAddress; + account: ChainAddress; }): Promise; abstract orderTypeHash(orderParams: T_OrderParams): string; abstract verifyOrderSignature( - address: `0x${string}`, + address: ChainAddress, orderHash: `0x${string}`, signature: `0x${string}`, ): boolean; diff --git a/apps/backend-relayer/src/providers/chain/evm-chain-provider.ts b/apps/backend-relayer/src/chain-adapters/adapters/evm-chain-adapter.ts similarity index 55% rename from apps/backend-relayer/src/providers/chain/evm-chain-provider.ts rename to apps/backend-relayer/src/chain-adapters/adapters/evm-chain-adapter.ts index 921beee..f43e495 100644 --- a/apps/backend-relayer/src/providers/chain/evm-chain-provider.ts +++ b/apps/backend-relayer/src/chain-adapters/adapters/evm-chain-adapter.ts @@ -1,7 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { ChainProvider } from './chain-provider.abstract'; -import { ViemService } from '../viem/viem.service'; +import { ChainAdapter } from './chain-adapter.abstract'; +import { ViemService } from '../../providers/viem/viem.service'; import { + ChainAddress, T_CloseAdRequest, T_CloseAdRequestContractDetails, T_CreatFundAdRequest, @@ -19,61 +20,86 @@ import { T_UnlockOrderContractDetails, T_WithdrawFromAdRequest, T_WithdrawFromAdRequestContractDetails, -} from '../viem/types'; +} from '../types'; @Injectable() -export class EvmChainProvider extends ChainProvider { +export class EvmChainAdapter extends ChainAdapter { + private static readonly EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/; + constructor(private readonly viem: ViemService) { super(); } + // EVM addresses are 20-byte, 0x-prefixed, 40 hex chars. Reject anything + // else at the adapter boundary so routing mistakes surface here instead + // of as opaque ABI errors downstream. + private assertLocalAddress(value: string, field: string): void { + if (!EvmChainAdapter.EVM_ADDRESS_RE.test(value)) { + throw new Error( + `${field}: expected EVM address (0x + 40 hex), got "${value}"`, + ); + } + } + getCreateAdRequestContractDetails( data: T_CreateAdRequest, ): Promise { + this.assertLocalAddress(data.adContractAddress, 'adContractAddress'); + this.assertLocalAddress(data.adToken, 'adToken'); return this.viem.getCreateAdRequestContractDetails(data); } getFundAdRequestContractDetails( data: T_CreatFundAdRequest, ): Promise { + this.assertLocalAddress(data.adContractAddress, 'adContractAddress'); return this.viem.getFundAdRequestContractDetails(data); } getWithdrawFromAdRequestContractDetails( data: T_WithdrawFromAdRequest, ): Promise { + this.assertLocalAddress(data.adContractAddress, 'adContractAddress'); + this.assertLocalAddress(data.to, 'to'); return this.viem.getWithdrawFromAdRequestContractDetails(data); } getCloseAdRequestContractDetails( data: T_CloseAdRequest, ): Promise { + this.assertLocalAddress(data.adContractAddress, 'adContractAddress'); + this.assertLocalAddress(data.to, 'to'); return this.viem.getCloseAdRequestContractDetails(data); } getLockForOrderRequestContractDetails( data: T_LockForOrderRequest, ): Promise { + this.assertLocalAddress(data.adContractAddress, 'adContractAddress'); return this.viem.getLockForOrderRequestContractDetails(data); } getCreateOrderRequestContractDetails( data: T_CreateOrderRequest, ): Promise { + this.assertLocalAddress(data.orderContractAddress, 'orderContractAddress'); return this.viem.getCreateOrderRequestContractDetails(data); } getUnlockOrderContractDetails( data: T_CreateUnlockOrderContractDetails, ): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); return this.viem.getUnlockOrderContractDetails(data); } validateAdManagerRequest(data: T_RequestValidation): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); return this.viem.validateAdManagerRequest(data); } validateOrderPortalRequest(data: T_RequestValidation): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); return this.viem.validateOrderPortalRequest(data); } @@ -81,14 +107,17 @@ export class EvmChainProvider extends ChainProvider { isAdCreator: boolean, data: T_FetchRoot, ): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); return this.viem.fetchOnChainLatestRoot(isAdCreator, data); } fetchAdChainLatestRoot(data: T_FetchRoot): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); return this.viem.fetchAdChainLatestRoot(data); } fetchOrderChainLatestRoot(data: T_FetchRoot): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); return this.viem.fetchOrderChainLatestRoot(data); } @@ -97,6 +126,7 @@ export class EvmChainProvider extends ChainProvider { isAdCreator: boolean, data: T_FetchRoot, ): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); return this.viem.checkLocalRootExist(localRoot, isAdCreator, data); } @@ -104,31 +134,46 @@ export class EvmChainProvider extends ChainProvider { isAdCreator: boolean, data: T_FetchRoot, ): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); return this.viem.fetchOnChainRoots(isAdCreator, data); } fetchAdChainRoots(data: T_FetchRoot): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); return this.viem.fetchAdChainRoots(data); } fetchOrderChainRoots(data: T_FetchRoot): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); return this.viem.fetchOrderChainRoots(data); } mintToken(data: { chainId: string; - tokenAddress: `0x${string}`; - receiver: `0x${string}`; + tokenAddress: ChainAddress; + receiver: ChainAddress; }): Promise<{ txHash: string }> { - return this.viem.mintToken(data); + this.assertLocalAddress(data.tokenAddress, 'tokenAddress'); + this.assertLocalAddress(data.receiver, 'receiver'); + return this.viem.mintToken({ + chainId: data.chainId, + tokenAddress: data.tokenAddress as `0x${string}`, + receiver: data.receiver as `0x${string}`, + }); } checkTokenBalance(data: { chainId: string; - tokenAddress: `0x${string}`; - account: `0x${string}`; + tokenAddress: ChainAddress; + account: ChainAddress; }): Promise { - return this.viem.checkTokenBalance(data); + this.assertLocalAddress(data.tokenAddress, 'tokenAddress'); + this.assertLocalAddress(data.account, 'account'); + return this.viem.checkTokenBalance({ + chainId: data.chainId, + tokenAddress: data.tokenAddress as `0x${string}`, + account: data.account as `0x${string}`, + }); } orderTypeHash(orderParams: T_OrderParams): string { @@ -136,10 +181,15 @@ export class EvmChainProvider extends ChainProvider { } verifyOrderSignature( - address: `0x${string}`, + address: ChainAddress, orderHash: `0x${string}`, signature: `0x${string}`, ): boolean { - return this.viem.verifyOrderSignature(address, orderHash, signature); + this.assertLocalAddress(address, 'address'); + return this.viem.verifyOrderSignature( + address as `0x${string}`, + orderHash, + signature, + ); } } diff --git a/apps/backend-relayer/src/chain-adapters/adapters/stellar-chain-adapter.ts b/apps/backend-relayer/src/chain-adapters/adapters/stellar-chain-adapter.ts new file mode 100644 index 0000000..283cf1c --- /dev/null +++ b/apps/backend-relayer/src/chain-adapters/adapters/stellar-chain-adapter.ts @@ -0,0 +1,196 @@ +import { Injectable } from '@nestjs/common'; +import { ChainAdapter } from './chain-adapter.abstract'; +import { StellarService } from '../../providers/stellar/stellar.service'; +import { + ChainAddress, + T_CloseAdRequest, + T_CloseAdRequestContractDetails, + T_CreatFundAdRequest, + T_CreatFundAdRequestContractDetails, + T_CreateAdRequest, + T_CreateAdRequestContractDetails, + T_CreateOrderRequest, + T_CreateOrderRequestContractDetails, + T_CreateUnlockOrderContractDetails, + T_FetchRoot, + T_LockForOrderRequest, + T_LockForOrderRequestContractDetails, + T_OrderParams, + T_RequestValidation, + T_UnlockOrderContractDetails, + T_WithdrawFromAdRequest, + T_WithdrawFromAdRequestContractDetails, +} from '../types'; + +@Injectable() +export class StellarChainAdapter extends ChainAdapter { + private static readonly HEX32_RE = /^0x[a-fA-F0-9]{64}$/; + + constructor(private readonly stellar: StellarService) { + super(); + } + + // Stellar chain-records store addresses as the 32-byte strkey payload in + // 0x-hex form (64 hex chars). Anything else — a short EVM-style 20-byte + // hex, a raw G.../C... strkey — is rejected here so routing mistakes + // don't silently reach the Soroban RPC path. + private assertLocalAddress(value: string, field: string): void { + if (!StellarChainAdapter.HEX32_RE.test(value)) { + throw new Error( + `${field}: expected Stellar address (0x + 64 hex), got "${value}"`, + ); + } + } + + getCreateAdRequestContractDetails( + data: T_CreateAdRequest, + ): Promise { + this.assertLocalAddress(data.adContractAddress, 'adContractAddress'); + this.assertLocalAddress(data.adToken, 'adToken'); + return this.stellar.getCreateAdRequestContractDetails(data); + } + + getFundAdRequestContractDetails( + data: T_CreatFundAdRequest, + ): Promise { + this.assertLocalAddress(data.adContractAddress, 'adContractAddress'); + return this.stellar.getFundAdRequestContractDetails(data); + } + + getWithdrawFromAdRequestContractDetails( + data: T_WithdrawFromAdRequest, + ): Promise { + this.assertLocalAddress(data.adContractAddress, 'adContractAddress'); + this.assertLocalAddress(data.to, 'to'); + return this.stellar.getWithdrawFromAdRequestContractDetails(data); + } + + getCloseAdRequestContractDetails( + data: T_CloseAdRequest, + ): Promise { + this.assertLocalAddress(data.adContractAddress, 'adContractAddress'); + this.assertLocalAddress(data.to, 'to'); + return this.stellar.getCloseAdRequestContractDetails(data); + } + + getLockForOrderRequestContractDetails( + data: T_LockForOrderRequest, + ): Promise { + this.assertLocalAddress(data.adContractAddress, 'adContractAddress'); + return this.stellar.getLockForOrderRequestContractDetails(data); + } + + getCreateOrderRequestContractDetails( + data: T_CreateOrderRequest, + ): Promise { + this.assertLocalAddress(data.orderContractAddress, 'orderContractAddress'); + return this.stellar.getCreateOrderRequestContractDetails(data); + } + + getUnlockOrderContractDetails( + data: T_CreateUnlockOrderContractDetails, + ): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); + return this.stellar.getUnlockOrderContractDetails(data); + } + + validateAdManagerRequest(data: T_RequestValidation): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); + return this.stellar.validateAdManagerRequest(data); + } + + validateOrderPortalRequest(data: T_RequestValidation): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); + return this.stellar.validateOrderPortalRequest(data); + } + + fetchOnChainLatestRoot( + isAdCreator: boolean, + data: T_FetchRoot, + ): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); + return this.stellar.fetchOnChainLatestRoot(isAdCreator, data); + } + + fetchAdChainLatestRoot(data: T_FetchRoot): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); + return this.stellar.fetchAdChainLatestRoot(data); + } + + fetchOrderChainLatestRoot(data: T_FetchRoot): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); + return this.stellar.fetchOrderChainLatestRoot(data); + } + + checkLocalRootExist( + localRoot: string, + isAdCreator: boolean, + data: T_FetchRoot, + ): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); + return this.stellar.checkLocalRootExist(localRoot, isAdCreator, data); + } + + fetchOnChainRoots( + isAdCreator: boolean, + data: T_FetchRoot, + ): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); + return this.stellar.fetchOnChainRoots(isAdCreator, data); + } + + fetchAdChainRoots(data: T_FetchRoot): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); + return this.stellar.fetchAdChainRoots(data); + } + + fetchOrderChainRoots(data: T_FetchRoot): Promise { + this.assertLocalAddress(data.contractAddress, 'contractAddress'); + return this.stellar.fetchOrderChainRoots(data); + } + + mintToken(data: { + chainId: string; + tokenAddress: ChainAddress; + receiver: ChainAddress; + }): Promise<{ txHash: string }> { + this.assertLocalAddress(data.tokenAddress, 'tokenAddress'); + this.assertLocalAddress(data.receiver, 'receiver'); + return this.stellar.mintToken({ + chainId: data.chainId, + tokenAddress: data.tokenAddress as `0x${string}`, + receiver: data.receiver as `0x${string}`, + }); + } + + checkTokenBalance(data: { + chainId: string; + tokenAddress: ChainAddress; + account: ChainAddress; + }): Promise { + this.assertLocalAddress(data.tokenAddress, 'tokenAddress'); + this.assertLocalAddress(data.account, 'account'); + return this.stellar.checkTokenBalance({ + chainId: data.chainId, + tokenAddress: data.tokenAddress as `0x${string}`, + account: data.account as `0x${string}`, + }); + } + + orderTypeHash(orderParams: T_OrderParams): string { + return this.stellar.orderTypeHash(orderParams); + } + + verifyOrderSignature( + address: ChainAddress, + orderHash: `0x${string}`, + signature: `0x${string}`, + ): boolean { + this.assertLocalAddress(address, 'address'); + return this.stellar.verifyOrderSignature( + address as `0x${string}`, + orderHash, + signature, + ); + } +} diff --git a/apps/backend-relayer/src/chain-adapters/chain-adapter.module.ts b/apps/backend-relayer/src/chain-adapters/chain-adapter.module.ts new file mode 100644 index 0000000..2a3fe53 --- /dev/null +++ b/apps/backend-relayer/src/chain-adapters/chain-adapter.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { ViemModule } from '../providers/viem/viem.module'; +import { StellarModule } from '../providers/stellar/stellar.module'; +import { EvmChainAdapter } from './adapters/evm-chain-adapter'; +import { StellarChainAdapter } from './adapters/stellar-chain-adapter'; +import { ChainAdapterService } from './chain-adapter.service'; + +@Module({ + imports: [ViemModule, StellarModule], + providers: [EvmChainAdapter, StellarChainAdapter, ChainAdapterService], + exports: [ChainAdapterService], +}) +export class ChainAdapterModule {} diff --git a/apps/backend-relayer/src/chain-adapters/chain-adapter.service.ts b/apps/backend-relayer/src/chain-adapters/chain-adapter.service.ts new file mode 100644 index 0000000..683adc4 --- /dev/null +++ b/apps/backend-relayer/src/chain-adapters/chain-adapter.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { ChainKind } from '@prisma/client'; +import { ChainAdapter } from './adapters/chain-adapter.abstract'; +import { EvmChainAdapter } from './adapters/evm-chain-adapter'; +import { StellarChainAdapter } from './adapters/stellar-chain-adapter'; + +@Injectable() +export class ChainAdapterService { + constructor( + private readonly evm: EvmChainAdapter, + private readonly stellar: StellarChainAdapter, + ) {} + + forChain(kind: ChainKind): ChainAdapter { + switch (kind) { + case ChainKind.EVM: + return this.evm; + case ChainKind.STELLAR: + return this.stellar; + default: { + throw new Error(`Unsupported chain kind: ${String(kind)}`); + } + } + } +} diff --git a/apps/backend-relayer/src/providers/viem/types.ts b/apps/backend-relayer/src/chain-adapters/types.ts similarity index 77% rename from apps/backend-relayer/src/providers/viem/types.ts rename to apps/backend-relayer/src/chain-adapters/types.ts index d023fac..f0641d8 100644 --- a/apps/backend-relayer/src/providers/viem/types.ts +++ b/apps/backend-relayer/src/chain-adapters/types.ts @@ -4,18 +4,27 @@ // (0x-prefixed, 64 hex chars). For EVM addresses this means left-padded // with 12 zero bytes; for Stellar addresses the full 32-byte strkey payload. // -// Fields that stay on the ad chain itself (the AdManager contract address, -// the ad's local ERC20 `adToken`, withdraw `to`) remain 20-byte EVM -// addresses because they're only ever consumed by EVM ABI calls. +// Local-chain address fields (adContractAddress, adToken, withdraw `to`, +// orderContractAddress) use `ChainAddress`, which is a 0x-prefixed hex +// string accepted in either the 20-byte (EVM) or 32-byte (Stellar) form. +// The chain-kind-specific adapter is responsible for validating that the +// shape matches the chain it's bound to — see `address-validators.ts`. +// +// `EvmAddress` is kept only for strictly EVM-typed values that never +// straddle chains (signatures, EIP-712 hashes). export type Bytes32Hex = `0x${string}`; export type EvmAddress = `0x${string}`; +// A local-chain address. 0x-prefixed hex. Either 40 hex chars (EVM, 20 +// bytes) or 64 hex chars (Stellar/bytes32). The bound adapter validates +// the concrete shape. +export type ChainAddress = string; export type T_CreateAdRequest = { - adContractAddress: EvmAddress; + adContractAddress: ChainAddress; adChainId: bigint; adId: string; - adToken: EvmAddress; + adToken: ChainAddress; initialAmount: string; orderChainId: bigint; adRecipient: Bytes32Hex; @@ -25,10 +34,11 @@ export type T_CreateAdRequestContractDetails = { chainId: string; contractAddress: string; signature: `0x${string}`; + signerPublicKey?: `0x${string}`; authToken: string; timeToExpire: number; adId: string; - adToken: EvmAddress; + adToken: ChainAddress; initialAmount: string; orderChainId: string; adRecipient: Bytes32Hex; @@ -36,7 +46,7 @@ export type T_CreateAdRequestContractDetails = { }; export type T_CreatFundAdRequest = { - adContractAddress: EvmAddress; + adContractAddress: ChainAddress; adChainId: bigint; adId: string; amount: string; @@ -46,6 +56,7 @@ export type T_CreatFundAdRequestContractDetails = { chainId: string; contractAddress: string; signature: `0x${string}`; + signerPublicKey?: `0x${string}`; authToken: string; timeToExpire: number; adId: string; @@ -54,40 +65,42 @@ export type T_CreatFundAdRequestContractDetails = { }; export type T_WithdrawFromAdRequest = { - adContractAddress: EvmAddress; + adContractAddress: ChainAddress; adChainId: bigint; adId: string; amount: string; - to: EvmAddress; + to: ChainAddress; }; export type T_WithdrawFromAdRequestContractDetails = { chainId: string; contractAddress: string; signature: `0x${string}`; + signerPublicKey?: `0x${string}`; authToken: string; timeToExpire: number; adId: string; amount: string; - to: EvmAddress; + to: ChainAddress; reqHash: `0x${string}`; }; export type T_CloseAdRequest = { - adContractAddress: EvmAddress; + adContractAddress: ChainAddress; adChainId: bigint; adId: string; - to: EvmAddress; + to: ChainAddress; }; export type T_CloseAdRequestContractDetails = { chainId: string; contractAddress: string; signature: `0x${string}`; + signerPublicKey?: `0x${string}`; authToken: string; timeToExpire: number; adId: string; - to: EvmAddress; + to: ChainAddress; reqHash: `0x${string}`; }; @@ -144,7 +157,7 @@ export type T_OrderPortalParams = { export type T_LockForOrderRequest = { adChainId: bigint; - adContractAddress: EvmAddress; + adContractAddress: ChainAddress; orderParams: T_OrderParams; }; @@ -152,6 +165,7 @@ export type T_LockForOrderRequestContractDetails = { chainId: string; contractAddress: string; signature: `0x${string}`; + signerPublicKey?: `0x${string}`; authToken: string; timeToExpire: number; orderParams: T_AdManagerOrderParams; @@ -161,13 +175,13 @@ export type T_LockForOrderRequestContractDetails = { export type T_CreateOrderRequest = { orderChainId: bigint; - orderContractAddress: EvmAddress; + orderContractAddress: ChainAddress; orderParams: T_OrderParams; }; export type T_CreateUnlockOrderContractDetails = { chainId: bigint; - contractAddress: EvmAddress; + contractAddress: ChainAddress; isAdCreator: boolean; orderParams: T_OrderParams; nullifierHash: string; @@ -177,8 +191,9 @@ export type T_CreateUnlockOrderContractDetails = { export type T_UnlockOrderContractDetails = { chainId: string; - contractAddress: EvmAddress; + contractAddress: ChainAddress; signature: `0x${string}`; + signerPublicKey?: `0x${string}`; authToken: string; timeToExpire: number; orderParams: T_AdManagerOrderParams | T_OrderPortalParams; @@ -193,6 +208,7 @@ export type T_CreateOrderRequestContractDetails = { chainId: string; contractAddress: string; signature: `0x${string}`; + signerPublicKey?: `0x${string}`; authToken: string; timeToExpire: number; orderParams: T_OrderPortalParams; @@ -202,13 +218,13 @@ export type T_CreateOrderRequestContractDetails = { export type T_RequestValidation = { chainId: bigint; - contractAddress: EvmAddress; + contractAddress: ChainAddress; reqHash: `0x${string}`; }; export type T_FetchRoot = { chainId: bigint; - contractAddress: EvmAddress; + contractAddress: ChainAddress; }; export type T_FetchRootResponse = { diff --git a/apps/backend-relayer/src/libs/configs.ts b/apps/backend-relayer/src/libs/configs.ts index 6048966..a6ed405 100644 --- a/apps/backend-relayer/src/libs/configs.ts +++ b/apps/backend-relayer/src/libs/configs.ts @@ -22,4 +22,16 @@ export const env = { secretKey: process.env.SECRET_KEY || '32_byte_secret_key_for_aes!', evmRpcApiKey: process.env.EVM_RPC_API_KEY || '', rpcUrlHedera: process.env.RPC_URL_HEDERA || '', + stellar: { + adminSecret: process.env.STELLAR_ADMIN_SECRET || '', + rpcUrl: + process.env.STELLAR_RPC_URL || 'https://soroban-testnet.stellar.org', + networkPassphrase: + process.env.STELLAR_NETWORK_PASSPHRASE || + 'Test SDF Network ; September 2015', + // SEP-10 server signing key. Separate from adminSecret so the auth + // surface can't move funds and a compromise is bounded to issuing + // challenges. + authSecret: process.env.STELLAR_AUTH_SECRET || '', + }, }; diff --git a/apps/backend-relayer/src/modules/ads/ad.module.ts b/apps/backend-relayer/src/modules/ads/ad.module.ts index ad88d6e..dfa682d 100644 --- a/apps/backend-relayer/src/modules/ads/ad.module.ts +++ b/apps/backend-relayer/src/modules/ads/ad.module.ts @@ -3,10 +3,10 @@ import { AdsController } from './ad.controller'; import { AdsService } from './ad.service'; import { PrismaService } from '@prisma/prisma.service'; import { JwtModule, JwtService } from '@nestjs/jwt'; -import { ChainProvidersModule } from '../../providers/chain/chain.module'; +import { ChainAdapterModule } from '../../chain-adapters/chain-adapter.module'; @Module({ - imports: [JwtModule.register({}), ChainProvidersModule], + imports: [JwtModule.register({}), ChainAdapterModule], controllers: [AdsController], providers: [AdsService, PrismaService, JwtService], }) diff --git a/apps/backend-relayer/src/modules/ads/ad.service.ts b/apps/backend-relayer/src/modules/ads/ad.service.ts index 51f1605..dca9b0f 100644 --- a/apps/backend-relayer/src/modules/ads/ad.service.ts +++ b/apps/backend-relayer/src/modules/ads/ad.service.ts @@ -19,7 +19,7 @@ import { import { getAddress } from 'viem'; import { AdStatus, Prisma } from '@prisma/client'; import { Request } from 'express'; -import { ChainProviderService } from '../../providers/chain/chain-provider.service'; +import { ChainAdapterService } from '../../chain-adapters/chain-adapter.service'; import { toBytes32 } from '../../providers/viem/ethers/typedData'; import { randomUUID } from 'crypto'; @@ -49,7 +49,7 @@ type AdUpdateLogInput = { export class AdsService { constructor( private readonly prisma: PrismaService, - private readonly chainProviders: ChainProviderService, + private readonly chainAdapters: ChainAdapterService, ) {} async list(query: QueryAdsDto) { @@ -389,7 +389,7 @@ export class AdsService { const adId = randomUUID(); - const reqContractDetails = await this.chainProviders + const reqContractDetails = await this.chainAdapters .forChain(route.adToken.chain.kind) .getCreateAdRequestContractDetails({ adChainId: route.adToken.chain.chainId, @@ -522,7 +522,7 @@ export class AdsService { const effectiveStatus = ad.status == 'EXHAUSTED' ? 'ACTIVE' : ad.status; - const reqContractDetails = await this.chainProviders + const reqContractDetails = await this.chainAdapters .forChain(ad.route.adToken.chain.kind) .getFundAdRequestContractDetails({ adContractAddress: ad.route.adToken.chain @@ -657,7 +657,7 @@ export class AdsService { ? 'EXHAUSTED' : ad.status; - const reqContractDetails = await this.chainProviders + const reqContractDetails = await this.chainAdapters .forChain(ad.route.adToken.chain.kind) .getWithdrawFromAdRequestContractDetails({ adContractAddress: ad.route.adToken.chain @@ -861,7 +861,7 @@ export class AdsService { throw new BadRequestException('Ad is already closed'); } - const reqContractDetails = await this.chainProviders + const reqContractDetails = await this.chainAdapters .forChain(ad.route.adToken.chain.kind) .getCloseAdRequestContractDetails({ adContractAddress: ad.route.adToken.chain @@ -980,7 +980,7 @@ export class AdsService { if (!ad) throw new NotFoundException('Ad for Ad Id not found'); // // verify adLog - const isValidated = await this.chainProviders + const isValidated = await this.chainAdapters .forChain(ad.route.adToken.chain.kind) .validateAdManagerRequest({ chainId: ad.route.adToken.chain.chainId, diff --git a/apps/backend-relayer/src/modules/ads/dto/ad.dto.ts b/apps/backend-relayer/src/modules/ads/dto/ad.dto.ts index 81e0c4f..9bbb089 100644 --- a/apps/backend-relayer/src/modules/ads/dto/ad.dto.ts +++ b/apps/backend-relayer/src/modules/ads/dto/ad.dto.ts @@ -1,4 +1,4 @@ -import { IsIn, IsOptional, IsString, IsUUID, Matches } from 'class-validator'; +import { IsIn, IsInt, IsOptional, IsString, IsUUID, Matches } from 'class-validator'; import { Transform } from 'class-transformer'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { JsonObject, JsonArray } from '@prisma/client/runtime/library'; @@ -26,12 +26,18 @@ export class QueryAdsDto { description: 'Chain ID of the ad token', example: 1, }) + @IsOptional() + @Transform(({ value }) => Number(value)) + @IsInt() adChainId?: number; @ApiPropertyOptional({ description: 'Chain ID of the order token', example: 298, }) + @IsOptional() + @Transform(({ value }) => Number(value)) + @IsInt() orderChainId?: number; @ApiPropertyOptional({ diff --git a/apps/backend-relayer/src/modules/auth/auth.controller.spec.ts b/apps/backend-relayer/src/modules/auth/auth.controller.spec.ts index bfdc10e..b1387c7 100644 --- a/apps/backend-relayer/src/modules/auth/auth.controller.spec.ts +++ b/apps/backend-relayer/src/modules/auth/auth.controller.spec.ts @@ -1,4 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { ChainKind } from '@prisma/client'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; @@ -29,10 +30,11 @@ describe('AuthController', () => { jest.resetAllMocks(); }); - describe('GET /v1/auth/challenge', () => { - it('should call AuthService.challenge with the address and return payload', async () => { + describe('POST /v1/auth/challenge', () => { + it('routes EVM challenge and returns payload', async () => { const address = '0x1234567890abcdef1234567890abcdef12345678'; const mockPayload = { + chainKind: ChainKind.EVM, nonce: 'abc123', address, expiresAt: new Date(Date.now() + 300_000).toISOString(), @@ -44,28 +46,53 @@ describe('AuthController', () => { .spyOn(service, 'challenge') .mockResolvedValueOnce(mockPayload); - const res = await controller.challenge({ address }); + const res = await controller.challenge({ + address, + chainKind: ChainKind.EVM, + }); expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledWith(address); - + expect(spy).toHaveBeenCalledWith(address, ChainKind.EVM); expect(res).toEqual(mockPayload); }); - it('should propagate service errors', async () => { - const address = '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; + it('routes Stellar challenge and returns SEP-10 payload', async () => { + const address = + 'GABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXY234567ABCDE'; + const mockPayload = { + chainKind: ChainKind.STELLAR, + address, + expiresAt: new Date(Date.now() + 300_000).toISOString(), + transaction: 'base64-xdr...', + networkPassphrase: 'Test SDF Network ; September 2015', + }; const spy = jest .spyOn(service, 'challenge') - .mockRejectedValueOnce(new Error('boom')); + .mockResolvedValueOnce(mockPayload); + + const res = await controller.challenge({ + address, + chainKind: ChainKind.STELLAR, + }); - await expect(controller.challenge({ address })).rejects.toThrow('boom'); - expect(spy).toHaveBeenCalledWith(address); + expect(spy).toHaveBeenCalledWith(address, ChainKind.STELLAR); + expect(res).toEqual(mockPayload); + }); + + it('propagates service errors', async () => { + const address = '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; + + jest.spyOn(service, 'challenge').mockRejectedValueOnce(new Error('boom')); + + await expect( + controller.challenge({ address, chainKind: ChainKind.EVM }), + ).rejects.toThrow('boom'); }); }); describe('POST /v1/auth/refresh', () => { - it('should call AuthService.refresh and return new tokens', async () => { + it('calls AuthService.refresh and returns new tokens', async () => { const dto = { refresh: 'valid-refresh-token' }; const mockResult = { tokens: { access: 'new-jwt-access', refresh: 'new-jwt-refresh' }, @@ -82,23 +109,23 @@ describe('AuthController', () => { expect(res).toEqual(mockResult); }); - it('should handle invalid refresh token', async () => { + it('propagates invalid refresh token error', async () => { const dto = { refresh: 'invalid-token' }; - const spy = jest + jest .spyOn(service, 'refresh') .mockRejectedValueOnce(new Error('Invalid refresh token')); await expect(controller.refresh(dto)).rejects.toThrow( 'Invalid refresh token', ); - expect(spy).toHaveBeenCalledWith(dto.refresh); }); }); describe('POST /v1/auth/login', () => { - it('should call AuthService.verify and return payload', async () => { + it('forwards EVM login DTO to AuthService.login', async () => { const dto = { + chainKind: ChainKind.EVM, message: 'service.xyz wants you to sign in with your Ethereum account:\n...', signature: '0xsignature', @@ -114,20 +141,42 @@ describe('AuthController', () => { const res = await controller.login(dto); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledWith(dto.message, dto.signature); + expect(spy).toHaveBeenCalledWith(dto); expect(res).toEqual(mockResult); }); - it('should propagate service errors (e.g., invalid signature)', async () => { - const dto = { message: 'bad', signature: '0x00' }; + it('forwards Stellar login DTO (transaction only) to AuthService.login', async () => { + const dto = { + chainKind: ChainKind.STELLAR, + transaction: 'co-signed-xdr-base64', + }; + const mockResult = { + user: { id: 'u2', username: 'stellar_user' }, + tokens: { access: 'jwt-access', refresh: 'jwt-refresh' }, + }; const spy = jest + .spyOn(service, 'login') + .mockResolvedValueOnce(mockResult); + + const res = await controller.login(dto); + + expect(spy).toHaveBeenCalledWith(dto); + expect(res).toEqual(mockResult); + }); + + it('propagates service errors (e.g., invalid signature)', async () => { + const dto = { + chainKind: ChainKind.EVM, + message: 'bad', + signature: '0x00', + }; + + jest .spyOn(service, 'login') .mockRejectedValueOnce(new Error('Unauthorized')); await expect(controller.login(dto)).rejects.toThrow('Unauthorized'); - expect(spy).toHaveBeenCalledWith(dto.message, dto.signature); }); }); }); diff --git a/apps/backend-relayer/src/modules/auth/auth.controller.ts b/apps/backend-relayer/src/modules/auth/auth.controller.ts index c87a6ef..480e347 100644 --- a/apps/backend-relayer/src/modules/auth/auth.controller.ts +++ b/apps/backend-relayer/src/modules/auth/auth.controller.ts @@ -23,7 +23,7 @@ export class AuthController { type: ChallengeResponseDto, }) async challenge(@Body() dto: ChallengeDTO) { - return this.auth.challenge(dto.address); + return this.auth.challenge(dto.address, dto.chainKind); } @Post('login') @@ -35,7 +35,7 @@ export class AuthController { type: LoginResponseDto, }) async login(@Body() dto: LoginDTO) { - return this.auth.login(dto.message, dto.signature); + return this.auth.login(dto); } @Post('refresh') diff --git a/apps/backend-relayer/src/modules/auth/auth.module.ts b/apps/backend-relayer/src/modules/auth/auth.module.ts index 0550735..12fc756 100644 --- a/apps/backend-relayer/src/modules/auth/auth.module.ts +++ b/apps/backend-relayer/src/modules/auth/auth.module.ts @@ -3,10 +3,12 @@ import { JwtModule } from '@nestjs/jwt'; import { PrismaService } from '@prisma/prisma.service'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; +import { EvmAuthService } from './evm/evm-auth.service'; +import { StellarAuthService } from './stellar/stellar-auth.service'; @Module({ imports: [JwtModule.register({})], controllers: [AuthController], - providers: [AuthService, PrismaService], + providers: [AuthService, EvmAuthService, StellarAuthService, PrismaService], }) export class AuthModule {} diff --git a/apps/backend-relayer/src/modules/auth/auth.service.ts b/apps/backend-relayer/src/modules/auth/auth.service.ts index f928efa..0eee716 100644 --- a/apps/backend-relayer/src/modules/auth/auth.service.ts +++ b/apps/backend-relayer/src/modules/auth/auth.service.ts @@ -3,80 +3,77 @@ import { Injectable, UnauthorizedException, } from '@nestjs/common'; -import { - uniqueNamesGenerator, - adjectives, - names, -} from 'unique-names-generator'; import { JwtService } from '@nestjs/jwt'; import { env } from '@libs/configs'; import { PrismaService } from '@prisma/prisma.service'; -import { SiweMessage } from 'siwe'; +import { ChainKind } from '@prisma/client'; import { randomUUID } from 'crypto'; -import { ethers } from 'ethers'; - -const CLOCK_SKEW_MS = 60_000; // 1 minute - +import { EvmAuthService } from './evm/evm-auth.service'; +import { StellarAuthService } from './stellar/stellar-auth.service'; + +/** + * Thin router over chain-specific auth services. Owns JWT minting and the + * shared refresh flow; delegates challenge construction and login signature + * verification to `EvmAuthService` or `StellarAuthService` based on + * `chainKind`. + */ @Injectable() export class AuthService { constructor( private readonly prisma: PrismaService, private readonly jwt: JwtService, + private readonly evmAuth: EvmAuthService, + private readonly stellarAuth: StellarAuthService, ) {} - async challenge(address: string) { - if (!ethers.isAddress(address)) { - throw new BadRequestException('Invalid EVM format'); - } - - try { - const value = crypto.randomUUID().replace(/-/g, ''); // 32 hex-like chars - const expiresAt = new Date(Date.now() + 5 * 60_000); // 5 min - await this.prisma.authNonce.create({ - data: { value, expiresAt, walletAddress: address }, - }); - return { - nonce: value, - address, - expiresAt: expiresAt.toISOString(), - domain: env.appDomain, - uri: env.appUri, - }; - } catch (error) { - console.error('Failed to create auth nonce', error); - throw new BadRequestException('Failed to create authentication nonce'); + async challenge(address: string, chainKind: ChainKind) { + switch (chainKind) { + case ChainKind.EVM: + return this.evmAuth.buildChallenge(address); + case ChainKind.STELLAR: + return this.stellarAuth.buildChallenge(address); + default: + throw new BadRequestException('Unsupported chainKind'); } } - async login(messageRaw: string, signature: string) { - const msg = this.parseSiwe(messageRaw); - - this.assertDomainAndUri(msg); - this.assertTimeWindows(msg, Date.now()); - - await this.verifySignature(msg, signature); - - const user = await this.consumeNonceAndUpsertUser(msg.address, msg.nonce); - - const [access, refresh] = await Promise.all([ - this.jwt.signAsync( - { sub: user.id, addr: user.walletAddress, typ: 'access' }, - { secret: env.jwt.secret, expiresIn: '24h', jwtid: randomUUID() }, - ), - this.jwt.signAsync( - { sub: user.id, addr: user.walletAddress, typ: 'refresh' }, - { secret: env.jwt.secret, expiresIn: '90d', jwtid: randomUUID() }, - ), - ]); + async login(input: { + chainKind: ChainKind; + message?: string; + signature?: string; + transaction?: string; + }) { + const user = await this.verifyLogin(input); + return this.issueTokens(user); + } - return { - user: { id: user.id, username: user.username }, - tokens: { access, refresh }, - }; + private async verifyLogin(input: { + chainKind: ChainKind; + message?: string; + signature?: string; + transaction?: string; + }) { + switch (input.chainKind) { + case ChainKind.EVM: { + if (!input.message || !input.signature) { + throw new BadRequestException( + 'message and signature required for EVM', + ); + } + return this.evmAuth.verifyLogin(input.message, input.signature); + } + case ChainKind.STELLAR: { + if (!input.transaction) { + throw new BadRequestException('transaction required for STELLAR'); + } + return this.stellarAuth.verifyLogin(input.transaction); + } + default: + throw new BadRequestException('Unsupported chainKind'); + } } async refresh(refreshToken: string) { - // Verify refresh JWT let payload: { sub: string; addr: string; typ?: string }; try { payload = await this.jwt.verifyAsync(refreshToken, { @@ -89,7 +86,6 @@ export class AuthService { throw new UnauthorizedException('Wrong token type'); } - // Ensure user still exists / allowed const user = await this.prisma.user.findUnique({ where: { id: payload.sub }, select: { id: true, walletAddress: true }, @@ -98,7 +94,22 @@ export class AuthService { throw new UnauthorizedException('User not found or mismatched address'); } - const [access, newRefresh] = await Promise.all([ + return { tokens: await this.mintTokenPair(user) }; + } + + private async issueTokens(user: { + id: string; + username: string; + walletAddress: string; + }) { + return { + user: { id: user.id, username: user.username }, + tokens: await this.mintTokenPair(user), + }; + } + + private async mintTokenPair(user: { id: string; walletAddress: string }) { + const [access, refresh] = await Promise.all([ this.jwt.signAsync( { sub: user.id, addr: user.walletAddress, typ: 'access' }, { secret: env.jwt.secret, expiresIn: '24h', jwtid: randomUUID() }, @@ -108,107 +119,6 @@ export class AuthService { { secret: env.jwt.secret, expiresIn: '90d', jwtid: randomUUID() }, ), ]); - return { - tokens: { access, refresh: newRefresh }, - }; - } - - /* --------------------------- private helpers --------------------------- */ - - private parseSiwe(messageRaw: string): SiweMessage { - try { - return new SiweMessage(messageRaw); - } catch { - throw new BadRequestException('Invalid SIWE message'); - } - } - - private assertDomainAndUri(msg: SiweMessage): void { - if (msg.domain !== env.appDomain) - throw new BadRequestException('Wrong domain'); - if (msg.uri !== env.appUri) throw new BadRequestException('Wrong URI'); - } - - private assertTimeWindows(msg: SiweMessage, nowMs: number): void { - if (msg.expirationTime) { - const exp = new Date(msg.expirationTime).getTime(); - if (Number.isNaN(exp) || exp < nowMs - CLOCK_SKEW_MS) { - throw new BadRequestException('Expired message'); - } - } - if (msg.notBefore) { - const nbf = new Date(msg.notBefore).getTime(); - if (Number.isNaN(nbf) || nbf > nowMs + CLOCK_SKEW_MS) { - throw new BadRequestException('Not yet valid'); - } - } - } - - private async verifySignature( - msg: SiweMessage, - signature: string, - ): Promise { - try { - const res = await msg.verify({ - signature, - domain: env.appDomain, - nonce: msg.nonce, - }); - if (!res.success) throw new UnauthorizedException('Verification failed'); - } catch (e) { - if (!(e instanceof UnauthorizedException)) { - throw new UnauthorizedException('Bad signature'); - } - throw e; - } - } - - private async consumeNonceAndUpsertUser(address: string, nonce: string) { - const now = Date.now(); - - const { user } = await this.prisma.$transaction(async (tx) => { - // requires @@unique([value, walletAddress]) on AuthNonce - const nonceRow = await tx.authNonce.findUnique({ - where: { - value: nonce, - walletAddress: address, - }, - }); - - if (!nonceRow) throw new BadRequestException('Unknown nonce'); - if (nonceRow.usedAt) throw new BadRequestException('Nonce already used'); - if (nonceRow.expiresAt.getTime() < now - CLOCK_SKEW_MS) { - throw new BadRequestException('Nonce expired'); - } - - await tx.authNonce.update({ - where: { - value: nonce, - walletAddress: address, - }, - data: { usedAt: new Date() }, - }); - - const user = await tx.user.upsert({ - where: { walletAddress: address }, - create: { walletAddress: address, username: this.generateUniqueName() }, - update: {}, - select: { id: true, username: true, walletAddress: true }, - }); - - return { user }; - }); - - return user; - } - - private generateUniqueName(): string { - const uniqueName = uniqueNamesGenerator({ - dictionaries: [adjectives, names], - separator: '-', - length: 2, - style: 'lowerCase', - }); - return uniqueName; + return { access, refresh }; } } diff --git a/apps/backend-relayer/src/modules/auth/dto/auth.dto.ts b/apps/backend-relayer/src/modules/auth/dto/auth.dto.ts index 0667bcb..22746ac 100644 --- a/apps/backend-relayer/src/modules/auth/dto/auth.dto.ts +++ b/apps/backend-relayer/src/modules/auth/dto/auth.dto.ts @@ -1,33 +1,71 @@ -import { IsString, MinLength } from 'class-validator'; +import { + IsEnum, + IsOptional, + IsString, + MinLength, + ValidateIf, +} from 'class-validator'; import { Transform } from 'class-transformer'; import { ApiProperty } from '@nestjs/swagger'; +import { ChainKind } from '@prisma/client'; export class ChallengeDTO { @ApiProperty({ - description: 'EVM address of the user', + description: 'Chain kind the caller is authenticating with', + enum: ChainKind, + example: ChainKind.EVM, + }) + @IsEnum(ChainKind) + chainKind!: ChainKind; + + @ApiProperty({ + description: + 'Wallet address — 0x-prefixed 20-byte hex for EVM, G-strkey for Stellar', example: '0x1234...', }) @IsString() - @Transform(({ value }) => value.trim()) - address: string; + @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + address!: string; } export class LoginDTO { @ApiProperty({ - description: 'Message to be signed', - example: 'Login message...', + description: 'Chain kind matching the original challenge', + enum: ChainKind, + example: ChainKind.EVM, + }) + @IsEnum(ChainKind) + chainKind!: ChainKind; + + // EVM (SIWE) path + @ApiProperty({ + description: 'SIWE message string (EVM path only)', + required: false, }) + @ValidateIf((o: LoginDTO) => o.chainKind === ChainKind.EVM) @IsString() - @Transform(({ value }) => value.trim()) - message!: string; + @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + message?: string; @ApiProperty({ - description: 'Signature of the message', - example: '0x1234...', + description: 'SIWE signature (EVM path only)', + required: false, + }) + @ValidateIf((o: LoginDTO) => o.chainKind === ChainKind.EVM) + @IsString() + @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + signature?: string; + + // Stellar (SEP-10) path + @ApiProperty({ + description: + 'Co-signed SEP-10 challenge transaction, base64 XDR (Stellar path only)', + required: false, }) + @ValidateIf((o: LoginDTO) => o.chainKind === ChainKind.STELLAR) @IsString() - @Transform(({ value }) => value.trim()) - signature!: string; + @Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)) + transaction?: string; } export class RefreshDto { @@ -38,40 +76,51 @@ export class RefreshDto { } export class ChallengeResponseDto { - @ApiProperty({ - description: 'Unique nonce for the challenge', - example: '123456789', - }) + @ApiProperty({ enum: ChainKind }) + @IsEnum(ChainKind) + chainKind!: ChainKind; + + @ApiProperty({ description: 'Echoed wallet address (canonical form)' }) @IsString() - nonce: string; + address!: string; - @ApiProperty({ - description: 'EVM address of the user', - example: '0x1234...', - }) + @ApiProperty({ description: 'Expiration timestamp (ISO8601)' }) @IsString() - address: string; + expiresAt!: string; @ApiProperty({ - description: 'Expiration timestamp', - example: '2024-01-01T00:00:00Z', + description: 'Unique nonce for SIWE (EVM only)', + required: false, }) + @IsOptional() + @IsString() + nonce?: string; + + @ApiProperty({ description: 'SIWE domain (EVM only)', required: false }) + @IsOptional() + @IsString() + domain?: string; + + @ApiProperty({ description: 'SIWE URI (EVM only)', required: false }) + @IsOptional() @IsString() - expiresAt: string; + uri?: string; @ApiProperty({ - description: 'Domain for the challenge', - example: 'example.com', + description: 'Server-signed SEP-10 challenge transaction (Stellar only)', + required: false, }) + @IsOptional() @IsString() - domain: string; + transaction?: string; @ApiProperty({ - description: 'URI for the challenge', - example: 'https://example.com/auth', + description: 'Stellar network passphrase (Stellar only)', + required: false, }) + @IsOptional() @IsString() - uri: string; + networkPassphrase?: string; } export class LoginResponseDto { @@ -82,7 +131,7 @@ export class LoginResponseDto { username: 'user123', }, }) - user: { + user!: { id: string; username: string; }; @@ -94,7 +143,7 @@ export class LoginResponseDto { refresh: 'eyJhbGciOiJIUzI1...', }, }) - tokens: { + tokens!: { access: string; refresh: string; }; @@ -108,7 +157,7 @@ export class RefreshResponseDto { refresh: 'eyJhbGciOiJIUzI1...', }, }) - tokens: { + tokens!: { access: string; refresh: string; }; diff --git a/apps/backend-relayer/src/modules/auth/evm/evm-auth.service.ts b/apps/backend-relayer/src/modules/auth/evm/evm-auth.service.ts new file mode 100644 index 0000000..d571113 --- /dev/null +++ b/apps/backend-relayer/src/modules/auth/evm/evm-auth.service.ts @@ -0,0 +1,145 @@ +import { + BadRequestException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { PrismaService } from '@prisma/prisma.service'; +import { ChainKind } from '@prisma/client'; +import { SiweMessage } from 'siwe'; +import { ethers } from 'ethers'; +import { env } from '@libs/configs'; +import { generateUniqueName } from '../username.util'; + +const CLOCK_SKEW_MS = 60_000; + +@Injectable() +export class EvmAuthService { + constructor(private readonly prisma: PrismaService) {} + + async buildChallenge(address: string): Promise<{ + chainKind: ChainKind; + nonce: string; + address: string; + expiresAt: string; + domain: string; + uri: string; + }> { + if (!ethers.isAddress(address)) { + throw new BadRequestException('Invalid EVM format'); + } + + try { + const value = crypto.randomUUID().replace(/-/g, ''); + const expiresAt = new Date(Date.now() + 5 * 60_000); + await this.prisma.authNonce.create({ + data: { value, expiresAt, walletAddress: address }, + }); + return { + chainKind: ChainKind.EVM, + nonce: value, + address, + expiresAt: expiresAt.toISOString(), + domain: env.appDomain, + uri: env.appUri, + }; + } catch (error) { + console.error('Failed to create auth nonce', error); + throw new BadRequestException('Failed to create authentication nonce'); + } + } + + /** + * Verify a SIWE login. Returns the resolved user row; the caller is + * responsible for minting JWTs. + */ + async verifyLogin( + messageRaw: string, + signature: string, + ): Promise<{ id: string; username: string; walletAddress: string }> { + const msg = this.parseSiwe(messageRaw); + this.assertDomainAndUri(msg); + this.assertTimeWindows(msg, Date.now()); + await this.verifySignature(msg, signature); + return this.consumeNonceAndUpsertUser(msg.address, msg.nonce); + } + + private parseSiwe(messageRaw: string): SiweMessage { + try { + return new SiweMessage(messageRaw); + } catch { + throw new BadRequestException('Invalid SIWE message'); + } + } + + private assertDomainAndUri(msg: SiweMessage): void { + if (msg.domain !== env.appDomain) + throw new BadRequestException('Wrong domain'); + if (msg.uri !== env.appUri) throw new BadRequestException('Wrong URI'); + } + + private assertTimeWindows(msg: SiweMessage, nowMs: number): void { + if (msg.expirationTime) { + const exp = new Date(msg.expirationTime).getTime(); + if (Number.isNaN(exp) || exp < nowMs - CLOCK_SKEW_MS) { + throw new BadRequestException('Expired message'); + } + } + if (msg.notBefore) { + const nbf = new Date(msg.notBefore).getTime(); + if (Number.isNaN(nbf) || nbf > nowMs + CLOCK_SKEW_MS) { + throw new BadRequestException('Not yet valid'); + } + } + } + + private async verifySignature( + msg: SiweMessage, + signature: string, + ): Promise { + try { + const res = await msg.verify({ + signature, + domain: env.appDomain, + nonce: msg.nonce, + }); + if (!res.success) throw new UnauthorizedException('Verification failed'); + } catch (e) { + if (!(e instanceof UnauthorizedException)) { + throw new UnauthorizedException('Bad signature'); + } + throw e; + } + } + + private async consumeNonceAndUpsertUser(address: string, nonce: string) { + const now = Date.now(); + + const { user } = await this.prisma.$transaction(async (tx) => { + const nonceRow = await tx.authNonce.findUnique({ + where: { value: nonce, walletAddress: address }, + }); + + if (!nonceRow) throw new BadRequestException('Unknown nonce'); + if (nonceRow.usedAt) throw new BadRequestException('Nonce already used'); + if (nonceRow.expiresAt.getTime() < now - CLOCK_SKEW_MS) { + throw new BadRequestException('Nonce expired'); + } + + await tx.authNonce.update({ + where: { value: nonce, walletAddress: address }, + data: { usedAt: new Date() }, + }); + + const user = await tx.user.upsert({ + where: { walletAddress: address }, + create: { walletAddress: address, username: generateUniqueName() }, + update: {}, + select: { id: true, username: true, walletAddress: true }, + }); + + return { user }; + }); + + return user; + } +} diff --git a/apps/backend-relayer/src/modules/auth/stellar/stellar-auth.service.spec.ts b/apps/backend-relayer/src/modules/auth/stellar/stellar-auth.service.spec.ts new file mode 100644 index 0000000..bd1d225 --- /dev/null +++ b/apps/backend-relayer/src/modules/auth/stellar/stellar-auth.service.spec.ts @@ -0,0 +1,129 @@ +import { UnauthorizedException, BadRequestException } from '@nestjs/common'; +import { Keypair, TransactionBuilder, Networks } from '@stellar/stellar-sdk'; + +// The service reads env.stellar.authSecret at construction time; set it before +// the module is evaluated so the Keypair.fromSecret path succeeds. +const SERVER_SECRET = + 'SA3C2KPR5TCHYJ5TNQXAY2776Z3H4CB723GDCAMEX5I2NLWP25QUYB3X'; +process.env.STELLAR_AUTH_SECRET = SERVER_SECRET; +process.env.SIGN_DOMAIN = 'proofbridge.xyz'; +process.env.SIGN_URI = 'https://proofbridge.xyz'; + +import { StellarAuthService } from './stellar-auth.service'; +import { accountIdToHex32 } from '../../../providers/stellar/utils/address'; +import type { PrismaService } from '@prisma/prisma.service'; + +describe('StellarAuthService (SEP-10)', () => { + const mockPrisma = { + authNonce: { create: jest.fn() }, + user: { upsert: jest.fn() }, + }; + + let service: StellarAuthService; + let client: Keypair; + + beforeEach(() => { + jest.clearAllMocks(); + service = new StellarAuthService(mockPrisma as unknown as PrismaService); + client = Keypair.random(); + }); + + describe('buildChallenge', () => { + it('returns a SEP-10 challenge for a valid G-strkey', () => { + const res = service.buildChallenge(client.publicKey()); + + expect(res.chainKind).toBe('STELLAR'); + expect(res.address).toBe(client.publicKey()); + expect(res.transaction).toEqual(expect.any(String)); + expect(res.networkPassphrase).toBeTruthy(); + expect(new Date(res.expiresAt).getTime()).toBeGreaterThan(Date.now()); + }); + + it('rejects non-Ed25519 inputs', () => { + expect(() => service.buildChallenge('not-a-strkey')).toThrow( + BadRequestException, + ); + expect(() => + service.buildChallenge('0x1234567890abcdef1234567890abcdef12345678'), + ).toThrow(BadRequestException); + }); + }); + + describe('verifyLogin', () => { + const sign = (xdr: string, kp: Keypair) => { + const tx = TransactionBuilder.fromXDR( + xdr, + (process.env.STELLAR_NETWORK_PASSPHRASE as Networks) ?? + Networks.TESTNET, + ); + tx.sign(kp); + return tx.toEnvelope().toXDR('base64'); + }; + + it('succeeds when the client co-signs the challenge and upserts the user', async () => { + mockPrisma.authNonce.create.mockResolvedValueOnce({}); + mockPrisma.user.upsert.mockImplementation( + (args: { create: { username: string; walletAddress: string } }) => + Promise.resolve({ + id: 'u1', + username: args.create.username, + walletAddress: args.create.walletAddress, + }), + ); + + const { transaction } = service.buildChallenge(client.publicKey()); + const signedXdr = sign(transaction, client); + + const user = await service.verifyLogin(signedXdr); + + expect(user.id).toBe('u1'); + expect(user.walletAddress).toBe(accountIdToHex32(client.publicKey())); + expect(mockPrisma.authNonce.create).toHaveBeenCalledTimes(1); + expect(mockPrisma.user.upsert).toHaveBeenCalledTimes(1); + }); + + it('rejects when the client signature is missing', async () => { + const { transaction } = service.buildChallenge(client.publicKey()); + + await expect(service.verifyLogin(transaction)).rejects.toThrow( + UnauthorizedException, + ); + expect(mockPrisma.authNonce.create).not.toHaveBeenCalled(); + }); + + it('rejects when a different keypair signs the challenge', async () => { + const imposter = Keypair.random(); + const { transaction } = service.buildChallenge(client.publicKey()); + const signedByImposter = sign(transaction, imposter); + + await expect(service.verifyLogin(signedByImposter)).rejects.toThrow( + UnauthorizedException, + ); + }); + + it('rejects replay (second login with the same XDR)', async () => { + mockPrisma.authNonce.create + .mockResolvedValueOnce({}) + .mockRejectedValueOnce(new Error('unique violation')); + mockPrisma.user.upsert.mockResolvedValue({ + id: 'u1', + username: 'a-b', + walletAddress: accountIdToHex32(client.publicKey()), + }); + + const { transaction } = service.buildChallenge(client.publicKey()); + const signedXdr = sign(transaction, client); + + await service.verifyLogin(signedXdr); + await expect(service.verifyLogin(signedXdr)).rejects.toThrow( + UnauthorizedException, + ); + }); + + it('rejects malformed XDR', async () => { + await expect(service.verifyLogin('not-valid-xdr')).rejects.toThrow( + UnauthorizedException, + ); + }); + }); +}); diff --git a/apps/backend-relayer/src/modules/auth/stellar/stellar-auth.service.ts b/apps/backend-relayer/src/modules/auth/stellar/stellar-auth.service.ts new file mode 100644 index 0000000..1326fb6 --- /dev/null +++ b/apps/backend-relayer/src/modules/auth/stellar/stellar-auth.service.ts @@ -0,0 +1,168 @@ +import { + BadRequestException, + Injectable, + Logger, + UnauthorizedException, +} from '@nestjs/common'; +import { + Keypair, + Networks, + StrKey, + TransactionBuilder, + WebAuth, +} from '@stellar/stellar-sdk'; +import { env } from '@libs/configs'; +import { PrismaService } from '@prisma/prisma.service'; +import { ChainKind } from '@prisma/client'; +import { accountIdToHex32 } from '../../../providers/stellar/utils/address'; +import { generateUniqueName } from '../username.util'; + +const CHALLENGE_TIMEOUT_SECONDS = 300; + +@Injectable() +export class StellarAuthService { + private readonly logger = new Logger(StellarAuthService.name); + private readonly serverKeypair: Keypair; + + constructor(private readonly prisma: PrismaService) { + const secret = env.stellar.authSecret; + if (!secret) { + throw new Error( + 'STELLAR_AUTH_SECRET not set — required for Stellar SEP-10 auth', + ); + } + if (!StrKey.isValidEd25519SecretSeed(secret)) { + throw new Error('STELLAR_AUTH_SECRET must be an S… Stellar secret seed'); + } + this.serverKeypair = Keypair.fromSecret(secret); + } + + /** + * Build a SEP-10 challenge transaction for the given account. The caller's + * wallet co-signs it and returns it to /auth/login. + */ + buildChallenge(accountId: string): { + chainKind: ChainKind; + address: string; + transaction: string; + networkPassphrase: string; + expiresAt: string; + } { + if (!StrKey.isValidEd25519PublicKey(accountId)) { + throw new BadRequestException( + 'Invalid Stellar account ID — expected G-strkey', + ); + } + + const transaction = WebAuth.buildChallengeTx( + this.serverKeypair, + accountId, + env.appDomain, + CHALLENGE_TIMEOUT_SECONDS, + env.stellar.networkPassphrase, + env.appDomain, + ); + + return { + chainKind: ChainKind.STELLAR, + address: accountId, + transaction, + networkPassphrase: env.stellar.networkPassphrase, + expiresAt: new Date( + Date.now() + CHALLENGE_TIMEOUT_SECONDS * 1000, + ).toISOString(), + }; + } + + /** + * Verify a co-signed SEP-10 challenge transaction and upsert the user. + * Returns the user row; the caller is responsible for minting JWTs. + */ + async verifyLogin(transactionXdr: string): Promise<{ + id: string; + username: string; + walletAddress: string; + }> { + const { walletAddress } = this.verifyChallenge(transactionXdr); + + // Challenge transactions are self-contained (server-signed + timebound), + // but nothing in SEP-10 prevents replay within the validity window — + // record the tx hash and reject duplicates. + const txHash = this.challengeTxHash(transactionXdr); + const now = new Date(); + try { + await this.prisma.authNonce.create({ + data: { + value: txHash, + walletAddress, + expiresAt: new Date(now.getTime() + CHALLENGE_TIMEOUT_SECONDS * 1000), + usedAt: now, + }, + }); + } catch { + throw new UnauthorizedException('Challenge already used'); + } + + return this.prisma.user.upsert({ + where: { walletAddress }, + create: { walletAddress, username: generateUniqueName() }, + update: {}, + select: { id: true, username: true, walletAddress: true }, + }); + } + + /** + * Verify the SEP-10 transaction structure and signatures. Throws on any + * failure; returns the client's G-strkey and its 0x+64hex canonical form. + */ + private verifyChallenge(transactionXdr: string): { + accountId: string; + walletAddress: `0x${string}`; + } { + let clientAccountID: string; + try { + const result = WebAuth.readChallengeTx( + transactionXdr, + this.serverKeypair.publicKey(), + env.stellar.networkPassphrase, + env.appDomain, + env.appDomain, + ); + clientAccountID = result.clientAccountID; + } catch (err) { + this.logger.warn( + `SEP-10 readChallengeTx failed: ${(err as Error).message}`, + ); + throw new UnauthorizedException('Invalid Stellar challenge transaction'); + } + + try { + WebAuth.verifyChallengeTxSigners( + transactionXdr, + this.serverKeypair.publicKey(), + env.stellar.networkPassphrase, + [clientAccountID], + env.appDomain, + env.appDomain, + ); + } catch (err) { + this.logger.warn( + `SEP-10 verifyChallengeTxSigners failed: ${(err as Error).message}`, + ); + throw new UnauthorizedException('Stellar challenge signature invalid'); + } + + return { + accountId: clientAccountID, + walletAddress: accountIdToHex32(clientAccountID), + }; + } + + private challengeTxHash(xdr: string): string { + const tx = TransactionBuilder.fromXDR( + xdr, + env.stellar.networkPassphrase as Networks, + ); + return tx.hash().toString('hex'); + } +} diff --git a/apps/backend-relayer/src/modules/auth/username.util.ts b/apps/backend-relayer/src/modules/auth/username.util.ts new file mode 100644 index 0000000..1cd1061 --- /dev/null +++ b/apps/backend-relayer/src/modules/auth/username.util.ts @@ -0,0 +1,14 @@ +import { + adjectives, + names, + uniqueNamesGenerator, +} from 'unique-names-generator'; + +export function generateUniqueName(): string { + return uniqueNamesGenerator({ + dictionaries: [adjectives, names], + separator: '-', + length: 2, + style: 'lowerCase', + }); +} diff --git a/apps/backend-relayer/src/modules/chains/dto/chain.dto.ts b/apps/backend-relayer/src/modules/chains/dto/chain.dto.ts index f95c196..ecbe74b 100644 --- a/apps/backend-relayer/src/modules/chains/dto/chain.dto.ts +++ b/apps/backend-relayer/src/modules/chains/dto/chain.dto.ts @@ -73,8 +73,9 @@ export class CreateChainDto { description: 'Ad Manager contract address', example: '0x1234567890abcdef1234567890abcdef12345678', }) - @Matches(/^0x[a-fA-F0-9]{40}$/, { - message: 'address must be a 0x-prefixed 20-byte hex address', + @Matches(/^0x[a-fA-F0-9]{40}(?:[a-fA-F0-9]{24})?$/, { + message: + 'address must be a 0x-prefixed 20-byte (EVM) or 32-byte (Stellar) hex address', }) @IsString() @IsNotEmpty() @@ -84,8 +85,9 @@ export class CreateChainDto { description: 'Order Portal contract address', example: '0xabcdef1234567890abcdef1234567890abcdef12', }) - @Matches(/^0x[a-fA-F0-9]{40}$/, { - message: 'address must be a 0x-prefixed 20-byte hex address', + @Matches(/^0x[a-fA-F0-9]{40}(?:[a-fA-F0-9]{24})?$/, { + message: + 'address must be a 0x-prefixed 20-byte (EVM) or 32-byte (Stellar) hex address', }) @IsString() @IsNotEmpty() diff --git a/apps/backend-relayer/src/modules/faucet/faucet.module.ts b/apps/backend-relayer/src/modules/faucet/faucet.module.ts index 0638228..d7673ed 100644 --- a/apps/backend-relayer/src/modules/faucet/faucet.module.ts +++ b/apps/backend-relayer/src/modules/faucet/faucet.module.ts @@ -3,11 +3,11 @@ import { JwtModule, JwtService } from '@nestjs/jwt'; import { PrismaService } from '@prisma/prisma.service'; import { FaucetController } from './faucet.controller'; import { FaucetService } from './faucet.service'; -import { ChainProvidersModule } from '../../providers/chain/chain.module'; +import { ChainAdapterModule } from '../../chain-adapters/chain-adapter.module'; import { UserJwtGuard } from '../../common/guards/user-jwt.guard'; @Module({ - imports: [JwtModule.register({}), ChainProvidersModule], + imports: [JwtModule.register({}), ChainAdapterModule], controllers: [FaucetController], providers: [FaucetService, PrismaService, UserJwtGuard, JwtService], }) diff --git a/apps/backend-relayer/src/modules/faucet/faucet.service.ts b/apps/backend-relayer/src/modules/faucet/faucet.service.ts index 431c073..afe97a3 100644 --- a/apps/backend-relayer/src/modules/faucet/faucet.service.ts +++ b/apps/backend-relayer/src/modules/faucet/faucet.service.ts @@ -7,7 +7,7 @@ import { HttpStatus, } from '@nestjs/common'; import { PrismaService } from '@prisma/prisma.service'; -import { ChainProviderService } from '../../providers/chain/chain-provider.service'; +import { ChainAdapterService } from '../../chain-adapters/chain-adapter.service'; import { RequestFaucetDto, FaucetResponseDto } from './dto/faucet.dto'; import { Request } from 'express'; @@ -15,7 +15,7 @@ import { Request } from 'express'; export class FaucetService { constructor( private readonly prisma: PrismaService, - private readonly chainProviders: ChainProviderService, + private readonly chainAdapters: ChainAdapterService, ) {} async requestFaucet( @@ -52,7 +52,7 @@ export class FaucetService { throw new NotFoundException(`Token with ID ${dto.tokenId} not found`); } - const provider = this.chainProviders.forChain(token.chain.kind); + const provider = this.chainAdapters.forChain(token.chain.kind); // Check user's token balance const balance = await provider.checkTokenBalance({ diff --git a/apps/backend-relayer/src/modules/trades/trade.module.ts b/apps/backend-relayer/src/modules/trades/trade.module.ts index 2727f39..1332cae 100644 --- a/apps/backend-relayer/src/modules/trades/trade.module.ts +++ b/apps/backend-relayer/src/modules/trades/trade.module.ts @@ -3,13 +3,13 @@ import { PrismaService } from '@prisma/prisma.service'; import { JwtModule, JwtService } from '@nestjs/jwt'; import { TradesService } from './trade.service'; import { TradesController } from './trade.controller'; -import { ChainProvidersModule } from '../../providers/chain/chain.module'; +import { ChainAdapterModule } from '../../chain-adapters/chain-adapter.module'; import { MMRService } from '../mmr/mmr.service'; import { ProofModule } from '../../providers/noir/proof.module'; import { EncryptionService } from '@libs/encryption.service'; @Module({ - imports: [JwtModule.register({}), ChainProvidersModule, ProofModule], + imports: [JwtModule.register({}), ChainAdapterModule, ProofModule], controllers: [TradesController], providers: [ TradesService, diff --git a/apps/backend-relayer/src/modules/trades/trade.service.ts b/apps/backend-relayer/src/modules/trades/trade.service.ts index 5d6c676..969d64f 100644 --- a/apps/backend-relayer/src/modules/trades/trade.service.ts +++ b/apps/backend-relayer/src/modules/trades/trade.service.ts @@ -15,8 +15,8 @@ import { UnlockTradeDto, } from './dto/trade.dto'; import { getAddress, isAddress } from 'viem'; -import { Request, Response } from 'express'; -import { ChainProviderService } from '../../providers/chain/chain-provider.service'; +import { Request } from 'express'; +import { ChainAdapterService } from '../../chain-adapters/chain-adapter.service'; import { MMRService } from '../mmr/mmr.service'; import { ProofService } from '../../providers/noir/proof.service'; import { randomUUID } from 'crypto'; @@ -28,7 +28,7 @@ import { toBytes32, uuidToBigInt } from '../../providers/viem/ethers/typedData'; export class TradesService { constructor( private readonly prisma: PrismaService, - private readonly chainProviders: ChainProviderService, + private readonly chainAdapters: ChainAdapterService, private readonly merkleService: MMRService, private readonly proofService: ProofService, private readonly encryptionService: EncryptionService, @@ -367,7 +367,7 @@ export class TradesService { const secret = this.proofService.generateSecret(); const tradeId = randomUUID(); - const reqContractDetails = await this.chainProviders + const reqContractDetails = await this.chainAdapters .forChain(ad.route.orderToken.chain.kind) .getCreateOrderRequestContractDetails({ orderChainId: ad.route.orderToken.chain.chainId, @@ -646,7 +646,7 @@ export class TradesService { throw new BadRequestException('AdLock amount mismatch'); } - const reqContractDetails = await this.chainProviders + const reqContractDetails = await this.chainAdapters .forChain(trade.route.adToken.chain.kind) .getLockForOrderRequestContractDetails({ adChainId: trade.route.adToken.chain.chainId, @@ -797,7 +797,7 @@ export class TradesService { ? trade.route.orderToken.chain : trade.route.adToken.chain; - const isAuthorized = this.chainProviders + const isAuthorized = this.chainAdapters .forChain(unlockChain.kind) .verifyOrderSignature( caller, @@ -841,7 +841,7 @@ export class TradesService { const localRoot = await this.merkleService.getRoot(mmrId); - const rootExists = await this.chainProviders + const rootExists = await this.chainAdapters .forChain(unlockChain.kind) .checkLocalRootExist(localRoot, isAdCreator, { chainId: unlockChain.chainId, @@ -876,7 +876,7 @@ export class TradesService { trade.orderHash, ); - const requestContractDetails = await this.chainProviders + const requestContractDetails = await this.chainAdapters .forChain(unlockChain.kind) .getUnlockOrderContractDetails({ chainId: unlockChain.chainId, @@ -1010,7 +1010,7 @@ export class TradesService { if (tradeLogUpdate.origin === 'AD_MANAGER') { // verify adLog - const isValidated = await this.chainProviders + const isValidated = await this.chainAdapters .forChain(trade.route.adToken.chain.kind) .validateAdManagerRequest({ chainId: trade.route.adToken.chain.chainId, @@ -1024,7 +1024,7 @@ export class TradesService { } } else { // verify orderPortal - const isValidated = await this.chainProviders + const isValidated = await this.chainAdapters .forChain(trade.route.orderToken.chain.kind) .validateOrderPortalRequest({ chainId: trade.route.orderToken.chain.chainId, @@ -1174,7 +1174,7 @@ export class TradesService { if (authorizationLog.origin === 'AD_MANAGER') { // verify log - const isValidated = await this.chainProviders + const isValidated = await this.chainAdapters .forChain(trade.route.adToken.chain.kind) .validateAdManagerRequest({ chainId: trade.route.adToken.chain.chainId, @@ -1188,7 +1188,7 @@ export class TradesService { } } else { // verify log - const isValidated = await this.chainProviders + const isValidated = await this.chainAdapters .forChain(trade.route.orderToken.chain.kind) .validateOrderPortalRequest({ chainId: trade.route.orderToken.chain.chainId, diff --git a/apps/backend-relayer/src/providers/chain/chain-provider.service.ts b/apps/backend-relayer/src/providers/chain/chain-provider.service.ts deleted file mode 100644 index c69838a..0000000 --- a/apps/backend-relayer/src/providers/chain/chain-provider.service.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ChainKind } from '@prisma/client'; -import { ChainProvider } from './chain-provider.abstract'; -import { EvmChainProvider } from './evm-chain-provider'; - -// Routes to the concrete ChainProvider implementation for a given chain kind. -// ad/trade/faucet services inject this and do -// `providers.forChain(chain.kind).X(...)` — adding a new chain family is just -// a new ChainProvider subclass wired into this switch. -@Injectable() -export class ChainProviderService { - constructor(private readonly evm: EvmChainProvider) {} - - forChain(kind: ChainKind): ChainProvider { - switch (kind) { - case ChainKind.EVM: - return this.evm; - case ChainKind.STELLAR: - throw new Error('StellarChainProvider not yet implemented'); - default: { - throw new Error(`Unsupported chain kind: ${String(kind)}`); - } - } - } -} diff --git a/apps/backend-relayer/src/providers/chain/chain.module.ts b/apps/backend-relayer/src/providers/chain/chain.module.ts deleted file mode 100644 index f79b952..0000000 --- a/apps/backend-relayer/src/providers/chain/chain.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ViemModule } from '../viem/viem.module'; -import { EvmChainProvider } from './evm-chain-provider'; -import { ChainProviderService } from './chain-provider.service'; - -@Module({ - imports: [ViemModule], - providers: [EvmChainProvider, ChainProviderService], - exports: [ChainProviderService], -}) -export class ChainProvidersModule {} diff --git a/apps/backend-relayer/src/providers/stellar/stellar.module.ts b/apps/backend-relayer/src/providers/stellar/stellar.module.ts new file mode 100644 index 0000000..628f998 --- /dev/null +++ b/apps/backend-relayer/src/providers/stellar/stellar.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { StellarService } from './stellar.service'; + +@Module({ + imports: [], + providers: [StellarService], + controllers: [], + exports: [StellarService], +}) +export class StellarModule {} diff --git a/apps/backend-relayer/src/providers/stellar/stellar.service.ts b/apps/backend-relayer/src/providers/stellar/stellar.service.ts new file mode 100644 index 0000000..18d830b --- /dev/null +++ b/apps/backend-relayer/src/providers/stellar/stellar.service.ts @@ -0,0 +1,599 @@ +// StellarService — Soroban counterpart to ViemService. Produces presigned +// manager blobs for AdManager / OrderPortal on Stellar (ed25519 over the same +// keccak256 request-hash layout the contracts compute in `verify_request`) +// and handles read/write RPC against Soroban contracts. + +import { Injectable } from '@nestjs/common'; +import { + Address, + Contract, + Keypair, + Networks, + StrKey, + TransactionBuilder, + nativeToScVal, + rpc, + scValToNative, + xdr, +} from '@stellar/stellar-sdk'; +import BigNumber from 'bignumber.js'; +import { getTime } from 'date-fns'; +import { env } from '@libs/configs'; +import { + T_AdManagerOrderParams, + T_CloseAdRequest, + T_CloseAdRequestContractDetails, + T_CreatFundAdRequest, + T_CreatFundAdRequestContractDetails, + T_CreateAdRequest, + T_CreateAdRequestContractDetails, + T_CreateOrderRequest, + T_CreateOrderRequestContractDetails, + T_CreateUnlockOrderContractDetails, + T_FetchRoot, + T_LockForOrderRequest, + T_LockForOrderRequestContractDetails, + T_OrderParams, + T_OrderPortalParams, + T_RequestValidation, + T_UnlockOrderContractDetails, + T_WithdrawFromAdRequest, + T_WithdrawFromAdRequestContractDetails, +} from '../../chain-adapters/types'; +import { buildOrderParams } from '../viem/ethers/typedData'; +import { + closeAdRequestHash, + createAdRequestHash, + createOrderRequestHash, + ed25519PublicKey, + fundAdRequestHash, + lockForOrderRequestHash, + randomAuthToken, + signEd25519, + unlockOrderRequestHash, + verifyEd25519, + withdrawFromAdRequestHash, +} from './utils/signing'; +import { + bufferToHex32, + hex32ToBuffer, + hex32ToContractId, +} from './utils/address'; +import { computeOrderHash } from './utils/eip712'; + +const MILLISECOND = 1000; +const FIVE_MINUTES_MS = 5 * 60 * 1000; +const TEN_MINUTES_MS = 10 * 60 * 1000; +const ONE_HOUR_MS = 60 * 60 * 1000; +const DEFAULT_BASE_FEE = '1000'; + +type ManagerSigner = { + seed: Buffer; + publicKey: Buffer; + keypair: Keypair; +}; + +@Injectable() +export class StellarService { + private signerCache: ManagerSigner | null = null; + + private getServer(): rpc.Server { + return new rpc.Server(env.stellar.rpcUrl, { + allowHttp: env.stellar.rpcUrl.startsWith('http://'), + }); + } + + private networkPassphrase(): string { + return env.stellar.networkPassphrase || Networks.TESTNET; + } + + private getSigner(): ManagerSigner { + if (this.signerCache) return this.signerCache; + if (!env.stellar.adminSecret) { + throw new Error( + 'Missing STELLAR_ADMIN_SECRET (expected S… strkey or 32-byte hex)', + ); + } + const raw = env.stellar.adminSecret.trim(); + let seed: Buffer; + if (StrKey.isValidEd25519SecretSeed(raw)) { + seed = Buffer.from(StrKey.decodeEd25519SecretSeed(raw)); + } else if (/^0x[a-fA-F0-9]{64}$/.test(raw)) { + seed = Buffer.from(raw.slice(2), 'hex'); + } else { + throw new Error( + 'Invalid STELLAR_ADMIN_SECRET format (want S… strkey or 0x-prefixed 32-byte hex)', + ); + } + const publicKey = ed25519PublicKey(seed); + const keypair = Keypair.fromRawEd25519Seed(seed); + this.signerCache = { seed, publicKey, keypair }; + return this.signerCache; + } + + private sign(message: Buffer): { + signature: `0x${string}`; + signerPublicKey: `0x${string}`; + } { + const signer = this.getSigner(); + const sig = signEd25519(message, signer.seed); + return { + signature: `0x${sig.toString('hex')}`, + signerPublicKey: `0x${signer.publicKey.toString('hex')}`, + }; + } + + private nextExpiry(horizonMs: number): { + authToken: Buffer; + timeToExpireSec: bigint; + timeToExpireNum: number; + } { + const authToken = randomAuthToken(); + const timeMs = getTime(new Date()) + horizonMs; + const timeToExpire = BigNumber(timeMs).div(MILLISECOND).toFixed(0); + return { + authToken, + timeToExpireSec: BigInt(timeToExpire), + timeToExpireNum: Number(timeToExpire), + }; + } + + // ── presigned requests ────────────────────────────────────────────── + + getCreateAdRequestContractDetails( + data: T_CreateAdRequest, + ): Promise { + const { + adChainId, + adContractAddress, + adId, + adToken, + initialAmount, + orderChainId, + adRecipient, + } = data; + const { authToken, timeToExpireSec, timeToExpireNum } = + this.nextExpiry(ONE_HOUR_MS); + const message = createAdRequestHash({ + authToken, + timeToExpire: timeToExpireSec, + adId, + adToken: hex32ToBuffer(adToken), + amount: BigInt(initialAmount), + orderChainId: BigInt(orderChainId), + adRecipient: hex32ToBuffer(adRecipient), + chainId: BigInt(adChainId), + contractAddress: hex32ToBuffer(adContractAddress), + }); + const { signature, signerPublicKey } = this.sign(message); + return Promise.resolve({ + chainId: adChainId.toString(), + contractAddress: adContractAddress, + signature, + signerPublicKey, + authToken: `0x${authToken.toString('hex')}`, + timeToExpire: timeToExpireNum, + adId, + adToken, + initialAmount, + orderChainId: orderChainId.toString(), + adRecipient, + reqHash: bufferToHex32(message), + }); + } + + getFundAdRequestContractDetails( + data: T_CreatFundAdRequest, + ): Promise { + const { adChainId, adContractAddress, adId, amount } = data; + const { authToken, timeToExpireSec, timeToExpireNum } = + this.nextExpiry(FIVE_MINUTES_MS); + const message = fundAdRequestHash({ + authToken, + timeToExpire: timeToExpireSec, + adId, + amount: BigInt(amount), + chainId: BigInt(adChainId), + contractAddress: hex32ToBuffer(adContractAddress), + }); + const { signature, signerPublicKey } = this.sign(message); + return Promise.resolve({ + chainId: adChainId.toString(), + contractAddress: adContractAddress, + signature, + signerPublicKey, + authToken: `0x${authToken.toString('hex')}`, + timeToExpire: timeToExpireNum, + adId, + amount, + reqHash: bufferToHex32(message), + }); + } + + getWithdrawFromAdRequestContractDetails( + data: T_WithdrawFromAdRequest, + ): Promise { + const { adChainId, adContractAddress, adId, amount, to } = data; + const { authToken, timeToExpireSec, timeToExpireNum } = + this.nextExpiry(FIVE_MINUTES_MS); + const message = withdrawFromAdRequestHash({ + authToken, + timeToExpire: timeToExpireSec, + adId, + amount: BigInt(amount), + to: hex32ToBuffer(to), + chainId: BigInt(adChainId), + contractAddress: hex32ToBuffer(adContractAddress), + }); + const { signature, signerPublicKey } = this.sign(message); + return Promise.resolve({ + chainId: adChainId.toString(), + contractAddress: adContractAddress, + signature, + signerPublicKey, + authToken: `0x${authToken.toString('hex')}`, + timeToExpire: timeToExpireNum, + adId, + amount, + to, + reqHash: bufferToHex32(message), + }); + } + + getCloseAdRequestContractDetails( + data: T_CloseAdRequest, + ): Promise { + const { adChainId, adContractAddress, adId, to } = data; + const { authToken, timeToExpireSec, timeToExpireNum } = + this.nextExpiry(FIVE_MINUTES_MS); + const message = closeAdRequestHash({ + authToken, + timeToExpire: timeToExpireSec, + adId, + to: hex32ToBuffer(to), + chainId: BigInt(adChainId), + contractAddress: hex32ToBuffer(adContractAddress), + }); + const { signature, signerPublicKey } = this.sign(message); + return Promise.resolve({ + chainId: adChainId.toString(), + contractAddress: adContractAddress, + signature, + signerPublicKey, + authToken: `0x${authToken.toString('hex')}`, + timeToExpire: timeToExpireNum, + adId, + to, + reqHash: bufferToHex32(message), + }); + } + + getLockForOrderRequestContractDetails( + data: T_LockForOrderRequest, + ): Promise { + const { adChainId, adContractAddress, orderParams } = data; + const { authToken, timeToExpireSec, timeToExpireNum } = + this.nextExpiry(TEN_MINUTES_MS); + const orderHash = computeOrderHash(orderParams); + const message = lockForOrderRequestHash({ + authToken, + timeToExpire: timeToExpireSec, + adId: orderParams.adId, + orderHash: hex32ToBuffer(orderHash), + chainId: BigInt(adChainId), + contractAddress: hex32ToBuffer(adContractAddress), + }); + const { signature, signerPublicKey } = this.sign(message); + const params = buildOrderParams( + orderParams, + true, + ) as T_AdManagerOrderParams; + return Promise.resolve({ + chainId: adChainId.toString(), + contractAddress: adContractAddress, + signature, + signerPublicKey, + authToken: `0x${authToken.toString('hex')}`, + timeToExpire: timeToExpireNum, + orderParams: params, + reqHash: bufferToHex32(message), + orderHash, + }); + } + + getCreateOrderRequestContractDetails( + data: T_CreateOrderRequest, + ): Promise { + const { orderChainId, orderContractAddress, orderParams } = data; + const { authToken, timeToExpireSec, timeToExpireNum } = + this.nextExpiry(TEN_MINUTES_MS); + const orderHash = computeOrderHash(orderParams); + const message = createOrderRequestHash({ + authToken, + timeToExpire: timeToExpireSec, + adId: orderParams.adId, + orderHash: hex32ToBuffer(orderHash), + chainId: BigInt(orderChainId), + contractAddress: hex32ToBuffer(orderContractAddress), + }); + const { signature, signerPublicKey } = this.sign(message); + const params = buildOrderParams(orderParams, false) as T_OrderPortalParams; + return Promise.resolve({ + chainId: orderChainId.toString(), + contractAddress: orderContractAddress, + signature, + signerPublicKey, + authToken: `0x${authToken.toString('hex')}`, + timeToExpire: timeToExpireNum, + orderParams: params, + reqHash: bufferToHex32(message), + orderHash, + }); + } + + getUnlockOrderContractDetails( + data: T_CreateUnlockOrderContractDetails, + ): Promise { + const { + chainId, + contractAddress, + isAdCreator, + orderParams, + nullifierHash, + targetRoot, + proof, + } = data; + const { authToken, timeToExpireSec, timeToExpireNum } = + this.nextExpiry(TEN_MINUTES_MS); + const orderHash = computeOrderHash(orderParams); + const message = unlockOrderRequestHash({ + authToken, + timeToExpire: timeToExpireSec, + adId: orderParams.adId, + orderHash: hex32ToBuffer(orderHash), + targetRoot: hex32ToBuffer(targetRoot), + chainId: BigInt(chainId), + contractAddress: hex32ToBuffer(contractAddress), + }); + const { signature, signerPublicKey } = this.sign(message); + const params = buildOrderParams(orderParams, !isAdCreator); + return Promise.resolve({ + chainId: chainId.toString(), + contractAddress, + signature, + signerPublicKey, + authToken: `0x${authToken.toString('hex')}`, + timeToExpire: timeToExpireNum, + orderParams: params, + nullifierHash, + targetRoot, + proof, + reqHash: bufferToHex32(message), + orderHash, + }); + } + + // ── read-only contract views (simulateTransaction) ────────────────── + + private async simulateView( + contractAddressHex: string, + method: string, + args: xdr.ScVal[], + ): Promise { + const server = this.getServer(); + const contract = new Contract(hex32ToContractId(contractAddressHex)); + // Use a throwaway source account — simulateTransaction only needs the + // sequence, not a valid signer. + const { publicKey } = this.getSigner(); + const sourceKey = StrKey.encodeEd25519PublicKey(publicKey); + const sourceAccount = await server.getAccount(sourceKey); + const tx = new TransactionBuilder(sourceAccount, { + fee: DEFAULT_BASE_FEE, + networkPassphrase: this.networkPassphrase(), + }) + .addOperation(contract.call(method, ...args)) + .setTimeout(30) + .build(); + const sim = await server.simulateTransaction(tx); + if (rpc.Api.isSimulationError(sim)) { + throw new Error(`Stellar simulate failed [${method}]: ${sim.error}`); + } + if (!sim.result) { + throw new Error(`Stellar simulate [${method}] returned no result`); + } + return scValToNative(sim.result.retval) as T; + } + + private async invokeWrite( + contractAddressHex: string, + method: string, + args: xdr.ScVal[], + ): Promise { + const server = this.getServer(); + const contract = new Contract(hex32ToContractId(contractAddressHex)); + const { keypair, publicKey } = this.getSigner(); + const sourceKey = StrKey.encodeEd25519PublicKey(publicKey); + const sourceAccount = await server.getAccount(sourceKey); + const tx = new TransactionBuilder(sourceAccount, { + fee: DEFAULT_BASE_FEE, + networkPassphrase: this.networkPassphrase(), + }) + .addOperation(contract.call(method, ...args)) + .setTimeout(60) + .build(); + const prepared = await server.prepareTransaction(tx); + prepared.sign(keypair); + const sent = await server.sendTransaction(prepared); + if (sent.status === 'ERROR') { + throw new Error( + `Stellar send failed [${method}]: ${JSON.stringify(sent.errorResult)}`, + ); + } + let attempts = 0; + let result = await server.getTransaction(sent.hash); + while ( + result.status === rpc.Api.GetTransactionStatus.NOT_FOUND && + attempts < 15 + ) { + await new Promise((r) => setTimeout(r, 1000)); + result = await server.getTransaction(sent.hash); + attempts += 1; + } + if (result.status !== rpc.Api.GetTransactionStatus.SUCCESS) { + throw new Error( + `Stellar tx [${method}] status=${result.status} hash=${sent.hash}`, + ); + } + return sent.hash; + } + + async validateAdManagerRequest(data: T_RequestValidation): Promise { + return this.simulateView( + data.contractAddress, + 'check_request_hash_exists', + [nativeToScVal(hex32ToBuffer(data.reqHash), { type: 'bytes' })], + ); + } + + async validateOrderPortalRequest( + data: T_RequestValidation, + ): Promise { + return this.simulateView( + data.contractAddress, + 'check_request_hash_exists', + [nativeToScVal(hex32ToBuffer(data.reqHash), { type: 'bytes' })], + ); + } + + async fetchOnChainLatestRoot( + isAdCreator: boolean, + data: T_FetchRoot, + ): Promise { + return isAdCreator + ? this.fetchAdChainLatestRoot(data) + : this.fetchOrderChainLatestRoot(data); + } + + async fetchAdChainLatestRoot(data: T_FetchRoot): Promise { + const raw = await this.simulateView( + data.contractAddress, + 'get_latest_merkle_root', + [], + ); + return bufferToHex32(Buffer.from(raw)); + } + + async fetchOrderChainLatestRoot(data: T_FetchRoot): Promise { + const raw = await this.simulateView( + data.contractAddress, + 'get_latest_merkle_root', + [], + ); + return bufferToHex32(Buffer.from(raw)); + } + + async checkLocalRootExist( + localRoot: string, + isAdCreator: boolean, + data: T_FetchRoot, + ): Promise { + const onChainRoots = await this.fetchOnChainRoots(isAdCreator, data); + const needle = localRoot.toLowerCase(); + return onChainRoots.map((r) => r.toLowerCase()).includes(needle); + } + + async fetchOnChainRoots( + isAdCreator: boolean, + data: T_FetchRoot, + ): Promise { + return isAdCreator + ? this.fetchOrderChainRoots(data) + : this.fetchAdChainRoots(data); + } + + async fetchAdChainRoots(data: T_FetchRoot): Promise { + return this.fetchHistoricalRoots(data); + } + + async fetchOrderChainRoots(data: T_FetchRoot): Promise { + return this.fetchHistoricalRoots(data); + } + + private async fetchHistoricalRoots(data: T_FetchRoot): Promise { + const leafCount = await this.simulateView( + data.contractAddress, + 'get_merkle_leaf_count', + [], + ); + const roots: string[] = []; + const max = Number(leafCount); + for (let i = 1; i <= max; i++) { + try { + const raw = await this.simulateView( + data.contractAddress, + 'get_historical_root', + [nativeToScVal(BigInt(i), { type: 'u128' })], + ); + roots.push(bufferToHex32(Buffer.from(raw))); + } catch (err) { + console.warn(`[stellar historicalRoot] index=${i} failed:`, err); + } + } + return roots; + } + + async mintToken(data: { + chainId: string; + tokenAddress: `0x${string}`; + receiver: `0x${string}`; + }): Promise<{ txHash: string }> { + const receiverStrkey = StrKey.isValidEd25519PublicKey(data.receiver) + ? data.receiver + : StrKey.encodeEd25519PublicKey(hex32ToBuffer(data.receiver)); + const amount = BigNumber('1000000').multipliedBy(1e7).toFixed(0); + const txHash = await this.invokeWrite(data.tokenAddress, 'mint', [ + new Address(receiverStrkey).toScVal(), + nativeToScVal(BigInt(amount), { type: 'i128' }), + ]); + return { txHash }; + } + + async checkTokenBalance(data: { + chainId: string; + tokenAddress: `0x${string}`; + account: `0x${string}`; + }): Promise { + const accountStrkey = StrKey.isValidEd25519PublicKey(data.account) + ? data.account + : StrKey.encodeEd25519PublicKey(hex32ToBuffer(data.account)); + const balance = await this.simulateView( + data.tokenAddress, + 'balance', + [new Address(accountStrkey).toScVal()], + ); + return balance.toString(); + } + + orderTypeHash(orderParams: T_OrderParams): string { + return computeOrderHash(orderParams); + } + + // Verify an ed25519 signature over `orderHash` (32-byte hex). On Stellar the + // "address" passed in is the G-strkey or its 32-byte hex — we take the + // 32-byte payload as the ed25519 public key. + verifyOrderSignature( + address: `0x${string}`, + orderHash: `0x${string}`, + signature: `0x${string}`, + ): boolean { + const publicKey = hex32ToBuffer(address); + const msg = hex32ToBuffer(orderHash); + const sigBytes = Buffer.from(signature.replace(/^0x/, ''), 'hex'); + if (sigBytes.length !== 64) return false; + try { + return verifyEd25519(msg, sigBytes, publicKey); + } catch { + return false; + } + } +} diff --git a/apps/backend-relayer/src/providers/stellar/utils/address.ts b/apps/backend-relayer/src/providers/stellar/utils/address.ts new file mode 100644 index 0000000..622f649 --- /dev/null +++ b/apps/backend-relayer/src/providers/stellar/utils/address.ts @@ -0,0 +1,49 @@ +// Address helpers for the Stellar side. Cross-chain fields in the relayer are +// already stored as `0x`-prefixed 32-byte hex (the payload of a C.../G... strkey +// without version byte or checksum). These helpers convert between that hex +// form and the Stellar SDK's strkey representation. +// +// For chain records, `adManagerAddress`/`orderPortalAddress` hold the 32-byte +// hex form of the contract's C-strkey. The service decodes it back to a +// strkey before issuing RPC calls. + +import { StrKey } from '@stellar/stellar-sdk'; + +const HEX32_RE = /^0x[a-fA-F0-9]{64}$/; +const HEX20_RE = /^0x[a-fA-F0-9]{40}$/; + +export function hex32ToBuffer(hex: string): Buffer { + if (HEX32_RE.test(hex)) return Buffer.from(hex.slice(2), 'hex'); + if (HEX20_RE.test(hex)) { + // 20-byte EVM address: left-pad with 12 zero bytes so it fits the + // bytes32 field shape used in cross-chain order params. + const clean = hex.slice(2).toLowerCase(); + return Buffer.from('00'.repeat(12) + clean, 'hex'); + } + throw new Error( + `invalid bytes32-or-evm hex address: ${hex} (want 0x + 40 or 64 hex chars)`, + ); +} + +export function bufferToHex32(buf: Buffer): `0x${string}` { + if (buf.length !== 32) throw new Error('bufferToHex32: expected 32 bytes'); + return `0x${buf.toString('hex')}`; +} + +// 32-byte hex → C-strkey (Soroban contract address). +export function hex32ToContractId(hex: string): string { + return StrKey.encodeContract(hex32ToBuffer(hex)); +} + +// 32-byte hex → G-strkey (ed25519 account). +export function hex32ToAccountId(hex: string): string { + return StrKey.encodeEd25519PublicKey(hex32ToBuffer(hex)); +} + +export function contractIdToHex32(strkey: string): `0x${string}` { + return bufferToHex32(Buffer.from(StrKey.decodeContract(strkey))); +} + +export function accountIdToHex32(strkey: string): `0x${string}` { + return bufferToHex32(Buffer.from(StrKey.decodeEd25519PublicKey(strkey))); +} diff --git a/apps/backend-relayer/src/providers/stellar/utils/eip712.ts b/apps/backend-relayer/src/providers/stellar/utils/eip712.ts new file mode 100644 index 0000000..9afd147 --- /dev/null +++ b/apps/backend-relayer/src/providers/stellar/utils/eip712.ts @@ -0,0 +1,68 @@ +// Canonical cross-chain Order hash, mirroring both the EVM EIP-712 typed-data +// encoding and Stellar's `eip712::hash_order` (contracts/stellar/…/eip712.rs). +// The Stellar contracts include this same hash over the bytes32-form order +// fields, so this one implementation serves both the EVM order signing and +// the Stellar request-hash `order_hash` parameter. + +import { keccak256 } from 'viem'; +import { T_OrderParams } from '../../../chain-adapters/types'; + +function keccak(data: Buffer): Buffer { + const hex = keccak256(`0x${data.toString('hex')}`); + return Buffer.from(hex.slice(2), 'hex'); +} + +function hexToBytes32(hex: string): Buffer { + const clean = hex.replace(/^0x/i, '').padStart(64, '0'); + return Buffer.from(clean, 'hex'); +} + +function u256BE(value: bigint): Buffer { + const buf = Buffer.alloc(32, 0); + const hex = value.toString(16).padStart(64, '0'); + Buffer.from(hex, 'hex').copy(buf); + return buf; +} + +const DOMAIN_TYPEHASH_MIN = keccak( + Buffer.from('EIP712Domain(string name,string version)'), +); +const NAME_HASH = keccak(Buffer.from('Proofbridge')); +const VERSION_HASH = keccak(Buffer.from('1')); +const ORDER_TYPEHASH = keccak( + Buffer.from( + 'Order(bytes32 orderChainToken,bytes32 adChainToken,uint256 amount,bytes32 bridger,uint256 orderChainId,bytes32 orderPortal,bytes32 orderRecipient,uint256 adChainId,bytes32 adManager,string adId,bytes32 adCreator,bytes32 adRecipient,uint256 salt)', + ), +); + +function domainSeparator(): Buffer { + return keccak(Buffer.concat([DOMAIN_TYPEHASH_MIN, NAME_HASH, VERSION_HASH])); +} + +function structHashOrder(p: T_OrderParams): Buffer { + return keccak( + Buffer.concat([ + ORDER_TYPEHASH, + hexToBytes32(p.orderChainToken), + hexToBytes32(p.adChainToken), + u256BE(BigInt(p.amount)), + hexToBytes32(p.bridger), + u256BE(BigInt(p.orderChainId)), + hexToBytes32(p.orderPortal), + hexToBytes32(p.orderRecipient), + u256BE(BigInt(p.adChainId)), + hexToBytes32(p.adManager), + keccak(Buffer.from(p.adId)), + hexToBytes32(p.adCreator), + hexToBytes32(p.adRecipient), + u256BE(BigInt(p.salt)), + ]), + ); +} + +export function computeOrderHash(params: T_OrderParams): `0x${string}` { + const structHash = structHashOrder(params); + const prefix = Buffer.from([0x19, 0x01]); + const hash = keccak(Buffer.concat([prefix, domainSeparator(), structHash])); + return `0x${hash.toString('hex')}`; +} diff --git a/apps/backend-relayer/src/providers/stellar/utils/signing.ts b/apps/backend-relayer/src/providers/stellar/utils/signing.ts new file mode 100644 index 0000000..3cd6dc3 --- /dev/null +++ b/apps/backend-relayer/src/providers/stellar/utils/signing.ts @@ -0,0 +1,251 @@ +// Keccak256-based request-hash builders mirroring `proofbridge-core::auth` on +// Stellar (see contracts/stellar/contracts/ad-manager/src/auth.rs). These must +// byte-for-byte match what the Soroban contracts compute in `verify_request`. +// +// Port of scripts/cross-chain-e2e/lib/signing.ts — kept locally here so the +// backend-relayer doesn't depend on the test-harness package. + +import { keccak256 } from 'viem'; +import { ed25519 } from '@noble/curves/ed25519.js'; +import { randomBytes } from 'crypto'; + +const U64_BE_LEN = 8; +const U128_BE_LEN = 16; +const BYTES32_LEN = 32; + +export function keccak(data: Buffer): Buffer { + const hex = keccak256(`0x${data.toString('hex')}`); + return Buffer.from(hex.slice(2), 'hex'); +} + +export function hashStringField(s: string): Buffer { + return keccak(Buffer.from(s)); +} + +function u64BE(value: bigint): Buffer { + const buf = Buffer.alloc(U64_BE_LEN); + buf.writeBigUInt64BE(value); + return buf; +} + +function u128BE(value: bigint): Buffer { + const buf = Buffer.alloc(U128_BE_LEN); + buf.writeBigUInt64BE(value >> 64n, 0); + buf.writeBigUInt64BE(value & 0xffffffffffffffffn, 8); + return buf; +} + +// keccak256(auth_token(32) || time_to_expire(8 BE) || keccak256(action)(32) +// || params(var) || chain_id(16 BE) || contract_address(32)) +export function hashRequest( + authToken: Buffer, + timeToExpire: bigint, + action: string, + params: Buffer, + chainId: bigint, + contractAddress: Buffer, +): Buffer { + if (authToken.length !== BYTES32_LEN) + throw new Error('authToken must be 32 bytes'); + if (contractAddress.length !== BYTES32_LEN) + throw new Error('contractAddress must be 32 bytes'); + + return keccak( + Buffer.concat([ + authToken, + u64BE(timeToExpire), + keccak(Buffer.from(action)), + params, + u128BE(chainId), + contractAddress, + ]), + ); +} + +export function createAdRequestHash(opts: { + authToken: Buffer; + timeToExpire: bigint; + adId: string; + adToken: Buffer; // 32 bytes + amount: bigint; + orderChainId: bigint; + adRecipient: Buffer; // 32 bytes + chainId: bigint; + contractAddress: Buffer; +}): Buffer { + // params: ad_id_hash(32) + ad_token(32) + amount(16) + order_chain_id(16) + ad_recipient(32) = 128 + const params = Buffer.alloc(128); + hashStringField(opts.adId).copy(params, 0); + opts.adToken.copy(params, 32); + u128BE(opts.amount).copy(params, 64); + u128BE(opts.orderChainId).copy(params, 80); + opts.adRecipient.copy(params, 96); + return hashRequest( + opts.authToken, + opts.timeToExpire, + 'createAd', + params, + opts.chainId, + opts.contractAddress, + ); +} + +export function fundAdRequestHash(opts: { + authToken: Buffer; + timeToExpire: bigint; + adId: string; + amount: bigint; + chainId: bigint; + contractAddress: Buffer; +}): Buffer { + // params: ad_id_hash(32) + amount(16) = 48 + const params = Buffer.alloc(48); + hashStringField(opts.adId).copy(params, 0); + u128BE(opts.amount).copy(params, 32); + return hashRequest( + opts.authToken, + opts.timeToExpire, + 'fundAd', + params, + opts.chainId, + opts.contractAddress, + ); +} + +export function withdrawFromAdRequestHash(opts: { + authToken: Buffer; + timeToExpire: bigint; + adId: string; + amount: bigint; + to: Buffer; // 32 bytes (bytes32-of-account) + chainId: bigint; + contractAddress: Buffer; +}): Buffer { + // params: ad_id_hash(32) + amount(16) + to(32) = 80 + const params = Buffer.alloc(80); + hashStringField(opts.adId).copy(params, 0); + u128BE(opts.amount).copy(params, 32); + opts.to.copy(params, 48); + return hashRequest( + opts.authToken, + opts.timeToExpire, + 'withdrawFromAd', + params, + opts.chainId, + opts.contractAddress, + ); +} + +export function closeAdRequestHash(opts: { + authToken: Buffer; + timeToExpire: bigint; + adId: string; + to: Buffer; // 32 bytes + chainId: bigint; + contractAddress: Buffer; +}): Buffer { + // params: ad_id_hash(32) + to(32) = 64 + const params = Buffer.alloc(64); + hashStringField(opts.adId).copy(params, 0); + opts.to.copy(params, 32); + return hashRequest( + opts.authToken, + opts.timeToExpire, + 'closeAd', + params, + opts.chainId, + opts.contractAddress, + ); +} + +export function lockForOrderRequestHash(opts: { + authToken: Buffer; + timeToExpire: bigint; + adId: string; + orderHash: Buffer; // 32 bytes + chainId: bigint; + contractAddress: Buffer; +}): Buffer { + // params: ad_id_hash(32) + order_hash(32) = 64 + const params = Buffer.alloc(64); + hashStringField(opts.adId).copy(params, 0); + opts.orderHash.copy(params, 32); + return hashRequest( + opts.authToken, + opts.timeToExpire, + 'lockForOrder', + params, + opts.chainId, + opts.contractAddress, + ); +} + +export function createOrderRequestHash(opts: { + authToken: Buffer; + timeToExpire: bigint; + adId: string; + orderHash: Buffer; // 32 bytes + chainId: bigint; + contractAddress: Buffer; +}): Buffer { + const params = Buffer.alloc(64); + hashStringField(opts.adId).copy(params, 0); + opts.orderHash.copy(params, 32); + return hashRequest( + opts.authToken, + opts.timeToExpire, + 'createOrder', + params, + opts.chainId, + opts.contractAddress, + ); +} + +export function unlockOrderRequestHash(opts: { + authToken: Buffer; + timeToExpire: bigint; + adId: string; + orderHash: Buffer; // 32 bytes + targetRoot: Buffer; // 32 bytes + chainId: bigint; + contractAddress: Buffer; +}): Buffer { + // params: ad_id_hash(32) + order_hash(32) + target_root(32) = 96 + const params = Buffer.alloc(96); + hashStringField(opts.adId).copy(params, 0); + opts.orderHash.copy(params, 32); + opts.targetRoot.copy(params, 64); + return hashRequest( + opts.authToken, + opts.timeToExpire, + 'unlockOrder', + params, + opts.chainId, + opts.contractAddress, + ); +} + +// 32-byte random auth token — matches `BytesN<32> auth_token` on Stellar. +export function randomAuthToken(): Buffer { + return randomBytes(32); +} + +// Sign a 32-byte message with ed25519. Returns the 64-byte signature. +export function signEd25519(message: Buffer, seed: Buffer): Buffer { + if (seed.length !== 32) + throw new Error('ed25519 seed must be 32 bytes (raw secret)'); + return Buffer.from(ed25519.sign(message, seed)); +} + +export function ed25519PublicKey(seed: Buffer): Buffer { + if (seed.length !== 32) throw new Error('ed25519 seed must be 32 bytes'); + return Buffer.from(ed25519.getPublicKey(seed)); +} + +export function verifyEd25519( + message: Buffer, + signature: Buffer, + publicKey: Buffer, +): boolean { + return ed25519.verify(signature, message, publicKey); +} diff --git a/apps/backend-relayer/src/providers/stellar/wasm/ad_manager.wasm b/apps/backend-relayer/src/providers/stellar/wasm/ad_manager.wasm new file mode 100644 index 0000000000000000000000000000000000000000..405c68edef42d70c73b28ad162dac8149d49698e GIT binary patch literal 45557 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 literal 0 HcmV?d00001 diff --git a/apps/backend-relayer/src/providers/stellar/wasm/order_portal.wasm b/apps/backend-relayer/src/providers/stellar/wasm/order_portal.wasm new file mode 100644 index 0000000000000000000000000000000000000000..6bf4e49c47cc038519539ba9c6c873a310eb0fb8 GIT binary patch literal 36207 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 WmCc=}F6&x;D2BZm3xR literal 0 HcmV?d00001 diff --git a/apps/backend-relayer/src/providers/stellar/wasm/test_token.wasm b/apps/backend-relayer/src/providers/stellar/wasm/test_token.wasm new file mode 100755 index 0000000000000000000000000000000000000000..83641557108d4beb036dd101d36cc88202b99e33 GIT binary patch literal 67376 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 { const userWallet = Wallet.createRandom(); beforeAll(async () => { - // Secrets for tests (align with your env loader) app = await createTestingApp(); - - // clean tables - await prisma.adLock.deleteMany({}); - await prisma.ad.deleteMany({}); }); afterAll(async () => { @@ -26,25 +21,26 @@ describe('Ads E2E', () => { await prisma.$disconnect(); }); - it('POST /v1/ads requires auth', async () => { + it('POST /v1/ads/create requires auth', async () => { await request(app.getHttpServer()) .post('/v1/ads/create') - .send({ routeId: 'r', poolAmount: '100' }) + .send({ + routeId: randomUUID(), + creatorDstAddress: userWallet.address, + fundAmount: '1000', + }) .expect(403); }); - it('creates an ad, then fetches it', async () => { - // seed chains/tokens/route (same symbol, cross-chain) + it('creates an ad, persists INACTIVE row, then fetches it', async () => { const c1 = await seedChain(prisma); const c2 = await seedChain(prisma); const t1 = await seedToken(prisma, c1.id, 'ETH'); const t2 = await seedToken(prisma, c2.id, 'ETH'); const route = await seedRoute(prisma, t1.id, t2.id); - // login const access = await loginUser(app, userWallet.privateKey as `0x${string}`); - // create const create = await request(app.getHttpServer()) .post('/v1/ads/create') .set('Authorization', `Bearer ${access}`) @@ -52,98 +48,29 @@ describe('Ads E2E', () => { routeId: route.id, creatorDstAddress: userWallet.address, fundAmount: '1000000000000000000', - }) // 1 ETH + }) .expect(201); expect(create.body).toMatchObject({ - id: expect.any(String), - creatorAddress: userWallet.address, - routeId: route.id, - poolAmount: '1000000000000000000', - availableAmount: '1000000000000000000', - status: 'ACTIVE', + adId: expect.any(String), + chainId: expect.any(String), + contractAddress: expect.any(String), + signature: expect.any(String), + authToken: expect.any(String), + timeToExpire: expect.any(Number), + initialAmount: '1000000000000000000', + reqHash: expect.any(String), }); - const adId = create.body.id as string; + const adId = create.body.adId as string; - // get by id const byId = await request(app.getHttpServer()) .get(`/v1/ads/${adId}`) .expect(200); expect(byId.body.id).toBe(adId); - expect(byId.body.availableAmount).toBe('1000000000000000000'); - }); - - it('lists by routeId/creatorAddress filters', async () => { - const base = await seedChain(prisma, { name: 'Base29', chainId: 80454n }); - const eth = await seedChain(prisma, { name: 'Ethereum29', chainId: 25n }); - const tBase = await seedToken(prisma, base.id, 'ETH'); - const tEth = await seedToken(prisma, eth.id, 'ETH'); - const route = await seedRoute(prisma, tBase.id, tEth.id); - - // login - const access = await loginUser(app, userWallet.privateKey as `0x${string}`); - - // create ad - const created = await request(app.getHttpServer()) - .post('/v1/ads') - .set('Authorization', `Bearer ${access}`) - .send({ routeId: route.id, poolAmount: '250' }) - .expect(201); - - // filter by routeId - const byRoute = await request(app.getHttpServer()) - .get('/v1/ads') - .query({ routeId: route.id }) - .expect(200); - expect(byRoute.body.data.map((a: any) => a.id)).toContain(created.body.id); - - // filter by creator - const byCreator = await request(app.getHttpServer()) - .get('/v1/ads') - .query({ creatorAddress: userWallet.address }) - .expect(200); - expect(byCreator.body.data.map((a: any) => a.id)).toContain( - created.body.id, - ); - }); - - it('makes top-up request', async () => { - const c1 = await seedChain(prisma); - const c2 = await seedChain(prisma); - const t1 = await seedToken(prisma, c1.id, 'ETH'); - const t2 = await seedToken(prisma, c2.id, 'ETH'); - const route = await seedRoute(prisma, t1.id, t2.id); - - // login - const access = await loginUser(app, userWallet.privateKey as `0x${string}`); - - const create = await request(app.getHttpServer()) - .post('/v1/ads/create') - .set('Authorization', `Bearer ${access}`) - .send({ routeId: route.id, creatorDstAddress: userWallet.address }) - .expect(201); - - const adId = create.body.id as string; - - // top-up pool - const topup = await request(app.getHttpServer()) - .patch(`/v1/ads/${adId}/fund`) - .set('Authorization', `Bearer ${access}`) - .send({ poolAmountTopUp: '500' }) - .expect(200); - - // returns a topup request - expect(topup.body).toMatchObject({ - contractAddress: expect.any(String), - signature: expect.any(String), - authToken: expect.any(String), - timeToExpire: expect.any(Number), - adId: expect.any(String), - amount: expect.any(String), - reqHash: expect.any(String), - }); + expect(byId.body.status).toBe('INACTIVE'); + expect(byId.body.poolAmount).toBe('0'); }); it('updates minAmount and maxAmount', async () => { @@ -153,18 +80,20 @@ describe('Ads E2E', () => { const t2 = await seedToken(prisma, c2.id, 'ETH'); const route = await seedRoute(prisma, t1.id, t2.id); - // login const access = await loginUser(app, userWallet.privateKey as `0x${string}`); const create = await request(app.getHttpServer()) .post('/v1/ads/create') .set('Authorization', `Bearer ${access}`) - .send({ routeId: route.id, creatorDstAddress: userWallet.address }) + .send({ + routeId: route.id, + creatorDstAddress: userWallet.address, + fundAmount: '1000000000000000000', + }) .expect(201); - const adId = create.body.id as string; + const adId = create.body.adId as string; - // pause const update = await request(app.getHttpServer()) .patch(`/v1/ads/${adId}/update`) .set('Authorization', `Bearer ${access}`) @@ -175,34 +104,38 @@ describe('Ads E2E', () => { expect(update.body.maxAmount).toBe('100000'); }); - it('close (DELETE) marks ad CLOSED and GET shows it', async () => { + it('lists ads by routeId/creatorAddress filters', async () => { const c1 = await seedChain(prisma); const c2 = await seedChain(prisma); const t1 = await seedToken(prisma, c1.id, 'ETH'); const t2 = await seedToken(prisma, c2.id, 'ETH'); const route = await seedRoute(prisma, t1.id, t2.id); - // login const access = await loginUser(app, userWallet.privateKey as `0x${string}`); const create = await request(app.getHttpServer()) - .post('/v1/ads') + .post('/v1/ads/create') .set('Authorization', `Bearer ${access}`) - .send({ routeId: route.id, poolAmount: '1' }) + .send({ + routeId: route.id, + creatorDstAddress: userWallet.address, + fundAmount: '1000000000000000000', + }) .expect(201); - const adId = create.body.id as string; + const adId = create.body.adId as string; - await request(app.getHttpServer()) - .post(`/v1/ads/${adId}/close`) - .set('Authorization', `Bearer ${access}`) - .send({ to: userWallet.address }) + const byRoute = await request(app.getHttpServer()) + .get('/v1/ads') + .query({ routeId: route.id }) .expect(200); + expect(byRoute.body.data.map((a: any) => a.id)).toContain(adId); - const byId = await request(app.getHttpServer()) - .get(`/v1/ads/${adId}`) + const byCreator = await request(app.getHttpServer()) + .get('/v1/ads') + .query({ creatorAddress: userWallet.address }) .expect(200); - expect(byId.body.status).toBe('CLOSED'); + expect(byCreator.body.data.map((a: any) => a.id)).toContain(adId); }); it('404 on unknown ad id', async () => { diff --git a/apps/backend-relayer/test/e2e/auth.e2e-spec.ts b/apps/backend-relayer/test/e2e/auth.e2e-spec.ts index e368121..440ea39 100644 --- a/apps/backend-relayer/test/e2e/auth.e2e-spec.ts +++ b/apps/backend-relayer/test/e2e/auth.e2e-spec.ts @@ -5,6 +5,7 @@ import { createTestingApp } from '../setups/create-app'; import { Wallet } from 'ethers'; import { SiweMessage } from 'siwe'; import { env } from '@libs/configs'; +import { ChainKind } from '@prisma/client'; import { LoginResponseDto } from '../../src/modules/auth/dto/auth.dto'; interface ChallengeResponse { @@ -49,7 +50,7 @@ describe('SIWE E2E', () => { // get nonce bound to address const prep = await request(app.getHttpServer()) .post('/v1/auth/challenge') - .send({ address: wallet.address }) + .send({ address: wallet.address, chainKind: ChainKind.EVM }) .expect(200); const body = prep.body as ChallengeResponse; @@ -77,7 +78,7 @@ describe('SIWE E2E', () => { const verify = await request(app.getHttpServer()) .post('/v1/auth/login') - .send({ message, signature }) + .send({ message, signature, chainKind: ChainKind.EVM }) .expect(201); expect(verify.body.user).toMatchObject({ @@ -93,7 +94,7 @@ describe('SIWE E2E', () => { it('rejects wrong domain', async () => { const prep = await request(app.getHttpServer()) .post('/v1/auth/challenge') - .send({ address: wallet.address }) + .send({ address: wallet.address, chainKind: ChainKind.EVM }) .expect(200); const body = prep.body as ChallengeResponse; @@ -115,14 +116,14 @@ describe('SIWE E2E', () => { await request(app.getHttpServer()) .post('/v1/auth/login') - .send({ message, signature: sig }) + .send({ message, signature: sig, chainKind: ChainKind.EVM }) .expect(400); }); it('rejects expired message', async () => { const prep = await request(app.getHttpServer()) .post('/v1/auth/challenge') - .send({ address: wallet.address }) + .send({ address: wallet.address, chainKind: ChainKind.EVM }) .expect(200); const body = prep.body as ChallengeResponse; @@ -149,14 +150,14 @@ describe('SIWE E2E', () => { await request(app.getHttpServer()) .post('/v1/auth/login') - .send({ message, signature }) + .send({ message, signature, chainKind: ChainKind.EVM }) .expect(400); }); it('rejects invalid signature', async () => { const prep = await request(app.getHttpServer()) .post('/v1/auth/challenge') - .send({ address: wallet.address }) + .send({ address: wallet.address, chainKind: ChainKind.EVM }) .expect(200); const body = prep.body as ChallengeResponse; @@ -178,7 +179,7 @@ describe('SIWE E2E', () => { await request(app.getHttpServer()) .post('/v1/auth/login') - .send({ message, signature: invalidSignature }) + .send({ message, signature: invalidSignature, chainKind: ChainKind.EVM }) .expect(401); }); @@ -200,14 +201,14 @@ describe('SIWE E2E', () => { await request(app.getHttpServer()) .post('/v1/auth/login') - .send({ message, signature }) + .send({ message, signature, chainKind: ChainKind.EVM }) .expect(400); }); it('POST /auth/refresh returns a fresh access token', async () => { const prep = await request(app.getHttpServer()) .post('/v1/auth/challenge') - .send({ address: wallet.address }) + .send({ address: wallet.address, chainKind: ChainKind.EVM }) .expect(200); const body = prep.body as ChallengeResponse; @@ -230,7 +231,7 @@ describe('SIWE E2E', () => { const verifyResponse = await request(app.getHttpServer()) .post('/v1/auth/login') - .send({ message, signature }) + .send({ message, signature, chainKind: ChainKind.EVM }) .expect(201); const { tokens } = verifyResponse.body as LoginResponseDto; diff --git a/apps/backend-relayer/src/app.e2e.spec.ts b/apps/backend-relayer/test/e2e/health.e2e-spec.ts similarity index 92% rename from apps/backend-relayer/src/app.e2e.spec.ts rename to apps/backend-relayer/test/e2e/health.e2e-spec.ts index d63ac23..ff6e81a 100644 --- a/apps/backend-relayer/src/app.e2e.spec.ts +++ b/apps/backend-relayer/test/e2e/health.e2e-spec.ts @@ -1,6 +1,6 @@ import request from 'supertest'; import { INestApplication } from '@nestjs/common'; -import { createTestingApp } from '../test/setups/create-app'; +import { createTestingApp } from '../setups/create-app'; describe('Health E2E', () => { let app: INestApplication; diff --git a/apps/backend-relayer/test/e2e/jest-e2e.json b/apps/backend-relayer/test/e2e/jest-e2e.json index 0067ce0..17248ff 100644 --- a/apps/backend-relayer/test/e2e/jest-e2e.json +++ b/apps/backend-relayer/test/e2e/jest-e2e.json @@ -2,10 +2,12 @@ "moduleFileExtensions": ["js", "json", "ts"], "rootDir": "../", "testEnvironment": "node", + "maxWorkers": 1, "testRegex": ".e2e-spec.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, + "transformIgnorePatterns": ["node_modules/(?!(?:\\.pnpm/)?@noble)"], "setupFilesAfterEnv": [], "globalSetup": "/setups/jest-e2e.setup.ts", "globalTeardown": "/setups/jest.teardown.ts", diff --git a/apps/backend-relayer/test/e2e/trade-e2e-spec.ts b/apps/backend-relayer/test/e2e/trade-e2e-spec.ts index 64dd425..c5902ff 100644 --- a/apps/backend-relayer/test/e2e/trade-e2e-spec.ts +++ b/apps/backend-relayer/test/e2e/trade-e2e-spec.ts @@ -1,90 +1,24 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import request from 'supertest'; import { INestApplication } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; import { createTestingApp } from '../setups/create-app'; -import { seedAd, seedChain, seedRoute, seedToken } from '../setups/utils'; -import { HDNodeWallet, TypedDataDomain, Wallet } from 'ethers'; -import { SiweMessage } from 'siwe'; +import { seedAd, seedChain, seedRoute, seedToken, loginUser } from '../setups/utils'; +import { Wallet } from 'ethers'; +import { randomUUID } from 'crypto'; -interface ChallengeResponse { - nonce: string; - address: string; - expiresAt: string; - domain: string; - uri: string; -} - -describe('Trades E2E ', () => { +describe('Trades E2E', () => { let app: INestApplication; const prisma = new PrismaClient(); - // --- helpers --------------------------------------------------------- - - const loginUser = async (wallet: HDNodeWallet) => { - const challenge = await request(app.getHttpServer()) - .post('/v1/auth/challenge') - .send({ address: wallet.address }) - .expect(200); - - const body = challenge.body as ChallengeResponse; - - const nowIso = new Date().toISOString(); - const expIso = new Date(Date.now() + 5 * 60_000).toISOString(); - - const msg = new SiweMessage({ - domain: body.domain, - address: wallet.address, - statement: 'Sign in to ProofBridge', - uri: body.uri, - version: '1', - chainId: 1, - nonce: body.nonce, - issuedAt: nowIso, - expirationTime: expIso, - }); - - const message = msg.prepareMessage(); - const signature = await wallet.signMessage(message); - - const res = await request(app.getHttpServer()) - .post('/v1/auth/login') - .send({ message, signature }) - .expect(201); - - return res.body.tokens.access as string; - }; - - const buildTypedData = (adChainId: bigint) => - ({ - name: 'ProofBridge', - version: '1', - chainId: Number(adChainId), - verifyingContract: '0x0000000000000000000000000000000000000000', - }) as TypedDataDomain; - - const types = { - TradeIntent: [ - { name: 'routeId', type: 'string' }, - { name: 'adId', type: 'string' }, - { name: 'amount', type: 'uint256' }, - { name: 'adChainId', type: 'uint256' }, - { name: 'orderChainId', type: 'uint256' }, - { name: 'symbol', type: 'string' }, - { name: 'bridger', type: 'address' }, - { name: 'creator', type: 'address' }, - { name: 'idemKey', type: 'string' }, - ], - } as const; - const seedFixture = async () => { - // unique-ish chain ids per test run to avoid collisions const base = await seedChain(prisma, { name: `Base_${Date.now()}`, - chainId: BigInt(800000 + Math.floor(Math.random() * 1000)), + chainId: BigInt(800000 + Math.floor(Math.random() * 1000000)), }); const eth = await seedChain(prisma, { name: `Ethereum_${Date.now()}`, - chainId: BigInt(100 + Math.floor(Math.random() * 1000)), + chainId: BigInt(100 + Math.floor(Math.random() * 100000)), }); const tBase = await seedToken(prisma, base.id, 'ETH'); const tEth = await seedToken(prisma, eth.id, 'ETH'); @@ -92,55 +26,20 @@ describe('Trades E2E ', () => { const creatorWallet = Wallet.createRandom(); const bridgerWallet = Wallet.createRandom(); + const ad = await seedAd( prisma, creatorWallet.address, route.id, tBase.id, tEth.id, - 10_000, + 1_000_000_000, + 'ACTIVE', ); - const access = await loginUser(bridgerWallet); - - const domain = buildTypedData(base.chainId); - const idem = 'idem-' + Math.random().toString(16).slice(2); - const message = { - routeId: route.id, - adId: ad.id, - amount: '1000', - adChainId: base.chainId.toString(), - orderChainId: eth.chainId.toString(), - symbol: 'ETH', - bridger: bridgerWallet.address, - creator: creatorWallet.address, - idemKey: idem, - }; - - const sigCreator = await creatorWallet.signTypedData( - domain, - types as any, - message, - ); - const sigBridger = await bridgerWallet.signTypedData( - domain, - types as any, - message, - ); + const access = await loginUser(app, bridgerWallet.privateKey as `0x${string}`); - return { - base, - eth, - route, - ad, - creatorWallet, - bridgerWallet, - access, - idem, - message, - sigCreator, - sigBridger, - }; + return { base, eth, route, ad, creatorWallet, bridgerWallet, access }; }; beforeAll(async () => { @@ -152,132 +51,133 @@ describe('Trades E2E ', () => { await prisma.$disconnect(); }); - // --- tests ----------------------------------------------------------- + it('POST /v1/trades/create requires auth', async () => { + await request(app.getHttpServer()) + .post('/v1/trades/create') + .send({ + adId: randomUUID(), + routeId: randomUUID(), + amount: '1000', + bridgerDstAddress: Wallet.createRandom().address, + }) + .expect(403); + }); it('creates a trade (happy path)', async () => { const f = await seedFixture(); const res = await request(app.getHttpServer()) - .post('/v1/trades') + .post('/v1/trades/create') .set('Authorization', `Bearer ${f.access}`) - .set('Idempotency-Key', f.idem) .send({ adId: f.ad.id, routeId: f.route.id, - amount: f.message.amount, - adCreatorAddress: f.creatorWallet.address, - bridgerAddress: f.bridgerWallet.address, + amount: '1000', + bridgerDstAddress: f.bridgerWallet.address, }) .expect(201); expect(res.body).toMatchObject({ - idempotentHit: false, - trade: { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - id: expect.any(String), - status: 'CREATED', - amount: '1000', - adId: f.ad.id, - routeId: f.route.id, - adCreatorAddress: f.creatorWallet.address, - bridgerAddress: f.bridgerWallet.address, - participantSignatures: null, - ad: { - id: f.ad.id, - creatorAddress: f.creatorWallet.address, - routeId: f.route.id, - }, - route: { - id: f.route.id, - }, - }, + tradeId: expect.any(String), + reqContractDetails: expect.objectContaining({ + chainId: expect.any(String), + contractAddress: expect.any(String), + signature: expect.any(String), + authToken: expect.any(String), + timeToExpire: expect.any(Number), + reqHash: expect.any(String), + orderHash: expect.any(String), + orderParams: expect.any(Object), + }), }); }); - it('replays with the same Idempotency-Key and returns the same trade', async () => { - const f = await seedFixture(); + it('rejects trade on non-existent ad', async () => { + const bridgerWallet = Wallet.createRandom(); + const access = await loginUser( + app, + bridgerWallet.privateKey as `0x${string}`, + ); - const first = await request(app.getHttpServer()) - .post('/v1/trades') - .set('Authorization', `Bearer ${f.access}`) + await request(app.getHttpServer()) + .post('/v1/trades/create') + .set('Authorization', `Bearer ${access}`) .send({ - adId: f.ad.id, - routeId: f.route.id, - amount: f.message.amount, - adCreatorAddress: f.creatorWallet.address, - bridgerAddress: f.bridgerWallet.address, + adId: randomUUID(), + routeId: randomUUID(), + amount: '1000', + bridgerDstAddress: bridgerWallet.address, }) - .expect(201); + .expect(404); + }); - const tradeId = first.body.id as string; + it('rejects ad creator from bridging own ad', async () => { + const f = await seedFixture(); + const creatorAccess = await loginUser( + app, + f.creatorWallet.privateKey as `0x${string}`, + ); - const replay = await request(app.getHttpServer()) - .post('/v1/trades') - .set('Authorization', `Bearer ${f.access}`) + await request(app.getHttpServer()) + .post('/v1/trades/create') + .set('Authorization', `Bearer ${creatorAccess}`) .send({ adId: f.ad.id, routeId: f.route.id, - amount: f.message.amount, - adCreatorAddress: f.creatorWallet.address, - bridgerAddress: f.bridgerWallet.address, + amount: '1000', + bridgerDstAddress: f.creatorWallet.address, }) - .expect((res) => [200, 201].includes(res.status)); - - expect(replay.body.id).toBe(tradeId); + .expect(400); }); - it('lists trades with filters', async () => { + it('gets a trade by id', async () => { const f = await seedFixture(); const create = await request(app.getHttpServer()) - .post('/v1/trades') + .post('/v1/trades/create') .set('Authorization', `Bearer ${f.access}`) - .set('Idempotency-Key', f.idem) .send({ adId: f.ad.id, routeId: f.route.id, - amount: f.message.amount, - adCreatorAddress: f.creatorWallet.address, - bridgerAddress: f.bridgerWallet.address, + amount: '1000', + bridgerDstAddress: f.bridgerWallet.address, }) .expect(201); - const tradeId = create.body.trade.id as string; + const tradeId = create.body.tradeId as string; - const list = await request(app.getHttpServer()) - .get('/v1/trades') - .query({ - routeId: f.route.id, - adId: f.ad.id, - bridgerAddress: f.bridgerWallet.address, - }) + const byId = await request(app.getHttpServer()) + .get(`/v1/trades/${tradeId}`) .expect(200); - expect(list.body.data.map((t: any) => t.id)).toContain(tradeId); + expect(byId.body.id).toBe(tradeId); }); - it('gets a trade by id', async () => { + it('lists trades with filters', async () => { const f = await seedFixture(); const create = await request(app.getHttpServer()) - .post('/v1/trades') + .post('/v1/trades/create') .set('Authorization', `Bearer ${f.access}`) - .set('Idempotency-Key', f.idem) .send({ adId: f.ad.id, routeId: f.route.id, - amount: f.message.amount, - adCreatorAddress: f.creatorWallet.address, - bridgerAddress: f.bridgerWallet.address, + amount: '1000', + bridgerDstAddress: f.bridgerWallet.address, }) .expect(201); - const tradeId = create.body.trade.id as string; + const tradeId = create.body.tradeId as string; - const byId = await request(app.getHttpServer()) - .get(`/v1/trades/${tradeId}`) + const list = await request(app.getHttpServer()) + .get('/v1/trades/all') + .query({ + routeId: f.route.id, + adId: f.ad.id, + bridgerAddress: f.bridgerWallet.address, + }) .expect(200); - expect(byId.body.id).toBe(tradeId); + expect(list.body.data.map((t: any) => t.id)).toContain(tradeId); }); }); diff --git a/apps/backend-relayer/test/integrations/eth-hedera-e2e-integration.ts b/apps/backend-relayer/test/integrations/eth-hedera-e2e-integration.ts deleted file mode 100644 index 7ec7af0..0000000 --- a/apps/backend-relayer/test/integrations/eth-hedera-e2e-integration.ts +++ /dev/null @@ -1,582 +0,0 @@ -import { INestApplication } from '@nestjs/common'; -import { createTestingApp } from '../setups/create-app'; -import { - fundEthAddress, - fundHBar, - loginUser, - makeEthClient, - makeHederaClient, -} from '../setups/utils'; -import * as ethContracts from '../setups/eth-deployed-contracts.json'; -import * as hederaContracts from '../setups/hedera-deployed-contracts.json'; -import { - getRoutes, - apiCreateAd, - apiConfirm, - apiFundAd, - apiWithdraw, - apiUpdateAd, - apiGetAd, - apiCloseAd, - apiCreateOrder, - apiGetTrade, - apiTradeConfirm, - apiLockOrder, - apiTradeParams, - apiUnlockOrder, - apiTradeUnlockConfirm, -} from './api'; -import { - createAd, - fundAd, - withdrawAdFunds, - approveToken, - mintToken, - closeAd, - createOrder, - lockForOrder, - unlockOrderChain, - unlockAdChain, -} from '../setups/contract-actions'; -import { getAddress, parseEther } from 'viem'; -import { expectObject } from '../setups/utils'; -import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; -import { - AdResponseDto, - CreateAdResponseDto, -} from '../../src/modules/ads/dto/ad.dto'; -import { - CreateOrderRequestContractDetailsDto, - LockForOrderResponseDto, - UnlockOrderResponseDto, -} from '../../src/modules/trades/dto/trade.dto'; -import { - domain, - orderTypes, - signTypedOrder, - verifyTypedData, -} from '../../src/providers/viem/ethers/typedData'; -import { - T_AdManagerOrderParams, - T_OrderParams, - T_OrderPortalParams, -} from '../../src/providers/viem/types'; -import { TypedDataEncoder } from 'ethers'; - -describe('Integrations E2E — (ETH → Hedera)', () => { - let app: INestApplication; - const privateKey1 = generatePrivateKey(); - const account1 = privateKeyToAccount(privateKey1); - const privatekey2 = generatePrivateKey(); - const account2 = privateKeyToAccount(privatekey2); - const ethChain = { - ...ethContracts, - adManagerAddress: ethContracts.adManagerAddress as `0x${string}`, - orderPortalAddress: ethContracts.orderPortalAddress as `0x${string}`, - tokenAddress: ethContracts.tokenAddress as `0x${string}`, - }; - - const hederaChain = { - ...hederaContracts, - adManagerAddress: hederaContracts.adManagerAddress as `0x${string}`, - orderPortalAddress: hederaContracts.orderPortalAddress as `0x${string}`, - tokenAddress: hederaContracts.tokenAddress as `0x${string}`, - }; - - const ethClient = makeEthClient(); - const hederaClient = makeHederaClient(); - - let route: AdResponseDto; - - beforeAll(async () => { - app = await createTestingApp(); - await fundEthAddress(ethClient, account1.address); - await fundHBar(hederaClient, account1.address); - await fundHBar(hederaClient, account2.address); - - // Fetch available routes between chains - const routes = await getRoutes( - app, - ethContracts.chainId.toString(), - hederaContracts.chainId.toString(), - ).expect(200); - - expect(routes.body.data.length).toBeGreaterThan(0); - - route = routes.body.data[0] as AdResponseDto; - }, 60_000); - - afterAll(async () => { - await app.close(); - }); - - it('Ad lifecycle', async () => { - // Login and get access token - const access = await loginUser(app, privateKey1); - - await mintToken( - ethClient, - account1, - ethChain.tokenAddress, - account1.address, - parseEther('100'), - ); - - await approveToken( - ethClient, - account1, - ethChain.tokenAddress, - ethChain.adManagerAddress, - parseEther('5'), - ); - - // Step 1: Create a new advertisement - const create = await apiCreateAd( - app, - access, - route.id, - account1.address, - parseEther('5').toString(), - ).expect(201); - - const req = create.body as CreateAdResponseDto; - console.log(req); - console.log(create); - const adId = req.adId; - - expect(ethChain.adManagerAddress).toEqual(req.contractAddress); - - // Create advertisement on blockchain - const txCreate = await createAd( - ethClient, - account1, - req.signature, - req.authToken as `0x${string}`, - req.timeToExpire, - req.adId, - req.adToken, - req.initialAmount, - req.orderChainId, - req.adRecipient, - req.contractAddress, - ); - await apiConfirm(app, adId, access, txCreate).expect(200); - - // Verify ad status after creation - const adAfterCreate = await apiGetAd(app, adId).expect(200); - - expectObject(adAfterCreate.body, { - id: adId, - status: 'ACTIVE', - poolAmount: parseEther('5').toString(), - }); - - // Step 2: Fund the advertisement - const topup = await apiFundAd(app, adId, access, '10').expect(200); - expect(ethChain.adManagerAddress).toEqual(topup.body.contractAddress); - - // Mint and approve tokens for funding - await mintToken( - ethClient, - account1, - ethChain.tokenAddress, - account1.address, - parseEther('100'), - ); - await approveToken( - ethClient, - account1, - ethChain.tokenAddress, - ethChain.adManagerAddress, - parseEther('10'), - ); - - // Fund ad on blockchain - const txFund = await fundAd( - ethClient, - account1, - topup.body.signature, - topup.body.authToken, - topup.body.timeToExpire, - topup.body.adId, - BigInt(topup.body.amount), - topup.body.contractAddress, - ); - await apiConfirm(app, adId, access, txFund).expect(200); - - // Verify ad is active with correct pool amount - const activeAd = await apiGetAd(app, adId).expect(200); - - expectObject(activeAd.body, { - poolAmount: parseEther('15').toString(), - status: 'ACTIVE', - }); - - // Step 3: Withdraw funds from advertisement - const withdraw = await apiWithdraw( - app, - adId, - access, - '3', - account1.address, - ).expect(200); - - // Execute withdrawal on blockchain - const txW = await withdrawAdFunds( - ethClient, - account1, - withdraw.body.signature, - withdraw.body.authToken, - withdraw.body.timeToExpire, - withdraw.body.adId, - BigInt(withdraw.body.amount), - withdraw.body.to, - withdraw.body.contractAddress, - ); - await apiConfirm(app, adId, access, txW).expect(200); - - // Verify remaining pool amount after withdrawal - const afterWithdraw = await apiGetAd(app, adId).expect(200); - - expectObject(afterWithdraw.body, { - poolAmount: parseEther('12').toString(), - status: 'ACTIVE', - }); - - // Step 4: Update advertisement parameters - await apiUpdateAd(app, adId, access, { - status: 'PAUSED', - minAmount: parseEther('0.05').toString(), - maxAmount: parseEther('1').toString(), - metadata: { test: 'data' }, - }).expect(200); - - // Verify updated parameters - const afterUpdate = await apiGetAd(app, adId).expect(200); - - expectObject(afterUpdate.body, { - status: 'PAUSED', - minAmount: parseEther('0.05').toString(), - maxAmount: parseEther('1').toString(), - metadata: { test: 'data' }, - }); - - // Reactivate the advertisement - await apiUpdateAd(app, adId, access, { status: 'ACTIVE' }).expect(200); - - // Step 5: Close advertisement - const close = await apiCloseAd(app, adId, access, { - to: account1.address, - }).expect(200); - - // Execute closure on blockchain - const txClose = await closeAd( - ethClient, - account1, - close.body.signature, - close.body.authToken, - close.body.timeToExpire, - close.body.adId, - close.body.to, - close.body.contractAddress, - ); - - await apiConfirm(app, adId, access, txClose).expect(200); - - // Verify final state - const finalAd = await apiGetAd(app, adId); - - expectObject(finalAd.body, { status: 'CLOSED', poolAmount: '0' }); - }, 300_000); - - it('Trade lifecycle', async () => { - const access = await loginUser(app, privateKey1); - - await mintToken( - ethClient, - account1, - ethChain.tokenAddress, - account1.address, - parseEther('100'), - ); - - await approveToken( - ethClient, - account1, - ethChain.tokenAddress, - ethChain.adManagerAddress, - parseEther('5'), - ); - - // Step 1: Create a new advertisement - const create = await apiCreateAd( - app, - access, - route.id, - account1.address, - parseEther('5').toString(), - ).expect(201); - - const req = create.body as CreateAdResponseDto; - const adId = req.adId; - - expect(ethChain.adManagerAddress).toEqual(req.contractAddress); - expect(ethChain.chainId).toEqual(req.chainId); - - // Create advertisement on blockchain - const txCreate = await createAd( - ethClient, - account1, - req.signature, - req.authToken as `0x${string}`, - req.timeToExpire, - req.adId, - req.adToken, - req.initialAmount, - req.orderChainId, - req.adRecipient, - req.contractAddress, - ); - await apiConfirm(app, adId, access, txCreate).expect(200); - - // Step 2: Fund the advertisement - const topup = await apiFundAd(app, adId, access, '50').expect(200); - expect(ethChain.adManagerAddress).toEqual(topup.body.contractAddress); - expect(ethChain.chainId).toEqual(topup.body.chainId); - - // Mint and approve tokens for funding - await mintToken( - ethClient, - account1, - ethChain.tokenAddress, - account1.address, - parseEther('100'), - ); - await approveToken( - ethClient, - account1, - ethChain.tokenAddress, - ethChain.adManagerAddress, - parseEther('50'), - ); - - // Fund ad on blockchain - const txFund = await fundAd( - ethClient, - account1, - topup.body.signature, - topup.body.authToken, - topup.body.timeToExpire, - topup.body.adId, - BigInt(topup.body.amount), - topup.body.contractAddress, - ); - await apiConfirm(app, adId, access, txFund).expect(200); - - // Step2 create order on orderchain - const access2 = await loginUser(app, privatekey2); - - const order = await apiCreateOrder(app, access2, { - adId: adId, - routeId: route.id, - amount: parseEther('20').toString(), - bridgerDstAddress: account2.address, - }).expect(201); - - const orderReq = order.body - .reqContractDetails as CreateOrderRequestContractDetailsDto; - - const tradeId = order.body.tradeId as string; - - expect(hederaChain.orderPortalAddress).toEqual(orderReq.contractAddress); - expect(hederaChain.chainId).toEqual(orderReq.chainId); - const afterCreateOrder = await apiGetTrade(app, tradeId).expect(200); - - expectObject(afterCreateOrder.body, { - adId: adId, - routeId: route.id, - amount: parseEther('20').toString(), - bridgerAddress: getAddress(account2.address), - adCreatorAddress: getAddress(account1.address), - status: 'INACTIVE', - }); - - // Mint and approve tokens for funding - await mintToken( - hederaClient, - account2, - hederaChain.tokenAddress, - account2.address, - parseEther('100'), - ); - - await approveToken( - hederaClient, - account2, - hederaChain.tokenAddress, - hederaChain.orderPortalAddress, - parseEther('25'), - ); - - const orderCreateTx = await createOrder( - hederaClient, - account2, - orderReq.signature, - orderReq.authToken as `0x${string}`, - orderReq.timeToExpire, - orderReq.orderParams as T_OrderPortalParams, - hederaChain.orderPortalAddress, - ); - - await apiTradeConfirm(app, tradeId, access2, orderCreateTx).expect(200); - - const afterChainDeposit = await apiGetTrade(app, tradeId).expect(200); - expectObject(afterChainDeposit.body, { - id: tradeId, - adId: adId, - status: 'ACTIVE', - }); - - // Lock Order on Ad chain - const lockOrder = await apiLockOrder(app, access, tradeId).expect(200); - - const lockOrderReq = lockOrder.body as LockForOrderResponseDto; - - expect(getAddress(ethChain.adManagerAddress)).toEqual( - getAddress(lockOrderReq.contractAddress), - ); - - expect(ethChain.chainId).toEqual(lockOrderReq.chainId); - - // check that it still remains active - const lockOrderBefore = await apiGetTrade(app, tradeId); - - expectObject(lockOrderBefore.body, { - status: 'ACTIVE', - }); - - const lockTxn = await lockForOrder( - ethClient, - account1, - lockOrderReq.signature as `0x${string}`, - lockOrderReq.authToken as `0x${string}`, - lockOrderReq.timeToExpire, - lockOrderReq.orderParams as T_AdManagerOrderParams, - getAddress(lockOrderReq.contractAddress), - ); - - await apiTradeConfirm(app, tradeId, access, lockTxn).expect(200); - - const lockOrderAfter = await apiGetTrade(app, tradeId); - - expectObject(lockOrderAfter.body, { - status: 'LOCKED', - }); - - /// AD MANAGER UNLOCKKKKKKK /// - - const orderParamsResponse = await apiTradeParams( - app, - access, - tradeId, - ).expect(200); - - const orderParams = orderParamsResponse.body as T_OrderParams; - - const signature = await signTypedOrder(privateKey1, orderParams); - const orderHash = TypedDataEncoder.hash(domain, orderTypes, orderParams); - const isValid = verifyTypedData( - orderHash as `0x${string}`, - signature as `0x${string}`, - account1.address, - ); - - expect(isValid).toBe(true); - - const unlock = await apiUnlockOrder(app, access, tradeId, signature); - - const unlockReq = unlock.body as UnlockOrderResponseDto; - - expect(unlockReq.contractAddress).toEqual(hederaChain.orderPortalAddress); - expect(unlockReq.chainId).toEqual(hederaChain.chainId); - - const unlockOrderChainTx = await unlockOrderChain( - hederaClient, - account1, - unlockReq.signature, - unlockReq.authToken as `0x${string}`, - unlockReq.timeToExpire, - unlockReq.orderParams as T_OrderPortalParams, - unlockReq.nullifierHash as `0x${string}`, - unlockReq.targetRoot as `0x${string}`, - unlockReq.proof as `0x${string}`, - unlockReq.contractAddress, - ); - - await apiTradeUnlockConfirm( - app, - access, - tradeId, - unlockOrderChainTx, - ).expect(200); - - /// BRIDGER MANAGER UNLOCKKKKKKK /// - - const orderParamsResponseBridger = await apiTradeParams( - app, - access2, - tradeId, - ).expect(200); - - const orderParamsBridger = orderParamsResponseBridger.body as T_OrderParams; - - const signatureBridger = await signTypedOrder( - privatekey2, - orderParamsBridger, - ); - const orderHashBridger = TypedDataEncoder.hash( - domain, - orderTypes, - orderParamsBridger, - ); - const isValidBridger = verifyTypedData( - orderHashBridger as `0x${string}`, - signatureBridger as `0x${string}`, - account2.address, - ); - - expect(isValidBridger).toBe(true); - - const unlockBridger = await apiUnlockOrder( - app, - access2, - tradeId, - signatureBridger, - ).expect(200); - - const unlockReqBridger = unlockBridger.body as UnlockOrderResponseDto; - - expect(unlockReqBridger.contractAddress).toEqual(ethChain.adManagerAddress); - expect(unlockReqBridger.chainId).toEqual(ethChain.chainId); - - const unlockOrderChainTxBridger = await unlockAdChain( - ethClient, - account1, - unlockReqBridger.signature, - unlockReqBridger.authToken as `0x${string}`, - unlockReqBridger.timeToExpire, - unlockReqBridger.orderParams as T_AdManagerOrderParams, - unlockReqBridger.nullifierHash as `0x${string}`, - unlockReqBridger.targetRoot as `0x${string}`, - unlockReqBridger.proof as `0x${string}`, - unlockReqBridger.contractAddress, - ); - - await apiTradeUnlockConfirm( - app, - access2, - tradeId, - unlockOrderChainTxBridger, - ).expect(200); - }, 300_000); -}); diff --git a/apps/backend-relayer/test/integrations/eth-stellar.e2e-integration.ts b/apps/backend-relayer/test/integrations/eth-stellar.e2e-integration.ts new file mode 100644 index 0000000..1f90239 --- /dev/null +++ b/apps/backend-relayer/test/integrations/eth-stellar.e2e-integration.ts @@ -0,0 +1,404 @@ +import { INestApplication } from '@nestjs/common'; +import { Keypair, StrKey } from '@stellar/stellar-sdk'; +import { createTestingApp } from '../setups/create-app'; +import { + fundEthAddress, + loginStellarUser, + loginUser, + makeEthClient, +} from '../setups/utils'; +import * as ethContracts from '../setups/evm-deployed-contracts.json'; +import { + getRoutes, + apiCreateAd, + apiConfirm, + apiFundAd, + apiWithdraw, + apiGetAd, + apiCloseAd, + apiCreateOrder, + apiGetTrade, + apiTradeConfirm, + apiLockOrder, + apiTradeParams, + apiUnlockOrder, + apiTradeUnlockConfirm, +} from './api'; +import { + createOrder, + unlockOrderChain, + mintToken, + approveToken, +} from '../setups/evm-actions'; +import { + createAdSoroban, + fundAdSoroban, + withdrawFromAdSoroban, + closeAdSoroban, + lockForOrderSoroban, + unlockSoroban, + StellarOrderParams, +} from '../setups/stellar-actions'; +import type { StellarChainData } from '../setups/stellar-setup'; +import { getAddress, parseEther } from 'viem'; +import { expectObject } from '../setups/utils'; +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; +import { + AdResponseDto, + CreateAdResponseDto, +} from '../../src/modules/ads/dto/ad.dto'; +import { + CreateOrderRequestContractDetailsDto, + LockForOrderResponseDto, + UnlockOrderResponseDto, +} from '../../src/modules/trades/dto/trade.dto'; +import { + domain, + orderTypes, + signTypedOrder, + verifyTypedData, +} from '../../src/providers/viem/ethers/typedData'; +import { + T_OrderParams, + T_OrderPortalParams, +} from '../../src/chain-adapters/types'; +import { TypedDataEncoder } from 'ethers'; + +// Gate the entire suite on the external orchestrator having provisioned +// a Stellar localnet + keypairs. When running `jest` standalone without the +// bash runner, the global will be unset and we skip cleanly. +const stellarContracts = (global as any).__STELLAR_CONTRACTS__ as + | StellarChainData + | undefined; +const describeIfStellar = stellarContracts ? describe : describe.skip; + +describeIfStellar('Integrations E2E — (Stellar → ETH)', () => { + let app: INestApplication; + + // EVM-side bridger (creates the order on the EVM order chain). + const bridgerKey = generatePrivateKey(); + const bridger = privateKeyToAccount(bridgerKey); + + // Ad creator is a Stellar account. The orchestrator passes the secret via + // STELLAR_AD_CREATOR_SECRET; fall back to the deploy admin as a last resort. + const adCreatorSecret = + process.env.STELLAR_AD_CREATOR_SECRET || + (stellarContracts?.adminSecret as string); + const adCreator = adCreatorSecret + ? Keypair.fromSecret(adCreatorSecret) + : Keypair.random(); + // EVM destination for the ad creator's proceeds on unlock. + const adCreatorEvmKey = generatePrivateKey(); + const adCreatorEvm = privateKeyToAccount(adCreatorEvmKey); + + const ethChain = { + ...ethContracts, + adManagerAddress: ethContracts.adManagerAddress as `0x${string}`, + orderPortalAddress: ethContracts.orderPortalAddress as `0x${string}`, + tokenAddress: ethContracts.tokenAddress as `0x${string}`, + }; + + const ethClient = makeEthClient(); + + let route: AdResponseDto; + + beforeAll(async () => { + app = await createTestingApp(); + await fundEthAddress(ethClient, bridger.address); + await fundEthAddress(ethClient, adCreatorEvm.address); + + // Route: Stellar ad token → EVM order token. + const routes = await getRoutes( + app, + stellarContracts!.chainId, + ethChain.chainId.toString(), + ).expect(200); + expect(routes.body.data.length).toBeGreaterThan(0); + route = routes.body.data[0] as AdResponseDto; + }, 120_000); + + afterAll(async () => { + await app.close(); + }); + + it('Ad lifecycle', async () => { + const access = await loginStellarUser(app, adCreator); + + // Create ad — Stellar uses 7-decimal XLM; use a modest amount in stroops. + const INITIAL = '500000000'; // 50 XLM + const create = await apiCreateAd( + app, + access, + route.id, + adCreatorEvm.address, + INITIAL, + ).expect(201); + + const req = create.body as CreateAdResponseDto; + const adId = req.adId; + + const txCreate = await createAdSoroban( + adCreator, + req.signature, + (req as any).signer, + req.authToken, + req.timeToExpire, + adCreator.publicKey(), + req.adId, + req.adToken, + req.initialAmount, + req.orderChainId, + req.adRecipient, + req.contractAddress, + ); + await apiConfirm(app, adId, access, txCreate as `0x${string}`).expect(200); + + const adAfterCreate = await apiGetAd(app, adId).expect(200); + expectObject(adAfterCreate.body, { + id: adId, + status: 'ACTIVE', + poolAmount: INITIAL, + }); + + // Fund. + const topup = await apiFundAd(app, adId, access, '5').expect(200); + const txFund = await fundAdSoroban( + adCreator, + topup.body.signature, + topup.body.signer, + topup.body.authToken, + topup.body.timeToExpire, + topup.body.adId, + topup.body.amount, + topup.body.contractAddress, + ); + await apiConfirm(app, adId, access, txFund as `0x${string}`).expect(200); + + // Withdraw — destination is the ad creator's Stellar account. + const withdraw = await apiWithdraw( + app, + adId, + access, + '1', + adCreator.publicKey() as `0x${string}`, + ).expect(200); + const txW = await withdrawFromAdSoroban( + adCreator, + withdraw.body.signature, + withdraw.body.signer, + withdraw.body.authToken, + withdraw.body.timeToExpire, + withdraw.body.adId, + withdraw.body.amount, + StrKey.isValidEd25519PublicKey(withdraw.body.to) + ? withdraw.body.to + : adCreator.publicKey(), + withdraw.body.contractAddress, + ); + await apiConfirm(app, adId, access, txW as `0x${string}`).expect(200); + + // Close. + const close = await apiCloseAd(app, adId, access, { + to: adCreator.publicKey(), + }).expect(200); + const txClose = await closeAdSoroban( + adCreator, + close.body.signature, + close.body.signer, + close.body.authToken, + close.body.timeToExpire, + close.body.adId, + StrKey.isValidEd25519PublicKey(close.body.to) + ? close.body.to + : adCreator.publicKey(), + close.body.contractAddress, + ); + await apiConfirm(app, adId, access, txClose as `0x${string}`).expect(200); + + const finalAd = await apiGetAd(app, adId); + expectObject(finalAd.body, { status: 'CLOSED', poolAmount: '0' }); + }, 600_000); + + it('Trade lifecycle', async () => { + const adAccess = await loginStellarUser(app, adCreator); + + // Seed the ad. + const INITIAL = '500000000'; // 50 XLM + const create = await apiCreateAd( + app, + adAccess, + route.id, + adCreatorEvm.address, + INITIAL, + ).expect(201); + const req = create.body as CreateAdResponseDto; + const adId = req.adId; + + const txCreate = await createAdSoroban( + adCreator, + req.signature, + (req as any).signer, + req.authToken, + req.timeToExpire, + adCreator.publicKey(), + req.adId, + req.adToken, + req.initialAmount, + req.orderChainId, + req.adRecipient, + req.contractAddress, + ); + await apiConfirm(app, adId, adAccess, txCreate as `0x${string}`).expect(200); + + // Bridger creates the order on the EVM side. + const bridgerAccess = await loginUser(app, bridgerKey); + + // Bridger's destination on the ad chain is a Stellar account (the ad creator here). + const order = await apiCreateOrder(app, bridgerAccess, { + adId, + routeId: route.id, + amount: '100000000', // 10 XLM worth + bridgerDstAddress: adCreator.publicKey(), + }).expect(201); + + const orderReq = order.body + .reqContractDetails as CreateOrderRequestContractDetailsDto; + const tradeId = order.body.tradeId as string; + + expect(getAddress(ethChain.orderPortalAddress)).toEqual( + getAddress(orderReq.contractAddress), + ); + + await mintToken( + ethClient, + bridger, + ethChain.tokenAddress, + bridger.address, + parseEther('1000'), + ); + await approveToken( + ethClient, + bridger, + ethChain.tokenAddress, + ethChain.orderPortalAddress, + parseEther('100'), + ); + + const orderCreateTx = await createOrder( + ethClient, + bridger, + orderReq.signature, + orderReq.authToken as `0x${string}`, + orderReq.timeToExpire, + orderReq.orderParams as T_OrderPortalParams, + ethChain.orderPortalAddress, + ); + await apiTradeConfirm(app, tradeId, bridgerAccess, orderCreateTx).expect( + 200, + ); + + // Lock on the Stellar ad chain — signed by the ad creator (maker). + const lockOrder = await apiLockOrder(app, adAccess, tradeId).expect(200); + const lockReq = lockOrder.body as LockForOrderResponseDto; + + const lockTxn = await lockForOrderSoroban( + adCreator, + lockReq.signature as `0x${string}`, + (lockReq as any).signer, + lockReq.authToken as `0x${string}`, + lockReq.timeToExpire, + lockReq.orderParams as unknown as StellarOrderParams, + lockReq.contractAddress, + ); + await apiTradeConfirm(app, tradeId, adAccess, lockTxn as `0x${string}`).expect( + 200, + ); + + const afterLock = await apiGetTrade(app, tradeId); + expectObject(afterLock.body, { status: 'LOCKED' }); + + // Ad-creator unlocks on the EVM order chain. + const adCreatorParams = await apiTradeParams(app, adAccess, tradeId).expect( + 200, + ); + const adCreatorOrderParams = adCreatorParams.body as T_OrderParams; + // Ad-creator signs with their EVM destination key so the order chain + // recognises the signature. + const adCreatorSig = await signTypedOrder( + adCreatorEvmKey, + adCreatorOrderParams, + ); + const adCreatorHash = TypedDataEncoder.hash( + domain, + orderTypes, + adCreatorOrderParams, + ); + expect( + verifyTypedData( + adCreatorHash as `0x${string}`, + adCreatorSig as `0x${string}`, + adCreatorEvm.address, + ), + ).toBe(true); + + const unlockOnOrder = await apiUnlockOrder( + app, + adAccess, + tradeId, + adCreatorSig, + ).expect(200); + const unlockOrderReq = unlockOnOrder.body as UnlockOrderResponseDto; + + const unlockOrderTx = await unlockOrderChain( + ethClient, + adCreatorEvm, + unlockOrderReq.signature, + unlockOrderReq.authToken as `0x${string}`, + unlockOrderReq.timeToExpire, + unlockOrderReq.orderParams as T_OrderPortalParams, + unlockOrderReq.nullifierHash as `0x${string}`, + unlockOrderReq.targetRoot as `0x${string}`, + unlockOrderReq.proof as `0x${string}`, + unlockOrderReq.contractAddress, + ); + await apiTradeUnlockConfirm(app, adAccess, tradeId, unlockOrderTx).expect( + 200, + ); + + // Bridger unlocks on the Stellar ad chain. + const bridgerParams = await apiTradeParams( + app, + bridgerAccess, + tradeId, + ).expect(200); + const bridgerOrderParams = bridgerParams.body as T_OrderParams; + const bridgerSig = await signTypedOrder(bridgerKey, bridgerOrderParams); + + const unlockOnAd = await apiUnlockOrder( + app, + bridgerAccess, + tradeId, + bridgerSig, + ).expect(200); + const unlockAdReq = unlockOnAd.body as UnlockOrderResponseDto; + + const unlockAdTx = await unlockSoroban( + adCreator, + unlockAdReq.signature as `0x${string}`, + (unlockAdReq as any).signer, + unlockAdReq.authToken as `0x${string}`, + unlockAdReq.timeToExpire, + unlockAdReq.orderParams as unknown as StellarOrderParams, + unlockAdReq.nullifierHash as `0x${string}`, + unlockAdReq.targetRoot as `0x${string}`, + Buffer.from((unlockAdReq.proof as string).replace(/^0x/, ''), 'hex'), + unlockAdReq.contractAddress, + ); + await apiTradeUnlockConfirm( + app, + bridgerAccess, + tradeId, + unlockAdTx as `0x${string}`, + ).expect(200); + }, 600_000); +}); diff --git a/apps/backend-relayer/test/integrations/jest-e2e.json b/apps/backend-relayer/test/integrations/jest-e2e.json index 436d646..cfdaaf8 100644 --- a/apps/backend-relayer/test/integrations/jest-e2e.json +++ b/apps/backend-relayer/test/integrations/jest-e2e.json @@ -6,6 +6,7 @@ "transform": { "^.+\\.(t|j)s$": "ts-jest" }, + "transformIgnorePatterns": ["node_modules/(?!(?:\\.pnpm/)?@noble)"], "setupFilesAfterEnv": [], "globalSetup": "/setups/jest-integrations.setup.ts", "globalTeardown": "/setups/jest.teardown.ts", diff --git a/apps/backend-relayer/test/setups/create-app.ts b/apps/backend-relayer/test/setups/create-app.ts index ab7cd91..2506adc 100644 --- a/apps/backend-relayer/test/setups/create-app.ts +++ b/apps/backend-relayer/test/setups/create-app.ts @@ -1,11 +1,18 @@ import { INestApplication, ValidationPipe } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { AppModule } from '../../src/app.module'; +import { ChainAdapterService } from '../../src/chain-adapters/chain-adapter.service'; +import { MockChainAdapter } from './mock-chain-adapter'; export async function createTestingApp(): Promise { + const mockAdapter = new MockChainAdapter(); + const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], - }).compile(); + }) + .overrideProvider(ChainAdapterService) + .useValue({ forChain: () => mockAdapter }) + .compile(); const app = moduleFixture.createNestApplication(); app.useGlobalPipes( diff --git a/apps/backend-relayer/test/setups/contract-actions.ts b/apps/backend-relayer/test/setups/evm-actions.ts similarity index 99% rename from apps/backend-relayer/test/setups/contract-actions.ts rename to apps/backend-relayer/test/setups/evm-actions.ts index 925884f..cd98cb1 100644 --- a/apps/backend-relayer/test/setups/contract-actions.ts +++ b/apps/backend-relayer/test/setups/evm-actions.ts @@ -9,7 +9,7 @@ import { ChainData, AddressLike } from './utils'; import { T_AdManagerOrderParams, T_OrderPortalParams, -} from '../../src/providers/viem/types'; +} from '../../src/chain-adapters/types'; import { createWalletClient, getAddress, diff --git a/apps/backend-relayer/test/setups/eth-deployed-contracts.json b/apps/backend-relayer/test/setups/evm-deployed-contracts.json similarity index 100% rename from apps/backend-relayer/test/setups/eth-deployed-contracts.json rename to apps/backend-relayer/test/setups/evm-deployed-contracts.json diff --git a/apps/backend-relayer/test/setups/contract-setup.ts b/apps/backend-relayer/test/setups/evm-setup.ts similarity index 68% rename from apps/backend-relayer/test/setups/contract-setup.ts rename to apps/backend-relayer/test/setups/evm-setup.ts index e745b8d..d75d927 100644 --- a/apps/backend-relayer/test/setups/contract-setup.ts +++ b/apps/backend-relayer/test/setups/evm-setup.ts @@ -1,7 +1,6 @@ import { writeFileSync } from 'fs'; import path from 'path'; import dotenv from 'dotenv'; -import { hederaTestnet as hederaLocalnet } from 'viem/chains'; import MerkleManagerArtifact from '../../../../contracts/evm/out/MerkleManager.sol/MerkleManager.json'; import VerifierArtifact from '../../../../contracts/evm/out/Verifier.sol/HonkVerifier.json'; @@ -10,25 +9,21 @@ import OrderPortalArtifact from '../../../../contracts/evm/out/OrderPortal.sol/O import Erc20MockArtifact from '../../../../contracts/evm/out/ERC20Mock.sol/ERC20Mock.json'; import { createPublicClient, createWalletClient, http } from 'viem'; -import { ethLocalnet } from '../../src/providers/viem/localnet'; +import { ethLocalnet } from '../../src/providers/viem/ethers/localnet'; import { AddressLike, ChainData, fundEthAddress } from './utils'; -import { adminSetup } from './contract-actions'; import { privateKeyToAccount } from 'viem/accounts'; dotenv.config({ path: path.resolve(__dirname, '../../.env.test') }); -export async function deployContracts(isEth = true): Promise { - // Log which network we're deploying to - console.log(`Deploying ${isEth ? 'ETH' : 'HEDERA'} contracts...`); +export async function deployEvmContracts(): Promise { + console.log(`Deploying ETH contracts...`); - // Validate environment and setup chain configuration const managerKey = process.env.MANAGER_KEY; if (!managerKey) { throw new Error('MANAGER_KEY not set in environment'); } - const chain = isEth ? ethLocalnet : hederaLocalnet; + const chain = ethLocalnet; - // Initialize clients const publicClient = createPublicClient({ chain, transport: http(), @@ -42,10 +37,7 @@ export async function deployContracts(isEth = true): Promise { const managerAddress = wallet.account.address; - // Fund manager account if on Ethereum network - if (isEth) { - await fundEthAddress(publicClient, managerAddress, '1'); - } + await fundEthAddress(publicClient, managerAddress, '1'); console.log('Using manager address:', wallet.account.address); // Deploy mock ERC20 token @@ -106,12 +98,11 @@ export async function deployContracts(isEth = true): Promise { const orderPortalAddress = orderReceipt.contractAddress!; console.log('OrderPortal deployed to:', orderPortalAddress); - // Create contracts data object const contracts: ChainData = { adManagerAddress, orderPortalAddress, chainId: chain.id.toString(), - name: isEth ? 'ETH LOCALNET' : 'HEDERA LOCALNET', + name: 'ETH LOCALNET', tokenName: 'ERC20Mock', tokenSymbol: 'E20M', tokenAddress: erc20Address, @@ -119,50 +110,9 @@ export async function deployContracts(isEth = true): Promise { verifierAddress, }; - // Save contract addresses to file - const filePath = path.join( - __dirname, - isEth ? 'eth-deployed-contracts.json' : 'hedera-deployed-contracts.json', - ); + const filePath = path.join(__dirname, 'evm-deployed-contracts.json'); writeFileSync(filePath, JSON.stringify(contracts, null, 2)); console.log('Contract addresses saved to:', filePath); return contracts; } - -export async function setupContract( - chain1: ChainData, - chain2: ChainData, - isEth = true, -) { - const managerKey = process.env.MANAGER_KEY; - - if (!managerKey) { - throw new Error('MANAGER_KEY not set in environment'); - } - const chain = isEth ? ethLocalnet : hederaLocalnet; - - const publicClient = createPublicClient({ - chain, - transport: http(), - }); - - const account = privateKeyToAccount(managerKey as AddressLike); - - await adminSetup(publicClient, account, chain1, chain2); -} - -export async function setupContracts() { - const ethContracts = await deployContracts(); - const hederaContracts = await deployContracts(false); - - console.log('Setting up ETH contracts...'); - - await setupContract(ethContracts, hederaContracts); - - console.log('Setting up HEDERA contracts...'); - - await setupContract(hederaContracts, ethContracts, false); - - return { ethContracts, hederaContracts }; -} diff --git a/apps/backend-relayer/test/setups/hedera-deployed-contracts.json b/apps/backend-relayer/test/setups/hedera-deployed-contracts.json deleted file mode 100644 index 553d160..0000000 --- a/apps/backend-relayer/test/setups/hedera-deployed-contracts.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "adManagerAddress": "0x4e20100f3D46dDFfDD754056b1246ee839EFE95e", - "orderPortalAddress": "0xF3a91cA73b1F85D32585CBa052c3c11119a909E7", - "chainId": "296", - "name": "HEDERA TESTNET", - "tokenName": "ProofBridge", - "tokenSymbol": "PBT", - "tokenAddress": "0x9318E8f8C1F7Bff8c00A062F80b391866fBE8d87", - "merkleManagerAddress": "0xDc64ca5aaDe3a1e50e430D601Dd48AB393C079dB", - "verifierAddress": "0xDbf6dA1aBD40f7b7eA7B935663D4F78325930e48" -} diff --git a/apps/backend-relayer/test/setups/jest-e2e.setup.ts b/apps/backend-relayer/test/setups/jest-e2e.setup.ts index d183869..fc80733 100644 --- a/apps/backend-relayer/test/setups/jest-e2e.setup.ts +++ b/apps/backend-relayer/test/setups/jest-e2e.setup.ts @@ -4,6 +4,7 @@ import { } from '@testcontainers/postgresql'; import * as dotenv from 'dotenv'; import { execa } from 'execa'; +import { rmSync } from 'fs'; import path from 'path'; import { seedDBe2e } from './seed'; @@ -21,6 +22,14 @@ async function migrate(databaseUrl: string) { } export default async () => { + // Wipe any leftover leveldb state from an aborted prior run before the + // first MMRService boots — stale MANIFEST files can otherwise surface as + // LEVEL_DATABASE_NOT_OPEN. + rmSync(path.resolve(__dirname, '../../leveldb_data'), { + recursive: true, + force: true, + }); + container = await new PostgreSqlContainer('postgres:16-alpine') .withDatabase('testdb') .withUsername('test') diff --git a/apps/backend-relayer/test/setups/jest-integrations.setup.ts b/apps/backend-relayer/test/setups/jest-integrations.setup.ts index 8e08bac..404db5e 100644 --- a/apps/backend-relayer/test/setups/jest-integrations.setup.ts +++ b/apps/backend-relayer/test/setups/jest-integrations.setup.ts @@ -5,7 +5,12 @@ import { import * as dotenv from 'dotenv'; import { execa } from 'execa'; import path from 'path'; -import { setupContracts } from './contract-setup'; +import { deployEvmContracts } from './evm-setup'; +import { + deployStellarContracts, + linkStellarAdManagerToOrderChain, + StellarChainData, +} from './stellar-setup'; import { seedDB } from './seed'; // Load .env (optional) @@ -38,11 +43,22 @@ export default async () => { await migrate(databaseUrl); - const { ethContracts, hederaContracts } = await setupContracts(); + const ethContracts = await deployEvmContracts(); + + // Stellar side is optional — only engages when the external bash + // orchestrator (scripts/run_cross_chain_e2e.sh) has exported the RPC + + // admin secret. Tests that depend on Stellar should skip when absent. + let stellarContracts: StellarChainData | undefined; + if (process.env.STELLAR_RPC_URL && process.env.STELLAR_ADMIN_SECRET) { + stellarContracts = await deployStellarContracts(); + await linkStellarAdManagerToOrderChain(stellarContracts, ethContracts); + } - await seedDB(ethContracts, hederaContracts); + await seedDB(ethContracts, stellarContracts); (global as any).__ETH_CONTRACTS__ = ethContracts; - (global as any).__HEDERA_CONTRACTS__ = hederaContracts; + if (stellarContracts) { + (global as any).__STELLAR_CONTRACTS__ = stellarContracts; + } (global as any).__PG_CONTAINER__ = container; }; diff --git a/apps/backend-relayer/test/setups/jest.teardown.ts b/apps/backend-relayer/test/setups/jest.teardown.ts index 30cd435..69590bf 100644 --- a/apps/backend-relayer/test/setups/jest.teardown.ts +++ b/apps/backend-relayer/test/setups/jest.teardown.ts @@ -1,7 +1,14 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { rmSync } from 'fs'; +import { resolve } from 'path'; + export default async () => { const container = (global as any).__PG_CONTAINER__; if (container) { await container.stop(); } + + // Clean up the ravedb leveldb data directory created during tests. + const leveldbPath = resolve(__dirname, '../../leveldb_data'); + rmSync(leveldbPath, { recursive: true, force: true }); }; diff --git a/apps/backend-relayer/test/setups/mock-chain-adapter.ts b/apps/backend-relayer/test/setups/mock-chain-adapter.ts new file mode 100644 index 0000000..493f6a9 --- /dev/null +++ b/apps/backend-relayer/test/setups/mock-chain-adapter.ts @@ -0,0 +1,264 @@ +import { randomBytes } from 'crypto'; +import { ChainAdapter } from '../../src/chain-adapters/adapters/chain-adapter.abstract'; +import { + T_AdManagerOrderParams, + T_CloseAdRequest, + T_CloseAdRequestContractDetails, + T_CreatFundAdRequest, + T_CreatFundAdRequestContractDetails, + T_CreateAdRequest, + T_CreateAdRequestContractDetails, + T_CreateOrderRequest, + T_CreateOrderRequestContractDetails, + T_CreateUnlockOrderContractDetails, + T_FetchRoot, + T_LockForOrderRequest, + T_LockForOrderRequestContractDetails, + T_OrderParams, + T_OrderPortalParams, + T_RequestValidation, + T_UnlockOrderContractDetails, + T_WithdrawFromAdRequest, + T_WithdrawFromAdRequestContractDetails, +} from '../../src/chain-adapters/types'; + +const ZERO_32 = + '0x0000000000000000000000000000000000000000000000000000000000000000' as const; +const FAKE_SIG = + ('0x' + 'ab'.repeat(65)) as `0x${string}`; +const ONE_HOUR_S = 3600; + +function uniqueHash(): `0x${string}` { + return ('0x' + randomBytes(32).toString('hex')) as `0x${string}`; +} + +// Stub adapter used by the e2e Nest app so `create*` / `lock*` / `unlock*` +// endpoints don't try to hit a live chain RPC. Returns deterministic values +// so DB rows and response shapes can be asserted. Real on-chain correctness +// is covered by `test:integrations`. +export class MockChainAdapter extends ChainAdapter { + private expiry(): number { + return Math.floor(Date.now() / 1000) + ONE_HOUR_S; + } + + getCreateAdRequestContractDetails( + data: T_CreateAdRequest, + ): Promise { + return Promise.resolve({ + chainId: data.adChainId.toString(), + contractAddress: data.adContractAddress, + signature: FAKE_SIG, + authToken: ZERO_32, + timeToExpire: this.expiry(), + adId: data.adId, + adToken: data.adToken, + initialAmount: data.initialAmount, + orderChainId: data.orderChainId.toString(), + adRecipient: data.adRecipient, + reqHash: uniqueHash(), + }); + } + + getFundAdRequestContractDetails( + data: T_CreatFundAdRequest, + ): Promise { + return Promise.resolve({ + chainId: data.adChainId.toString(), + contractAddress: data.adContractAddress, + signature: FAKE_SIG, + authToken: ZERO_32, + timeToExpire: this.expiry(), + adId: data.adId, + amount: data.amount, + reqHash: uniqueHash(), + }); + } + + getWithdrawFromAdRequestContractDetails( + data: T_WithdrawFromAdRequest, + ): Promise { + return Promise.resolve({ + chainId: data.adChainId.toString(), + contractAddress: data.adContractAddress, + signature: FAKE_SIG, + authToken: ZERO_32, + timeToExpire: this.expiry(), + adId: data.adId, + amount: data.amount, + to: data.to, + reqHash: uniqueHash(), + }); + } + + getCloseAdRequestContractDetails( + data: T_CloseAdRequest, + ): Promise { + return Promise.resolve({ + chainId: data.adChainId.toString(), + contractAddress: data.adContractAddress, + signature: FAKE_SIG, + authToken: ZERO_32, + timeToExpire: this.expiry(), + adId: data.adId, + to: data.to, + reqHash: uniqueHash(), + }); + } + + getLockForOrderRequestContractDetails( + data: T_LockForOrderRequest, + ): Promise { + return Promise.resolve({ + chainId: data.adChainId.toString(), + contractAddress: data.adContractAddress, + signature: FAKE_SIG, + authToken: ZERO_32, + timeToExpire: this.expiry(), + orderParams: toAdManagerParams(data.orderParams), + reqHash: uniqueHash(), + orderHash: uniqueHash(), + }); + } + + getCreateOrderRequestContractDetails( + data: T_CreateOrderRequest, + ): Promise { + return Promise.resolve({ + chainId: data.orderChainId.toString(), + contractAddress: data.orderContractAddress, + signature: FAKE_SIG, + authToken: ZERO_32, + timeToExpire: this.expiry(), + orderParams: toOrderPortalParams(data.orderParams), + orderHash: uniqueHash(), + reqHash: uniqueHash(), + }); + } + + getUnlockOrderContractDetails( + data: T_CreateUnlockOrderContractDetails, + ): Promise { + const params = data.isAdCreator + ? toOrderPortalParams(data.orderParams) + : toAdManagerParams(data.orderParams); + return Promise.resolve({ + chainId: data.chainId.toString(), + contractAddress: data.contractAddress, + signature: FAKE_SIG, + authToken: ZERO_32, + timeToExpire: this.expiry(), + orderParams: params, + nullifierHash: data.nullifierHash, + targetRoot: data.targetRoot, + proof: data.proof, + orderHash: uniqueHash(), + reqHash: uniqueHash(), + }); + } + + validateAdManagerRequest(_data: T_RequestValidation): Promise { + return Promise.resolve(true); + } + + validateOrderPortalRequest(_data: T_RequestValidation): Promise { + return Promise.resolve(true); + } + + fetchOnChainLatestRoot( + _isAdCreator: boolean, + _data: T_FetchRoot, + ): Promise { + return Promise.resolve(ZERO_32); + } + + fetchAdChainLatestRoot(_data: T_FetchRoot): Promise { + return Promise.resolve(ZERO_32); + } + + fetchOrderChainLatestRoot(_data: T_FetchRoot): Promise { + return Promise.resolve(ZERO_32); + } + + checkLocalRootExist( + _localRoot: string, + _isAdCreator: boolean, + _data: T_FetchRoot, + ): Promise { + return Promise.resolve(true); + } + + fetchOnChainRoots( + _isAdCreator: boolean, + _data: T_FetchRoot, + ): Promise { + return Promise.resolve([ZERO_32]); + } + + fetchAdChainRoots(_data: T_FetchRoot): Promise { + return Promise.resolve([ZERO_32]); + } + + fetchOrderChainRoots(_data: T_FetchRoot): Promise { + return Promise.resolve([ZERO_32]); + } + + mintToken(_data: { + chainId: string; + tokenAddress: `0x${string}`; + receiver: `0x${string}`; + }): Promise<{ txHash: string }> { + return Promise.resolve({ txHash: ZERO_32 }); + } + + checkTokenBalance(_data: { + chainId: string; + tokenAddress: `0x${string}`; + account: `0x${string}`; + }): Promise { + return Promise.resolve('0'); + } + + orderTypeHash(_orderParams: T_OrderParams): string { + return ZERO_32; + } + + verifyOrderSignature( + _address: `0x${string}`, + _orderHash: `0x${string}`, + _signature: `0x${string}`, + ): boolean { + return true; + } +} + +function toAdManagerParams(p: T_OrderParams): T_AdManagerOrderParams { + return { + orderChainToken: p.orderChainToken, + adChainToken: p.adChainToken, + amount: p.amount, + bridger: p.bridger, + orderChainId: p.orderChainId, + srcOrderPortal: p.orderPortal, + orderRecipient: p.orderRecipient, + adId: p.adId, + adCreator: p.adCreator, + adRecipient: p.adRecipient, + salt: p.salt, + }; +} + +function toOrderPortalParams(p: T_OrderParams): T_OrderPortalParams { + return { + orderChainToken: p.orderChainToken, + adChainToken: p.adChainToken, + amount: p.amount, + bridger: p.bridger, + orderRecipient: p.orderRecipient, + adChainId: p.adChainId, + adManager: p.adManager, + adId: p.adId, + adCreator: p.adCreator, + adRecipient: p.adRecipient, + salt: p.salt, + }; +} diff --git a/apps/backend-relayer/test/setups/seed.ts b/apps/backend-relayer/test/setups/seed.ts index 7bbe02b..6781966 100644 --- a/apps/backend-relayer/test/setups/seed.ts +++ b/apps/backend-relayer/test/setups/seed.ts @@ -1,9 +1,10 @@ -import { PrismaClient } from '@prisma/client'; -import { ChainData, seedAdmin, seedChain, seedToken } from './utils'; +import { ChainKind, PrismaClient } from '@prisma/client'; +import { ChainData, seedAdmin, seedChain, seedRoute, seedToken } from './utils'; +import { StellarChainData } from './stellar-setup'; export const seedDB = async ( ethContracts: ChainData, - hederaContracts: ChainData, + stellarContracts?: StellarChainData, ) => { const prisma = new PrismaClient(); @@ -12,49 +13,46 @@ export const seedDB = async ( await seedAdmin(prisma, 'admin@x.com', 'ChangeMe123!'); - const chain1 = await seedChain(prisma, { + const ethChain = await seedChain(prisma, { name: ethContracts.name, chainId: BigInt(ethContracts.chainId), ad: ethContracts.adManagerAddress, op: ethContracts.orderPortalAddress, + kind: ChainKind.EVM, }); - const chain2 = await seedChain(prisma, { - name: hederaContracts.name, - chainId: BigInt(hederaContracts.chainId), - ad: hederaContracts.adManagerAddress, - op: hederaContracts.orderPortalAddress, - }); - - const token1 = await seedToken( + const ethToken = await seedToken( prisma, - chain1.id, + ethChain.id, ethContracts.tokenName, ethContracts.tokenSymbol, ethContracts.tokenAddress, ); - const token2 = await seedToken( - prisma, - chain2.id, - hederaContracts.tokenName, - hederaContracts.tokenSymbol, - hederaContracts.tokenAddress, - ); + if (stellarContracts) { + const stellarChain = await seedChain(prisma, { + name: stellarContracts.name, + chainId: BigInt(stellarContracts.chainId), + ad: stellarContracts.adManagerAddress, + // Stellar side has no OrderPortal in this direction — reuse the + // AdManager address as a non-null placeholder for the schema. + op: stellarContracts.adManagerAddress, + kind: ChainKind.STELLAR, + }); - // Create route in both directions - await prisma.route.createMany({ - data: [ - { - adTokenId: token1.id, - orderTokenId: token2.id, - }, - { - adTokenId: token2.id, - orderTokenId: token1.id, - }, - ], - }); + const stellarToken = await seedToken( + prisma, + stellarChain.id, + stellarContracts.tokenName, + stellarContracts.tokenSymbol, + stellarContracts.tokenAddress, + 'NATIVE', + 7, + ); + + // Stellar ad token → EVM order token. + await seedRoute(prisma, stellarToken.id, ethToken.id); + } await prisma.$disconnect(); diff --git a/apps/backend-relayer/test/setups/stellar-actions.ts b/apps/backend-relayer/test/setups/stellar-actions.ts new file mode 100644 index 0000000..6c1f460 --- /dev/null +++ b/apps/backend-relayer/test/setups/stellar-actions.ts @@ -0,0 +1,260 @@ +// Stellar contract-action helpers — Soroban analogue of contract-actions.ts. +// +// Each wrapper takes the relayer's signed-request payload (signature + signer +// public key + authToken + timeToExpire) plus the raw contract args, builds a +// Soroban invocation, prepares + signs + submits, and polls for success. + +import { + Address, + Contract, + Keypair, + Networks, + TransactionBuilder, + nativeToScVal, + rpc, + xdr, +} from '@stellar/stellar-sdk'; +import { hex32ToBuffer, hex32ToContractId } from '../../src/providers/stellar/utils/address'; + +const BASE_FEE = '1000'; + +function getServer(): rpc.Server { + const url = process.env.STELLAR_RPC_URL; + if (!url) throw new Error('STELLAR_RPC_URL not set'); + return new rpc.Server(url, { allowHttp: url.startsWith('http://') }); +} + +function passphrase(): string { + return process.env.STELLAR_NETWORK_PASSPHRASE || Networks.TESTNET; +} + +async function invoke( + signer: Keypair, + contractHex: string, + method: string, + args: xdr.ScVal[], +): Promise { + const server = getServer(); + const contract = new Contract(hex32ToContractId(contractHex)); + const source = await server.getAccount(signer.publicKey()); + const tx = new TransactionBuilder(source, { + fee: BASE_FEE, + networkPassphrase: passphrase(), + }) + .addOperation(contract.call(method, ...args)) + .setTimeout(60) + .build(); + const prepared = await server.prepareTransaction(tx); + prepared.sign(signer); + const sent = await server.sendTransaction(prepared); + if (sent.status === 'ERROR') { + throw new Error( + `Stellar send failed [${method}]: ${JSON.stringify(sent.errorResult)}`, + ); + } + for (let i = 0; i < 20; i++) { + const got = await server.getTransaction(sent.hash); + if (got.status === rpc.Api.GetTransactionStatus.SUCCESS) return sent.hash; + if (got.status === rpc.Api.GetTransactionStatus.FAILED) { + throw new Error(`Stellar tx [${method}] FAILED hash=${sent.hash}`); + } + await new Promise((r) => setTimeout(r, 1000)); + } + throw new Error(`Stellar tx [${method}] timed out hash=${sent.hash}`); +} + +// ── scval helpers ─────────────────────────────────────────────────── + +function bytesN(hex: string): xdr.ScVal { + return nativeToScVal(hex32ToBuffer(hex), { type: 'bytes' }); +} + +function bytes(buf: Buffer): xdr.ScVal { + return nativeToScVal(buf, { type: 'bytes' }); +} + +function u64(n: number | bigint): xdr.ScVal { + return nativeToScVal(BigInt(n), { type: 'u64' }); +} + +function u128(n: string | bigint): xdr.ScVal { + return nativeToScVal(BigInt(n), { type: 'u128' }); +} + +function strVal(s: string): xdr.ScVal { + return nativeToScVal(s, { type: 'string' }); +} + +// Shared auth quadruple (signature, public_key, auth_token, time_to_expire). +function authArgs( + signatureHex: string, + publicKeyHex: string, + authTokenHex: string, + timeToExpire: number, +): xdr.ScVal[] { + return [ + bytes(Buffer.from(signatureHex.replace(/^0x/, ''), 'hex')), + bytesN(publicKeyHex), + bytesN(authTokenHex), + u64(timeToExpire), + ]; +} + +// ── ad-manager wrappers ───────────────────────────────────────────── + +export async function createAdSoroban( + signer: Keypair, + signatureHex: string, + signerPublicKeyHex: string, + authTokenHex: string, + timeToExpire: number, + creatorPublicKey: string, // G-strkey of the ad creator (signer) + adId: string, + adTokenHex: string, + initialAmount: string, + orderChainId: string, + adRecipientHex: string, + adManagerHex: string, +): Promise { + const args = [ + ...authArgs(signatureHex, signerPublicKeyHex, authTokenHex, timeToExpire), + new Address(creatorPublicKey).toScVal(), + strVal(adId), + bytesN(adTokenHex), + u128(initialAmount), + u128(orderChainId), + bytesN(adRecipientHex), + ]; + return invoke(signer, adManagerHex, 'create_ad', args); +} + +export async function fundAdSoroban( + signer: Keypair, + signatureHex: string, + signerPublicKeyHex: string, + authTokenHex: string, + timeToExpire: number, + adId: string, + amount: string, + adManagerHex: string, +): Promise { + const args = [ + ...authArgs(signatureHex, signerPublicKeyHex, authTokenHex, timeToExpire), + strVal(adId), + u128(amount), + ]; + return invoke(signer, adManagerHex, 'fund_ad', args); +} + +export async function withdrawFromAdSoroban( + signer: Keypair, + signatureHex: string, + signerPublicKeyHex: string, + authTokenHex: string, + timeToExpire: number, + adId: string, + amount: string, + toPublicKey: string, // G-strkey + adManagerHex: string, +): Promise { + const args = [ + ...authArgs(signatureHex, signerPublicKeyHex, authTokenHex, timeToExpire), + strVal(adId), + u128(amount), + new Address(toPublicKey).toScVal(), + ]; + return invoke(signer, adManagerHex, 'withdraw_from_ad', args); +} + +export async function closeAdSoroban( + signer: Keypair, + signatureHex: string, + signerPublicKeyHex: string, + authTokenHex: string, + timeToExpire: number, + adId: string, + toPublicKey: string, // G-strkey + adManagerHex: string, +): Promise { + const args = [ + ...authArgs(signatureHex, signerPublicKeyHex, authTokenHex, timeToExpire), + strVal(adId), + new Address(toPublicKey).toScVal(), + ]; + return invoke(signer, adManagerHex, 'close_ad', args); +} + +// Matches contracts/stellar/contracts/ad-manager/src/types.rs::OrderParams. +export interface StellarOrderParams { + orderChainToken: string; // 0x + 64 hex + adChainToken: string; + amount: string; + bridger: string; + orderChainId: string; + srcOrderPortal: string; + orderRecipient: string; + adId: string; + adCreator: string; + adRecipient: string; + salt: string; +} + +function orderParamsScVal(p: StellarOrderParams): xdr.ScVal { + // Soroban struct is encoded as an ScMap with entries sorted by key. + const entries: Array<[string, xdr.ScVal]> = [ + ['ad_chain_token', bytesN(p.adChainToken)], + ['ad_creator', bytesN(p.adCreator)], + ['ad_id', strVal(p.adId)], + ['ad_recipient', bytesN(p.adRecipient)], + ['amount', u128(p.amount)], + ['bridger', bytesN(p.bridger)], + ['order_chain_id', u128(p.orderChainId)], + ['order_chain_token', bytesN(p.orderChainToken)], + ['order_recipient', bytesN(p.orderRecipient)], + ['salt', u128(p.salt)], + ['src_order_portal', bytesN(p.srcOrderPortal)], + ]; + return xdr.ScVal.scvMap( + entries.map(([k, v]) => + new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol(k), val: v }), + ), + ); +} + +export async function lockForOrderSoroban( + signer: Keypair, + signatureHex: string, + signerPublicKeyHex: string, + authTokenHex: string, + timeToExpire: number, + params: StellarOrderParams, + adManagerHex: string, +): Promise { + const args = [ + ...authArgs(signatureHex, signerPublicKeyHex, authTokenHex, timeToExpire), + orderParamsScVal(params), + ]; + return invoke(signer, adManagerHex, 'lock_for_order', args); +} + +export async function unlockSoroban( + signer: Keypair, + signatureHex: string, + signerPublicKeyHex: string, + authTokenHex: string, + timeToExpire: number, + params: StellarOrderParams, + nullifierHashHex: string, + targetRootHex: string, + proof: Buffer, + adManagerHex: string, +): Promise { + const args = [ + ...authArgs(signatureHex, signerPublicKeyHex, authTokenHex, timeToExpire), + orderParamsScVal(params), + bytesN(nullifierHashHex), + bytesN(targetRootHex), + bytes(proof), + ]; + return invoke(signer, adManagerHex, 'unlock', args); +} diff --git a/apps/backend-relayer/test/setups/stellar-setup.ts b/apps/backend-relayer/test/setups/stellar-setup.ts new file mode 100644 index 0000000..911aa0b --- /dev/null +++ b/apps/backend-relayer/test/setups/stellar-setup.ts @@ -0,0 +1,300 @@ +// Stellar deploy helper — symmetric with `deployEvmContracts()`. +// Uploads each WASM, instantiates the contracts, initialises them, and +// deploys the native XLM SAC so the test has a token route. +// +// Assumes a Stellar network is already running at STELLAR_RPC_URL with the +// admin account (STELLAR_ADMIN_SECRET) friendbot-funded. The external +// `run_cross_chain_e2e.sh` script sets both up. + +import fs from 'node:fs'; +import path from 'node:path'; +import { + Asset, + Keypair, + Networks, + Operation, + StrKey, + TransactionBuilder, + hash, + nativeToScVal, + rpc, + xdr, + Address, +} from '@stellar/stellar-sdk'; +import { + accountIdToHex32, + contractIdToHex32, + hex32ToBuffer, + hex32ToContractId, +} from '../../src/providers/stellar/utils/address'; +import { ChainData } from './utils'; + +const BASE_FEE = '1000'; + +export interface StellarChainData { + adManagerAddress: `0x${string}`; + merkleManagerAddress: `0x${string}`; + verifierAddress: `0x${string}`; + // The native XLM SAC doubles as the ad token for this test. + tokenAddress: `0x${string}`; + chainId: string; + name: string; + tokenName: string; + tokenSymbol: string; + adminPublicKeyHex: `0x${string}`; + adminSecret: string; // S… strkey, handed back so the test can sign with it +} + +export const STELLAR_CHAIN_ID = '1000001'; +const STELLAR_CHAIN_NAME = 'STELLAR LOCALNET'; + +function getServer(): rpc.Server { + const url = process.env.STELLAR_RPC_URL; + if (!url) throw new Error('STELLAR_RPC_URL not set'); + return new rpc.Server(url, { allowHttp: url.startsWith('http://') }); +} + +function networkPassphrase(): string { + return process.env.STELLAR_NETWORK_PASSPHRASE || Networks.TESTNET; +} + +function loadAdminKeypair(): Keypair { + const raw = (process.env.STELLAR_ADMIN_SECRET ?? '').trim(); + if (!raw) throw new Error('STELLAR_ADMIN_SECRET not set'); + if (StrKey.isValidEd25519SecretSeed(raw)) return Keypair.fromSecret(raw); + if (/^0x[a-fA-F0-9]{64}$/.test(raw)) { + return Keypair.fromRawEd25519Seed(Buffer.from(raw.slice(2), 'hex')); + } + throw new Error( + 'Invalid STELLAR_ADMIN_SECRET (expected S… strkey or 0x + 64 hex)', + ); +} + +async function submit( + server: rpc.Server, + signer: Keypair, + buildOp: () => xdr.Operation, +): Promise { + const source = await server.getAccount(signer.publicKey()); + const tx = new TransactionBuilder(source, { + fee: BASE_FEE, + networkPassphrase: networkPassphrase(), + }) + .addOperation(buildOp()) + .setTimeout(60) + .build(); + const prepared = await server.prepareTransaction(tx); + prepared.sign(signer); + const sent = await server.sendTransaction(prepared); + if (sent.status === 'ERROR') { + throw new Error( + `Stellar send failed: ${JSON.stringify(sent.errorResult)}`, + ); + } + for (let i = 0; i < 20; i++) { + const got = await server.getTransaction(sent.hash); + if (got.status === rpc.Api.GetTransactionStatus.SUCCESS) return got; + if (got.status === rpc.Api.GetTransactionStatus.FAILED) { + throw new Error(`Stellar tx FAILED hash=${sent.hash}`); + } + await new Promise((r) => setTimeout(r, 1000)); + } + throw new Error(`Stellar tx timed out hash=${sent.hash}`); +} + +async function uploadWasm( + server: rpc.Server, + signer: Keypair, + wasm: Buffer, +): Promise { + await submit(server, signer, () => Operation.uploadContractWasm({ wasm })); + return hash(wasm); +} + +async function createContract( + server: rpc.Server, + signer: Keypair, + wasmHash: Buffer, + constructorArgs: xdr.ScVal[] = [], +): Promise { + const salt = new Uint8Array(32); + globalThis.crypto.getRandomValues(salt); + const res = await submit(server, signer, () => + Operation.createCustomContract({ + address: Address.fromString(signer.publicKey()), + wasmHash, + salt: Buffer.from(salt), + constructorArgs, + }), + ); + const retval = res.returnValue; + if (!retval) throw new Error('createCustomContract: no return value'); + const addr = Address.fromScAddress(retval.address()).toString(); + if (!addr.startsWith('C')) throw new Error(`unexpected contract addr: ${addr}`); + return addr; +} + +async function invoke( + server: rpc.Server, + signer: Keypair, + contractIdStrkey: string, + method: string, + args: xdr.ScVal[], +): Promise { + await submit(server, signer, () => + Operation.invokeContractFunction({ + contract: contractIdStrkey, + function: method, + args, + }), + ); +} + +async function deployNativeSac( + server: rpc.Server, + signer: Keypair, +): Promise { + // If the native SAC is already deployed on this network, createStellarAssetContract + // returns an error — fall back to the deterministic contractId. + const asset = Asset.native(); + try { + const res = await submit(server, signer, () => + Operation.createStellarAssetContract({ asset }), + ); + const retval = res.returnValue; + if (!retval) throw new Error('createStellarAssetContract: no return value'); + return Address.fromScAddress(retval.address()).toString(); + } catch { + return asset.contractId(networkPassphrase()); + } +} + +function wasmPath(name: string): string { + return path.join( + __dirname, + '../../src/providers/stellar/wasm', + `${name}.wasm`, + ); +} + +function vkBytes(): Buffer { + const vkPath = path.resolve( + __dirname, + '../../../../proof_circuits/deposits/target/vk', + ); + if (!fs.existsSync(vkPath)) { + throw new Error( + `Verifier VK not found at ${vkPath}. Run scripts/build_circuits.sh first.`, + ); + } + return fs.readFileSync(vkPath); +} + +export async function deployStellarContracts(): Promise { + const admin = loadAdminKeypair(); + const server = getServer(); + console.log( + `Deploying STELLAR contracts (admin=${admin.publicKey()}, rpc=${process.env.STELLAR_RPC_URL})...`, + ); + + // Upload WASMs. + const verifierWasm = fs.readFileSync(wasmPath('verifier')); + const merkleWasm = fs.readFileSync(wasmPath('merkle_manager')); + const adWasm = fs.readFileSync(wasmPath('ad_manager')); + const verifierHash = await uploadWasm(server, admin, verifierWasm); + const merkleHash = await uploadWasm(server, admin, merkleWasm); + const adHash = await uploadWasm(server, admin, adWasm); + + // Native XLM SAC — used as the Stellar-side ad token. + const xlmSacStrkey = await deployNativeSac(server, admin); + console.log(` Native XLM SAC: ${xlmSacStrkey}`); + + // Verifier — constructor takes the VK bytes. + const verifierStrkey = await createContract(server, admin, verifierHash, [ + nativeToScVal(vkBytes(), { type: 'bytes' }), + ]); + console.log(` Verifier: ${verifierStrkey}`); + + // MerkleManager — initialize(admin). + const merkleStrkey = await createContract(server, admin, merkleHash); + await invoke(server, admin, merkleStrkey, 'initialize', [ + new Address(admin.publicKey()).toScVal(), + ]); + console.log(` MerkleManager: ${merkleStrkey}`); + + // AdManager — initialize(admin, verifier, merkle, w_native, chain_id). + const adStrkey = await createContract(server, admin, adHash); + await invoke(server, admin, adStrkey, 'initialize', [ + new Address(admin.publicKey()).toScVal(), + new Address(verifierStrkey).toScVal(), + new Address(merkleStrkey).toScVal(), + new Address(xlmSacStrkey).toScVal(), + nativeToScVal(BigInt(STELLAR_CHAIN_ID), { type: 'u128' }), + ]); + console.log(` AdManager: ${adStrkey}`); + + // Grant AdManager merkle_manager role so it can write roots on create/lock. + await invoke(server, admin, merkleStrkey, 'set_manager', [ + new Address(adStrkey).toScVal(), + xdr.ScVal.scvBool(true), + ]); + + const contracts: StellarChainData = { + adManagerAddress: contractIdToHex32(adStrkey), + merkleManagerAddress: contractIdToHex32(merkleStrkey), + verifierAddress: contractIdToHex32(verifierStrkey), + tokenAddress: contractIdToHex32(xlmSacStrkey), + chainId: STELLAR_CHAIN_ID, + name: STELLAR_CHAIN_NAME, + tokenName: 'Native XLM', + tokenSymbol: 'XLM', + adminPublicKeyHex: accountIdToHex32(admin.publicKey()), + adminSecret: admin.secret(), + }; + + const filePath = path.join(__dirname, 'stellar-deployed-contracts.json'); + fs.writeFileSync(filePath, JSON.stringify(contracts, null, 2)); + console.log('Stellar contract addresses saved to:', filePath); + + return contracts; +} + +// Hex-form bytes32 helpers so callers on the EVM side can pass raw addresses. +function bytes32FromEvmAddress(evmAddress: string): `0x${string}` { + const clean = evmAddress.replace(/^0x/i, '').toLowerCase().padStart(40, '0'); + return `0x${'00'.repeat(12)}${clean}` as `0x${string}`; +} + +function bytesN(hex: string) { + return nativeToScVal(hex32ToBuffer(hex), { type: 'bytes' }); +} + +// Register the order chain + token route on the Stellar AdManager so orders +// from the EVM chain are accepted. Analogous to setupAdManager() on EVM. +export async function linkStellarAdManagerToOrderChain( + stellar: StellarChainData, + orderChain: ChainData, +): Promise { + const admin = loadAdminKeypair(); + const server = getServer(); + const adStrkey = hex32ToContractId(stellar.adManagerAddress); + + // Stellar AdManager expects bytes32 for addresses from the order chain — + // left-pad 20-byte EVM addresses to 32 bytes. + const orderPortalBytes32 = bytes32FromEvmAddress(orderChain.orderPortalAddress); + const orderTokenBytes32 = bytes32FromEvmAddress(orderChain.tokenAddress); + + await invoke(server, admin, adStrkey, 'set_chain', [ + nativeToScVal(BigInt(orderChain.chainId), { type: 'u128' }), + bytesN(orderPortalBytes32), + xdr.ScVal.scvBool(true), + ]); + console.log(' Stellar AdManager.set_chain → order chain registered'); + + await invoke(server, admin, adStrkey, 'set_token_route', [ + bytesN(stellar.tokenAddress), + bytesN(orderTokenBytes32), + nativeToScVal(BigInt(orderChain.chainId), { type: 'u128' }), + ]); + console.log(' Stellar AdManager.set_token_route → route set'); +} diff --git a/apps/backend-relayer/test/setups/utils.ts b/apps/backend-relayer/test/setups/utils.ts index 340df43..9a10afd 100644 --- a/apps/backend-relayer/test/setups/utils.ts +++ b/apps/backend-relayer/test/setups/utils.ts @@ -2,7 +2,7 @@ import { INestApplication } from '@nestjs/common'; import { ethers } from 'ethers'; import request from 'supertest'; import { SiweMessage } from 'siwe'; -import { PrismaClient } from '@prisma/client'; +import { PrismaClient, ChainKind } from '@prisma/client'; import { hash } from '@node-rs/argon2'; import { privateKeyToAddress, signMessage } from 'viem/accounts'; import { @@ -13,8 +13,12 @@ import { } from 'viem'; import { parseEther } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; -import { ethLocalnet } from '../../src/providers/viem/localnet'; -import { hederaTestnet } from 'viem/chains'; +import { + Keypair, + Networks, + TransactionBuilder, +} from '@stellar/stellar-sdk'; +import { ethLocalnet } from '../../src/providers/viem/ethers/localnet'; export type AddressLike = `0x${string}`; @@ -47,7 +51,7 @@ export const loginUser = async ( // make challenge request const challenge = await request(app.getHttpServer()) .post('/v1/auth/challenge') - .send({ address }) + .send({ address, chainKind: ChainKind.EVM }) .expect(200); const body = challenge.body as ChallengeResponse; @@ -76,7 +80,36 @@ export const loginUser = async ( // send to login const res = await request(app.getHttpServer()) .post('/v1/auth/login') - .send({ message, signature }) + .send({ message, signature, chainKind: ChainKind.EVM }) + .expect(201); + + return res.body.tokens.access as string; +}; + +export const loginStellarUser = async ( + app: INestApplication, + keypair: Keypair, +): Promise => { + const address = keypair.publicKey(); + + const challenge = await request(app.getHttpServer()) + .post('/v1/auth/challenge') + .send({ address, chainKind: ChainKind.STELLAR }) + .expect(200); + + const xdrString = challenge.body.transaction as string; + const passphrase = + (challenge.body.networkPassphrase as string) || + process.env.STELLAR_NETWORK_PASSPHRASE || + Networks.TESTNET; + + const tx = TransactionBuilder.fromXDR(xdrString, passphrase as Networks); + tx.sign(keypair); + const signedXdr = tx.toEnvelope().toXDR('base64'); + + const res = await request(app.getHttpServer()) + .post('/v1/auth/login') + .send({ transaction: signedXdr, chainKind: ChainKind.STELLAR }) .expect(201); return res.body.tokens.access as string; @@ -107,6 +140,8 @@ export const seedToken = async ( name: string = 'Ether', symbol = 'ETH', address?: string, + kind: 'NATIVE' | 'ERC20' | 'SAC' | 'SEP41' = 'ERC20', + decimals = 18, ) => { return prisma.token.upsert({ where: { @@ -120,14 +155,14 @@ export const seedToken = async ( symbol, name: name, address: address ?? randomAddress(), - decimals: 18, - kind: 'ERC20', + decimals, + kind, }, update: { symbol, name: name, - decimals: 18, - kind: 'ERC20', + decimals, + kind, }, select: { id: true, symbol: true, chain: true }, }); @@ -140,12 +175,14 @@ export const seedChain = async ( chainId: bigint; ad: string; op: string; + kind: ChainKind; }>, ) => { const name = params?.name ?? `Chain-${Math.floor(Math.random() * 10000)}`; const chainId = params?.chainId ?? BigInt(Math.floor(Math.random() * 10000)); const ad = params?.ad ?? randomAddress(); const op = params?.op ?? randomAddress(); + const kind = params?.kind ?? ChainKind.EVM; return prisma.chain.upsert({ where: { @@ -154,6 +191,7 @@ export const seedChain = async ( create: { name, chainId: chainId, + kind, adManagerAddress: ad, orderPortalAddress: op, mmr: { @@ -164,6 +202,7 @@ export const seedChain = async ( }, update: { name, + kind, adManagerAddress: ad, orderPortalAddress: op, }, @@ -191,6 +230,7 @@ export const seedAd = async ( adTokenId: string, orderTokenId: string, pool = 1_000_000, + status: 'INACTIVE' | 'ACTIVE' | 'PAUSED' | 'CLOSED' = 'INACTIVE', ) => prisma.ad.create({ data: { @@ -199,7 +239,7 @@ export const seedAd = async ( adTokenId, orderTokenId, poolAmount: pool, - status: 'INACTIVE', + status, creatorDstAddress: creator, }, select: { id: true, creatorAddress: true, routeId: true }, @@ -213,9 +253,6 @@ export function randomAddress() { export const makeEthClient = () => createPublicClient({ chain: ethLocalnet, transport: http() }); -export const makeHederaClient = () => - createPublicClient({ chain: hederaTestnet, transport: http() }); - async function tryTopUpViaRpc(addr: AddressLike, hexWei: string) { const ethRpc = process.env.ETHEREUM_RPC_URL ?? 'http://localhost:9545'; @@ -272,38 +309,6 @@ export async function fundEthAddress( } } -export async function fundHBar( - client: PublicClient, - to: AddressLike, - minBalanceEther = '3.0', -): Promise { - const needed = parseEther(minBalanceEther); - - const current = await client.getBalance({ address: to }); - if (current >= needed) return; - - const managerKey = process.env.MANAGER_KEY as `0x${string}` | undefined; - - if (!managerKey) { - throw new Error('Manager address not set'); - } - - console.log('funding with hbar', to); - - const wallet = createWalletClient({ - chain: client.chain ?? hederaTestnet, - transport: http(), - account: privateKeyToAccount(managerKey), - }); - - const hash = await wallet.sendTransaction({ - to, - value: parseEther('10'), // send 10 ETH - }); - - await client.waitForTransactionReceipt({ hash }); -} - export const expectObject = ( obj: any, fields: Partial>, diff --git a/apps/backend-relayer/tsconfig.json b/apps/backend-relayer/tsconfig.json index df5fb9f..8a9178b 100644 --- a/apps/backend-relayer/tsconfig.json +++ b/apps/backend-relayer/tsconfig.json @@ -16,6 +16,7 @@ "baseUrl": "./", "incremental": true, "skipLibCheck": true, + "types": ["jest", "node"], "strictNullChecks": true, "forceConsistentCasingInFileNames": true, "noImplicitAny": false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 322b7b4..e4d138b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,6 +70,9 @@ importers: '@prisma/client': specifier: 6.16.1 version: 6.16.1(prisma@6.16.1(typescript@5.9.3))(typescript@5.9.3) + '@stellar/stellar-sdk': + specifier: ^15.0.1 + version: 15.0.1 '@types/morgan': specifier: ^1.9.10 version: 1.9.10 @@ -232,10 +235,10 @@ importers: version: 2.1.2(gsap@3.14.2)(react@19.1.0) '@rainbow-me/rainbowkit': specifier: ^2.2.8 - version: 2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6)) + version: 2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) '@rainbow-me/rainbowkit-siwe-next-auth': specifier: ^0.5.0 - version: 0.5.0(@rainbow-me/rainbowkit@2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6)))(next-auth@4.24.11(next@15.5.15(@babel/core@7.29.0)(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react@19.1.0) + version: 0.5.0(@rainbow-me/rainbowkit@2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)))(next-auth@4.24.11(next@15.5.15(@babel/core@7.29.0)(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react@19.1.0) '@tanstack/react-query': specifier: ^5.89.0 version: 5.98.0(react@19.1.0) @@ -301,10 +304,10 @@ importers: version: 4.0.0(tailwindcss@4.2.2) viem: specifier: ^2.37.7 - version: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + version: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: specifier: ^2.17.1 - version: 2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6) + version: 2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) devDependencies: '@eslint/eslintrc': specifier: ^3 @@ -2612,6 +2615,19 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@stellar/js-xdr@4.0.0': + resolution: {integrity: sha512-+NmNa7Tk5BI5XFdy/6xGTqAN4J9a9KgCrCGhj2uEUTCBhLkch0M+QbKzNH8zEnejWe0p8w+0q5hUVX6L3OzoVA==} + engines: {node: '>=20.0.0', pnpm: '>=9.0.0'} + + '@stellar/stellar-base@15.0.0': + resolution: {integrity: sha512-XQhxUr9BYiEcFcgc4oWcCMR9QJCny/GmmGsuwPKf/ieIcOeb5149KLHYx9mJCA0ea8QbucR2/GzV58QbXOTxQA==} + engines: {node: '>=20.0.0'} + + '@stellar/stellar-sdk@15.0.1': + resolution: {integrity: sha512-iZjWKXtfohsPh+CX9wRyQNIlXLeA9VyuQB6UMC7AFBD9XnR92eOjnlfeONzk/Bsrkk6+UPlpzSy2MuF+ydHP1A==} + engines: {node: '>=20.0.0'} + hasBin: true + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -3591,6 +3607,10 @@ packages: base-x@5.0.1: resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + base32.js@0.1.0: + resolution: {integrity: sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==} + engines: {node: '>=0.12.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -3612,6 +3632,9 @@ packages: bignumber.js@10.0.2: resolution: {integrity: sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -3888,6 +3911,10 @@ packages: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} + 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==} @@ -4549,6 +4576,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -4628,6 +4659,9 @@ packages: picomatch: optional: true + feaxios@0.0.23: + resolution: {integrity: sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==} + fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} @@ -5080,6 +5114,10 @@ packages: resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} engines: {node: '>=10'} + is-retry-allowed@3.0.0: + resolution: {integrity: sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==} + engines: {node: '>=12'} + is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} @@ -6394,6 +6432,9 @@ packages: radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -7242,6 +7283,9 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -7532,6 +7576,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urijs@1.19.11: + resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -8266,16 +8313,16 @@ snapshots: '@balena/dockerignore@1.0.2': {} - '@base-org/account@2.4.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@4.3.6)': + '@base-org/account@2.4.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@coinbase/cdp-sdk': 1.47.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 idb-keyval: 6.2.1 - ox: 0.6.9(typescript@5.9.3)(zod@4.3.6) + ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) preact: 10.24.2 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.3(@types/react@19.2.14)(react@19.1.0)(use-sync-external-store@1.4.0(react@19.1.0)) transitivePeerDependencies: - '@types/react' @@ -8338,15 +8385,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@coinbase/wallet-sdk@4.3.6(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@4.3.6)': + '@coinbase/wallet-sdk@4.3.6(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 idb-keyval: 6.2.1 - ox: 0.6.9(typescript@5.9.3)(zod@4.3.6) + ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) preact: 10.24.2 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.3(@types/react@19.2.14)(react@19.1.0)(use-sync-external-store@1.4.0(react@19.1.0)) transitivePeerDependencies: - '@types/react' @@ -8555,11 +8602,11 @@ snapshots: ethereum-cryptography: 2.2.1 micro-ftch: 0.3.1 - '@gemini-wallet/core@0.3.2(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))': + '@gemini-wallet/core@0.3.2(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@metamask/rpc-errors': 7.0.2 eventemitter3: 5.0.1 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - supports-color @@ -9682,13 +9729,13 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@rainbow-me/rainbowkit-siwe-next-auth@0.5.0(@rainbow-me/rainbowkit@2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6)))(next-auth@4.24.11(next@15.5.15(@babel/core@7.29.0)(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react@19.1.0)': + '@rainbow-me/rainbowkit-siwe-next-auth@0.5.0(@rainbow-me/rainbowkit@2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)))(next-auth@4.24.11(next@15.5.15(@babel/core@7.29.0)(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react@19.1.0)': dependencies: - '@rainbow-me/rainbowkit': 2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6)) + '@rainbow-me/rainbowkit': 2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) next-auth: 4.24.11(next@15.5.15(@babel/core@7.29.0)(react-dom@19.2.5(react@19.1.0))(react@19.1.0))(react-dom@19.2.5(react@19.1.0))(react@19.1.0) react: 19.1.0 - '@rainbow-me/rainbowkit@2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6))': + '@rainbow-me/rainbowkit@2.2.10(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(react-dom@19.2.5(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))': dependencies: '@tanstack/react-query': 5.98.0(react@19.1.0) '@vanilla-extract/css': 1.17.3 @@ -9700,8 +9747,8 @@ snapshots: react-dom: 19.2.5(react@19.1.0) react-remove-scroll: 2.6.2(@types/react@19.2.14)(react@19.1.0) ua-parser-js: 1.0.41 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - wagmi: 2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + wagmi: 2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) transitivePeerDependencies: - '@types/react' - babel-plugin-macros @@ -9786,24 +9833,24 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-common@1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@reown/appkit-common@1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript - utf-8-validate - zod - '@reown/appkit-controllers@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@reown/appkit-controllers@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@19.2.14)(react@19.1.0) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -9832,12 +9879,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@reown/appkit-pay@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@4.3.6) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@3.25.76) lit: 3.3.0 valtio: 1.13.2(@types/react@19.2.14)(react@19.1.0) transitivePeerDependencies: @@ -9872,12 +9919,12 @@ snapshots: dependencies: buffer: 6.0.3 - '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@4.3.6)': + '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@4.3.6) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -9909,10 +9956,10 @@ snapshots: - valtio - zod - '@reown/appkit-ui@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@reown/appkit-ui@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 @@ -9944,16 +9991,16 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@4.3.6)': + '@reown/appkit-utils@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/logger': 2.1.2 - '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@19.2.14)(react@19.1.0) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -9993,21 +10040,21 @@ snapshots: - typescript - utf-8-validate - '@reown/appkit@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@reown/appkit@1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - '@reown/appkit-pay': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@4.3.6) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@4.3.6) + '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.1.0))(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/types': 2.21.0 - '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) bs58: 6.0.0 valtio: 1.13.2(@types/react@19.2.14)(react@19.1.0) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10038,9 +10085,9 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - bufferutil @@ -10048,10 +10095,10 @@ snapshots: - utf-8-validate - zod - '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.23.1 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript @@ -10572,6 +10619,31 @@ snapshots: '@standard-schema/spec@1.0.0': {} + '@stellar/js-xdr@4.0.0': {} + + '@stellar/stellar-base@15.0.0': + dependencies: + '@noble/curves': 1.9.7 + '@stellar/js-xdr': 4.0.0 + base32.js: 0.1.0 + bignumber.js: 9.3.1 + buffer: 6.0.3 + sha.js: 2.4.12 + + '@stellar/stellar-sdk@15.0.1': + dependencies: + '@stellar/stellar-base': 15.0.0 + axios: 1.15.0 + bignumber.js: 9.3.1 + commander: 14.0.3 + eventsource: 2.0.2 + feaxios: 0.0.23 + randombytes: 2.1.0 + toml: 3.0.0 + urijs: 1.19.11 + transitivePeerDependencies: + - debug + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -11057,19 +11129,19 @@ snapshots: dependencies: '@vanilla-extract/css': 1.17.3 - '@wagmi/connectors@6.2.0(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6))(zod@4.3.6)': + '@wagmi/connectors@6.2.0(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@4.3.6) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@4.3.6) - '@gemini-wallet/core': 0.3.2(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) + '@base-org/account': 2.4.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@3.25.76) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@3.25.76) + '@gemini-wallet/core': 0.3.2(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@metamask/sdk': 0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) - '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) - '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6)) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + porto: 0.2.35(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -11110,11 +11182,11 @@ snapshots: - wagmi - zod - '@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))': + '@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.0(@types/react@19.2.14)(react@19.1.0)(use-sync-external-store@1.4.0(react@19.1.0)) optionalDependencies: '@tanstack/query-core': 5.98.0 @@ -11125,7 +11197,7 @@ snapshots: - react - use-sync-external-store - '@walletconnect/core@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@walletconnect/core@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 @@ -11139,7 +11211,7 @@ snapshots: '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.0 - '@walletconnect/utils': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/utils': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.33.0 events: 3.3.0 @@ -11169,7 +11241,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/core@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@walletconnect/core@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 @@ -11183,7 +11255,7 @@ snapshots: '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.1 - '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.33.0 events: 3.3.0 @@ -11217,18 +11289,18 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@reown/appkit': 1.7.8(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/jsonrpc-http-connection': 1.0.8 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1 - '@walletconnect/sign-client': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/sign-client': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.21.1 - '@walletconnect/universal-provider': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) - '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/universal-provider': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -11351,16 +11423,16 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/sign-client@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@walletconnect/sign-client@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@walletconnect/core': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/core': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.0 - '@walletconnect/utils': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/utils': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -11387,16 +11459,16 @@ snapshots: - utf-8-validate - zod - '@walletconnect/sign-client@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@walletconnect/sign-client@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@walletconnect/core': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/core': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.1 - '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -11485,7 +11557,7 @@ snapshots: - ioredis - uploadthing - '@walletconnect/universal-provider@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@walletconnect/universal-provider@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 @@ -11494,9 +11566,9 @@ snapshots: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1 '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/sign-client': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.21.0 - '@walletconnect/utils': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/utils': 2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) es-toolkit: 1.33.0 events: 3.3.0 transitivePeerDependencies: @@ -11525,7 +11597,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/universal-provider@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@walletconnect/universal-provider@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 @@ -11534,9 +11606,9 @@ snapshots: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1 '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/sign-client': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.21.1 - '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + '@walletconnect/utils': 2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) es-toolkit: 1.33.0 events: 3.3.0 transitivePeerDependencies: @@ -11565,7 +11637,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@walletconnect/utils@2.21.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 @@ -11583,7 +11655,7 @@ snapshots: detect-browser: 5.3.0 query-string: 7.1.3 uint8arrays: 3.1.0 - viem: 2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + viem: 2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -11609,7 +11681,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)': + '@walletconnect/utils@2.21.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 @@ -11627,7 +11699,7 @@ snapshots: detect-browser: 5.3.0 query-string: 7.1.3 uint8arrays: 3.1.0 - viem: 2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + viem: 2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -11749,10 +11821,10 @@ snapshots: typescript: 5.9.3 zod: 3.25.76 - abitype@1.0.8(typescript@5.9.3)(zod@4.3.6): + abitype@1.0.8(typescript@5.9.3)(zod@3.25.76): optionalDependencies: typescript: 5.9.3 - zod: 4.3.6 + zod: 3.25.76 abitype@1.2.3(typescript@5.9.3)(zod@3.22.4): optionalDependencies: @@ -12177,6 +12249,8 @@ snapshots: base-x@5.0.1: {} + base32.js@0.1.0: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.10.18: {} @@ -12193,6 +12267,8 @@ snapshots: bignumber.js@10.0.2: {} + bignumber.js@9.3.1: {} + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -12471,6 +12547,8 @@ snapshots: commander@14.0.2: {} + commander@14.0.3: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -12973,7 +13051,7 @@ snapshots: eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.4(jiti@2.6.1)) @@ -13010,7 +13088,7 @@ snapshots: tinyglobby: 0.2.16 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -13024,7 +13102,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -13265,6 +13343,8 @@ snapshots: events@3.3.0: {} + eventsource@2.0.2: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -13392,6 +13472,10 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + feaxios@0.0.23: + dependencies: + is-retry-allowed: 3.0.0 + fecha@4.2.3: {} fflate@0.8.2: {} @@ -13857,6 +13941,8 @@ snapshots: is-retry-allowed@2.2.0: {} + is-retry-allowed@3.0.0: {} + is-set@2.0.3: {} is-shared-array-buffer@1.0.4: @@ -15058,28 +15144,28 @@ snapshots: transitivePeerDependencies: - zod - ox@0.6.7(typescript@5.9.3)(zod@4.3.6): + ox@0.6.7(typescript@5.9.3)(zod@3.25.76): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.0.8(typescript@5.9.3)(zod@4.3.6) + abitype: 1.0.8(typescript@5.9.3)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - zod - ox@0.6.9(typescript@5.9.3)(zod@4.3.6): + ox@0.6.9(typescript@5.9.3)(zod@3.25.76): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) + abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -15213,21 +15299,21 @@ snapshots: pony-cause@2.1.11: {} - porto@0.2.35(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6)): + porto@0.2.35(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): dependencies: - '@wagmi/core': 2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) hono: 4.12.12 idb-keyval: 6.2.2 mipd: 0.0.7(typescript@5.9.3) ox: 0.9.17(typescript@5.9.3)(zod@4.3.6) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zod: 4.3.6 zustand: 5.0.12(@types/react@19.2.14)(react@19.1.0)(use-sync-external-store@1.4.0(react@19.1.0)) optionalDependencies: '@tanstack/react-query': 5.98.0(react@19.1.0) react: 19.1.0 typescript: 5.9.3 - wagmi: 2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6) + wagmi: 2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) transitivePeerDependencies: - '@types/react' - immer @@ -15399,6 +15485,10 @@ snapshots: radix3@1.1.2: {} + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + range-parser@1.2.1: {} rave-level@1.0.0: @@ -16484,6 +16574,8 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + toml@3.0.0: {} + tr46@0.0.3: {} treeify@1.1.0: {} @@ -16759,6 +16851,8 @@ snapshots: dependencies: punycode: 2.3.1 + urijs@1.19.11: {} + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.1.0): dependencies: react: 19.1.0 @@ -16829,15 +16923,15 @@ snapshots: vary@1.1.2: {} - viem@2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6): + viem@2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76): dependencies: '@noble/curves': 1.8.1 '@noble/hashes': 1.7.1 '@scure/bip32': 1.6.2 '@scure/bip39': 1.5.4 - abitype: 1.0.8(typescript@5.9.3)(zod@4.3.6) + abitype: 1.0.8(typescript@5.9.3)(zod@3.25.76) isows: 1.0.6(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - ox: 0.6.7(typescript@5.9.3)(zod@4.3.6) + ox: 0.6.7(typescript@5.9.3)(zod@3.25.76) ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.9.3 @@ -16897,14 +16991,14 @@ snapshots: - utf-8-validate - zod - wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6): + wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76): dependencies: '@tanstack/react-query': 5.98.0(react@19.1.0) - '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)))(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6))(zod@4.3.6))(zod@4.3.6) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)) + '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.98.0)(@tanstack/react-query@5.98.0(react@19.1.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.98.0)(@types/react@19.2.14)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.0))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) react: 19.1.0 use-sync-external-store: 1.4.0(react@19.1.0) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: