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 0000000..405c68e Binary files /dev/null and b/apps/backend-relayer/src/providers/stellar/wasm/ad_manager.wasm differ diff --git a/apps/backend-relayer/src/providers/stellar/wasm/merkle_manager.wasm b/apps/backend-relayer/src/providers/stellar/wasm/merkle_manager.wasm new file mode 100644 index 0000000..44a0058 Binary files /dev/null and b/apps/backend-relayer/src/providers/stellar/wasm/merkle_manager.wasm differ 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 0000000..6bf4e49 Binary files /dev/null and b/apps/backend-relayer/src/providers/stellar/wasm/order_portal.wasm differ 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 0000000..8364155 Binary files /dev/null and b/apps/backend-relayer/src/providers/stellar/wasm/test_token.wasm differ diff --git a/apps/backend-relayer/src/providers/stellar/wasm/verifier.wasm b/apps/backend-relayer/src/providers/stellar/wasm/verifier.wasm new file mode 100644 index 0000000..73b5d26 Binary files /dev/null and b/apps/backend-relayer/src/providers/stellar/wasm/verifier.wasm differ diff --git a/apps/backend-relayer/src/providers/viem/localnet.ts b/apps/backend-relayer/src/providers/viem/ethers/localnet.ts similarity index 100% rename from apps/backend-relayer/src/providers/viem/localnet.ts rename to apps/backend-relayer/src/providers/viem/ethers/localnet.ts diff --git a/apps/backend-relayer/src/providers/viem/ethers/typedData.ts b/apps/backend-relayer/src/providers/viem/ethers/typedData.ts index 5b1ae79..b5f971e 100644 --- a/apps/backend-relayer/src/providers/viem/ethers/typedData.ts +++ b/apps/backend-relayer/src/providers/viem/ethers/typedData.ts @@ -4,7 +4,7 @@ import { T_AdManagerOrderParams, T_OrderParams, T_OrderPortalParams, -} from '../types'; +} from '../../../chain-adapters/types'; // Left-pad a 20-byte EVM address to 32 bytes (the cross-chain wire format). // Accepts an already-32-byte hex string and returns it unchanged. Throws on diff --git a/apps/backend-relayer/src/providers/viem/viem.service.ts b/apps/backend-relayer/src/providers/viem/viem.service.ts index 55af6eb..3ec7bf5 100644 --- a/apps/backend-relayer/src/providers/viem/viem.service.ts +++ b/apps/backend-relayer/src/providers/viem/viem.service.ts @@ -39,7 +39,7 @@ import { T_UnlockOrderContractDetails, T_WithdrawFromAdRequest, T_WithdrawFromAdRequestContractDetails, -} from './types'; +} from '../../chain-adapters/types'; import { AD_MANAGER_ABI } from './abis/adManager.abi'; import { ORDER_PORTAL_ABI } from './abis/orderPortal.abi'; import { env } from '@libs/configs'; @@ -48,7 +48,7 @@ import { getTypedHash, verifyTypedData, } from './ethers/typedData'; -import { ethLocalnet, hederaLocalnet } from '../viem/localnet'; +import { ethLocalnet, hederaLocalnet } from './ethers/localnet'; import { MOCK_ERC20_ABI } from './abis/mockERC20.abi'; @Injectable() diff --git a/apps/backend-relayer/test/e2e/ads.e2e-spec.ts b/apps/backend-relayer/test/e2e/ads.e2e-spec.ts index 1a98e83..555f5d3 100644 --- a/apps/backend-relayer/test/e2e/ads.e2e-spec.ts +++ b/apps/backend-relayer/test/e2e/ads.e2e-spec.ts @@ -13,12 +13,7 @@ describe('Ads E2E', () => { 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: