tapimo 0.0.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -16
- package/dist/ApiKey.d.ts +14 -11
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +11 -12
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +207 -0
- package/dist/ApiKeys.d.ts.map +1 -0
- package/dist/ApiKeys.js +282 -0
- package/dist/ApiKeys.js.map +1 -0
- package/dist/App.d.ts +6771 -3819
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +145 -317
- package/dist/App.js.map +1 -1
- package/dist/Handler.d.ts +86 -0
- package/dist/Handler.d.ts.map +1 -0
- package/dist/Handler.js +83 -0
- package/dist/Handler.js.map +1 -0
- package/dist/Scope.d.ts +14 -4
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +16 -4
- package/dist/Scope.js.map +1 -1
- package/dist/Webhooks.d.ts +5 -7
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +11 -4
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/Access.d.ts +1 -3
- package/dist/admin/Access.d.ts.map +1 -1
- package/dist/admin/Access.js +1 -3
- package/dist/admin/Access.js.map +1 -1
- package/dist/admin/App.d.ts +167 -205
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/App.js +2 -4
- package/dist/admin/App.js.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +105 -5
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +75 -9
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/scopes.d.ts +6 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +35 -149
- package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.js +18 -39
- package/dist/admin/apps/verified-tokens.js.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +1 -1
- package/dist/apps/data/App.d.ts +19176 -0
- package/dist/apps/data/App.d.ts.map +1 -0
- package/dist/apps/data/App.js +236 -0
- package/dist/apps/data/App.js.map +1 -0
- package/dist/apps/data/index.d.ts +3 -0
- package/dist/apps/data/index.d.ts.map +1 -0
- package/dist/apps/data/index.js +3 -0
- package/dist/apps/data/index.js.map +1 -0
- package/dist/apps/{activities.d.ts → data/routes/activities.d.ts} +32 -32
- package/dist/apps/{activities.d.ts.map → data/routes/activities.d.ts.map} +1 -1
- package/dist/apps/{activities.js → data/routes/activities.js} +13 -13
- package/dist/apps/data/routes/activities.js.map +1 -0
- package/dist/apps/{addresses.d.ts → data/routes/addresses.d.ts} +22 -22
- package/dist/apps/{addresses.d.ts.map → data/routes/addresses.d.ts.map} +1 -1
- package/dist/apps/{addresses.js → data/routes/addresses.js} +5 -5
- package/dist/apps/data/routes/addresses.js.map +1 -0
- package/dist/apps/{balances.d.ts → data/routes/balances.d.ts} +49 -49
- package/dist/apps/data/routes/balances.d.ts.map +1 -0
- package/dist/apps/{balances.js → data/routes/balances.js} +42 -11
- package/dist/apps/data/routes/balances.js.map +1 -0
- package/dist/apps/{blocks.d.ts → data/routes/blocks.d.ts} +47 -47
- package/dist/apps/data/routes/blocks.d.ts.map +1 -0
- package/dist/apps/{blocks.js → data/routes/blocks.js} +13 -13
- package/dist/apps/data/routes/blocks.js.map +1 -0
- package/dist/apps/{coingecko.d.ts → data/routes/coingecko.d.ts} +183 -183
- package/dist/apps/data/routes/coingecko.d.ts.map +1 -0
- package/dist/apps/{coingecko.js → data/routes/coingecko.js} +13 -13
- package/dist/apps/data/routes/coingecko.js.map +1 -0
- package/dist/apps/{exchanges.d.ts → data/routes/exchanges.d.ts} +460 -460
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -0
- package/dist/apps/{exchanges.js → data/routes/exchanges.js} +19 -19
- package/dist/apps/data/routes/exchanges.js.map +1 -0
- package/dist/apps/{fee-amm.d.ts → data/routes/fee-amm.d.ts} +94 -94
- package/dist/apps/data/routes/fee-amm.d.ts.map +1 -0
- package/dist/apps/{fee-amm.js → data/routes/fee-amm.js} +17 -17
- package/dist/apps/data/routes/fee-amm.js.map +1 -0
- package/dist/apps/{indexer.d.ts → data/routes/indexer.d.ts} +4 -4
- package/dist/apps/data/routes/indexer.d.ts.map +1 -0
- package/dist/apps/{indexer.js → data/routes/indexer.js} +13 -9
- package/dist/apps/data/routes/indexer.js.map +1 -0
- package/dist/apps/{receipts.d.ts → data/routes/receipts.d.ts} +6 -6
- package/dist/apps/data/routes/receipts.d.ts.map +1 -0
- package/dist/apps/{receipts.js → data/routes/receipts.js} +12 -12
- package/dist/apps/data/routes/receipts.js.map +1 -0
- package/dist/apps/{rpc.d.ts → data/routes/rpc.d.ts} +4 -4
- package/dist/apps/data/routes/rpc.d.ts.map +1 -0
- package/dist/apps/{rpc.js → data/routes/rpc.js} +6 -6
- package/dist/apps/data/routes/rpc.js.map +1 -0
- package/dist/apps/{tokenlist.d.ts → data/routes/tokenlist.d.ts} +4 -4
- package/dist/apps/data/routes/tokenlist.d.ts.map +1 -0
- package/dist/apps/{tokenlist.js → data/routes/tokenlist.js} +8 -8
- package/dist/apps/data/routes/tokenlist.js.map +1 -0
- package/dist/apps/{tokens.d.ts → data/routes/tokens.d.ts} +11 -11
- package/dist/apps/data/routes/tokens.d.ts.map +1 -0
- package/dist/apps/{tokens.js → data/routes/tokens.js} +19 -19
- package/dist/apps/data/routes/tokens.js.map +1 -0
- package/dist/apps/{transactions.d.ts → data/routes/transactions.d.ts} +288 -288
- package/dist/apps/data/routes/transactions.d.ts.map +1 -0
- package/dist/apps/{transactions.js → data/routes/transactions.js} +13 -13
- package/dist/apps/data/routes/transactions.js.map +1 -0
- package/dist/apps/{transfers.d.ts → data/routes/transfers.d.ts} +35 -35
- package/dist/apps/data/routes/transfers.d.ts.map +1 -0
- package/dist/apps/{transfers.js → data/routes/transfers.js} +13 -13
- package/dist/apps/data/routes/transfers.js.map +1 -0
- package/dist/apps/{verified-tokens.d.ts → data/routes/verified-tokens.d.ts} +42 -42
- package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -0
- package/dist/apps/{verified-tokens.js → data/routes/verified-tokens.js} +9 -9
- package/dist/apps/data/routes/verified-tokens.js.map +1 -0
- package/dist/apps/{webhooks.d.ts → data/routes/webhooks.d.ts} +66 -63
- package/dist/apps/{webhooks.d.ts.map → data/routes/webhooks.d.ts.map} +1 -1
- package/dist/apps/{webhooks.js → data/routes/webhooks.js} +67 -59
- package/dist/apps/data/routes/webhooks.js.map +1 -0
- package/dist/apps/docs/App.d.ts +27 -0
- package/dist/apps/docs/App.d.ts.map +1 -0
- package/dist/apps/docs/App.js +122 -0
- package/dist/apps/docs/App.js.map +1 -0
- package/dist/{internal/OpenApi.fonts.d.ts → apps/docs/fonts.d.ts} +1 -1
- package/dist/apps/docs/fonts.d.ts.map +1 -0
- package/dist/{internal/OpenApi.fonts.js → apps/docs/fonts.js} +1 -1
- package/dist/apps/docs/fonts.js.map +1 -0
- package/dist/apps/docs/index.d.ts +2 -0
- package/dist/apps/docs/index.d.ts.map +1 -0
- package/dist/apps/docs/index.js +2 -0
- package/dist/apps/docs/index.js.map +1 -0
- package/dist/{internal/OpenApi.logo.d.ts → apps/docs/logo.d.ts} +1 -1
- package/dist/apps/docs/logo.d.ts.map +1 -0
- package/dist/{internal/OpenApi.logo.js → apps/docs/logo.js} +1 -1
- package/dist/apps/docs/logo.js.map +1 -0
- package/dist/apps/index.d.ts +4 -0
- package/dist/apps/index.d.ts.map +1 -0
- package/dist/apps/index.js +4 -0
- package/dist/apps/index.js.map +1 -0
- package/dist/apps/metadata.d.ts +7 -0
- package/dist/apps/metadata.d.ts.map +1 -0
- package/dist/apps/metadata.js +15 -0
- package/dist/apps/metadata.js.map +1 -0
- package/dist/apps/platform/App.d.ts +2985 -0
- package/dist/apps/platform/App.d.ts.map +1 -0
- package/dist/apps/platform/App.js +74 -0
- package/dist/apps/platform/App.js.map +1 -0
- package/dist/apps/platform/index.d.ts +2 -0
- package/dist/apps/platform/index.d.ts.map +1 -0
- package/dist/apps/platform/index.js +2 -0
- package/dist/apps/platform/index.js.map +1 -0
- package/dist/apps/platform/routes/api-keys.d.ts +681 -0
- package/dist/apps/platform/routes/api-keys.d.ts.map +1 -0
- package/dist/apps/platform/routes/api-keys.js +328 -0
- package/dist/apps/platform/routes/api-keys.js.map +1 -0
- package/dist/apps/platform/routes/invitations.d.ts +733 -0
- package/dist/apps/platform/routes/invitations.d.ts.map +1 -0
- package/dist/apps/platform/routes/invitations.js +413 -0
- package/dist/apps/platform/routes/invitations.js.map +1 -0
- package/dist/apps/platform/routes/me.d.ts +95 -0
- package/dist/apps/platform/routes/me.d.ts.map +1 -0
- package/dist/apps/platform/routes/me.js +81 -0
- package/dist/apps/platform/routes/me.js.map +1 -0
- package/dist/apps/platform/routes/members.d.ts +554 -0
- package/dist/apps/platform/routes/members.d.ts.map +1 -0
- package/dist/apps/platform/routes/members.js +251 -0
- package/dist/apps/platform/routes/members.js.map +1 -0
- package/dist/apps/platform/routes/orgs.d.ts +582 -0
- package/dist/apps/platform/routes/orgs.d.ts.map +1 -0
- package/dist/apps/platform/routes/orgs.js +265 -0
- package/dist/apps/platform/routes/orgs.js.map +1 -0
- package/dist/apps/platform/routes/projects.d.ts +740 -0
- package/dist/apps/platform/routes/projects.d.ts.map +1 -0
- package/dist/apps/platform/routes/projects.js +286 -0
- package/dist/apps/platform/routes/projects.js.map +1 -0
- package/dist/apps/relay/App.d.ts +41 -0
- package/dist/apps/relay/App.d.ts.map +1 -0
- package/dist/apps/relay/App.js +103 -0
- package/dist/apps/relay/App.js.map +1 -0
- package/dist/apps/relay/Sponsorships.d.ts +107 -0
- package/dist/apps/relay/Sponsorships.d.ts.map +1 -0
- package/dist/apps/relay/Sponsorships.js +193 -0
- package/dist/apps/relay/Sponsorships.js.map +1 -0
- package/dist/apps/relay/index.d.ts +3 -0
- package/dist/apps/relay/index.d.ts.map +1 -0
- package/dist/apps/relay/index.js +3 -0
- package/dist/apps/relay/index.js.map +1 -0
- package/dist/{Cli.d.ts → cli/Cli.d.ts} +13 -2
- package/dist/cli/Cli.d.ts.map +1 -0
- package/dist/cli/Cli.js +19 -0
- package/dist/cli/Cli.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +4 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/commands/admin.d.ts +15 -0
- package/dist/cli/commands/admin.d.ts.map +1 -0
- package/dist/cli/commands/admin.js +60 -0
- package/dist/cli/commands/admin.js.map +1 -0
- package/dist/cloudflare.d.ts +7 -0
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +9 -0
- package/dist/cloudflare.js.map +1 -1
- package/dist/db/Db.d.ts +113 -0
- package/dist/db/Db.d.ts.map +1 -0
- package/dist/db/Db.js +402 -0
- package/dist/db/Db.js.map +1 -0
- package/dist/db/Schema.d.ts +168 -0
- package/dist/db/Schema.d.ts.map +1 -0
- package/dist/db/Schema.js +229 -0
- package/dist/db/Schema.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +84 -0
- package/dist/db/tables/invitations.d.ts.map +1 -0
- package/dist/db/tables/invitations.js +165 -0
- package/dist/db/tables/invitations.js.map +1 -0
- package/dist/db/tables/memberships.d.ts +108 -0
- package/dist/db/tables/memberships.d.ts.map +1 -0
- package/dist/db/tables/memberships.js +153 -0
- package/dist/db/tables/memberships.js.map +1 -0
- package/dist/db/tables/organizations.d.ts +123 -0
- package/dist/db/tables/organizations.d.ts.map +1 -0
- package/dist/db/tables/organizations.js +170 -0
- package/dist/db/tables/organizations.js.map +1 -0
- package/dist/db/tables/projects.d.ts +65 -0
- package/dist/db/tables/projects.d.ts.map +1 -0
- package/dist/db/tables/projects.js +75 -0
- package/dist/db/tables/projects.js.map +1 -0
- package/dist/db/tables/sponsoredTransactions.d.ts +120 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -0
- package/dist/db/tables/sponsoredTransactions.js +161 -0
- package/dist/db/tables/sponsoredTransactions.js.map +1 -0
- package/dist/db/tables/users.d.ts +48 -0
- package/dist/db/tables/users.d.ts.map +1 -0
- package/dist/db/tables/users.js +65 -0
- package/dist/db/tables/users.js.map +1 -0
- package/dist/db/tables/verifiedTokens.d.ts +53 -0
- package/dist/db/tables/verifiedTokens.d.ts.map +1 -0
- package/dist/db/tables/verifiedTokens.js +74 -0
- package/dist/db/tables/verifiedTokens.js.map +1 -0
- package/dist/db/tables/webhookDeliveries.d.ts +71 -0
- package/dist/db/tables/webhookDeliveries.d.ts.map +1 -0
- package/dist/db/tables/webhookDeliveries.js +119 -0
- package/dist/db/tables/webhookDeliveries.js.map +1 -0
- package/dist/db/tables/webhookSubscriptions.d.ts +158 -0
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -0
- package/dist/db/tables/webhookSubscriptions.js +272 -0
- package/dist/db/tables/webhookSubscriptions.js.map +1 -0
- package/dist/handlers/internal/executionError.d.ts +25 -0
- package/dist/handlers/internal/executionError.d.ts.map +1 -0
- package/dist/handlers/internal/executionError.js +186 -0
- package/dist/handlers/internal/executionError.js.map +1 -0
- package/dist/handlers/internal/multisig.d.ts +233 -0
- package/dist/handlers/internal/multisig.d.ts.map +1 -0
- package/dist/handlers/internal/multisig.js +542 -0
- package/dist/handlers/internal/multisig.js.map +1 -0
- package/dist/handlers/internal/requestListener.d.ts +124 -0
- package/dist/handlers/internal/requestListener.d.ts.map +1 -0
- package/dist/handlers/internal/requestListener.js +173 -0
- package/dist/handlers/internal/requestListener.js.map +1 -0
- package/dist/handlers/internal/sponsorship.d.ts +113 -0
- package/dist/handlers/internal/sponsorship.d.ts.map +1 -0
- package/dist/handlers/internal/sponsorship.js +128 -0
- package/dist/handlers/internal/sponsorship.js.map +1 -0
- package/dist/handlers/internal/types.d.ts +284 -0
- package/dist/handlers/internal/types.d.ts.map +1 -0
- package/dist/handlers/internal/types.js +2 -0
- package/dist/handlers/internal/types.js.map +1 -0
- package/dist/handlers/internal/utils.d.ts +34 -0
- package/dist/handlers/internal/utils.d.ts.map +1 -0
- package/dist/handlers/internal/utils.js +131 -0
- package/dist/handlers/internal/utils.js.map +1 -0
- package/dist/handlers/relay.d.ts +123 -0
- package/dist/handlers/relay.d.ts.map +1 -0
- package/dist/handlers/relay.js +1278 -0
- package/dist/handlers/relay.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/Auth.d.ts +224 -16
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +572 -33
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Log.d.ts +7 -3
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +4 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +0 -19
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +0 -57
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/RpcSchema.d.ts +10 -0
- package/dist/internal/RpcSchema.d.ts.map +1 -0
- package/dist/internal/RpcSchema.js +2 -0
- package/dist/internal/RpcSchema.js.map +1 -0
- package/dist/internal/Store.d.ts +49 -4
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +81 -5
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/VerifiedTokens.d.ts +15 -34
- package/dist/internal/VerifiedTokens.d.ts.map +1 -1
- package/dist/internal/VerifiedTokens.js +115 -159
- package/dist/internal/VerifiedTokens.js.map +1 -1
- package/dist/internal/Viem.d.ts +84 -0
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Webhooks.d.ts +50 -36
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +92 -205
- package/dist/internal/Webhooks.js.map +1 -1
- package/package.json +37 -6
- package/src/ApiKey.test.ts +3 -3
- package/src/ApiKey.ts +11 -12
- package/src/ApiKeys.test.ts +278 -0
- package/src/ApiKeys.ts +358 -0
- package/src/App.test-d.ts +1 -1
- package/src/App.test.ts +40508 -19083
- package/src/App.ts +239 -416
- package/src/Client.test-d.ts +307 -16
- package/src/Handler.ts +154 -0
- package/src/Scope.ts +17 -4
- package/src/Webhooks.test.ts +8 -7
- package/src/Webhooks.ts +13 -7
- package/src/admin/Access.ts +1 -3
- package/src/admin/App.ts +19 -24
- package/src/admin/apps/api-keys.test.ts +70 -18
- package/src/admin/apps/api-keys.ts +92 -9
- package/src/admin/apps/scopes.test.ts +8 -0
- package/src/admin/apps/verified-tokens.test.ts +3 -30
- package/src/admin/apps/verified-tokens.ts +18 -37
- package/src/admin/ui.generated.ts +1 -1
- package/src/apps/composition.test.ts +66 -0
- package/src/apps/data/App.ts +295 -0
- package/src/apps/data/index.ts +2 -0
- package/src/apps/{activities.test.ts → data/routes/activities.test.ts} +3 -3
- package/src/apps/{activities.ts → data/routes/activities.ts} +15 -15
- package/src/apps/{addresses.ts → data/routes/addresses.ts} +6 -6
- package/src/apps/{balances.test.ts → data/routes/balances.test.ts} +6 -5
- package/src/apps/{balances.ts → data/routes/balances.ts} +46 -12
- package/src/apps/{blocks.test.ts → data/routes/blocks.test.ts} +4 -4
- package/src/apps/{blocks.ts → data/routes/blocks.ts} +14 -14
- package/src/apps/{coingecko.test.ts → data/routes/coingecko.test.ts} +2 -2
- package/src/apps/{coingecko.ts → data/routes/coingecko.ts} +14 -14
- package/src/apps/{exchanges.test.ts → data/routes/exchanges.test.ts} +2 -2
- package/src/apps/{exchanges.ts → data/routes/exchanges.ts} +20 -20
- package/src/apps/{fee-amm.test.ts → data/routes/fee-amm.test.ts} +3 -3
- package/src/apps/{fee-amm.ts → data/routes/fee-amm.ts} +18 -18
- package/src/apps/{indexer.test.ts → data/routes/indexer.test.ts} +27 -6
- package/src/apps/{indexer.ts → data/routes/indexer.ts} +13 -10
- package/src/apps/{receipts.test.ts → data/routes/receipts.test.ts} +4 -4
- package/src/apps/{receipts.ts → data/routes/receipts.ts} +13 -13
- package/src/apps/{rpc.test.ts → data/routes/rpc.test.ts} +142 -13
- package/src/apps/{rpc.ts → data/routes/rpc.ts} +7 -7
- package/src/apps/{tokenlist.test.ts → data/routes/tokenlist.test.ts} +8 -10
- package/src/apps/{tokenlist.ts → data/routes/tokenlist.ts} +9 -9
- package/src/apps/{tokens.test.ts → data/routes/tokens.test.ts} +6 -5
- package/src/apps/{tokens.ts → data/routes/tokens.ts} +20 -20
- package/src/apps/{transactions.test.ts → data/routes/transactions.test.ts} +4 -4
- package/src/apps/{transactions.ts → data/routes/transactions.ts} +15 -15
- package/src/apps/{transfers.test.ts → data/routes/transfers.test.ts} +3 -3
- package/src/apps/{transfers.ts → data/routes/transfers.ts} +15 -15
- package/src/apps/{verified-tokens.test.ts → data/routes/verified-tokens.test.ts} +27 -15
- package/src/apps/{verified-tokens.ts → data/routes/verified-tokens.ts} +10 -10
- package/src/apps/{webhooks.test.ts → data/routes/webhooks.test.ts} +137 -150
- package/src/apps/{webhooks.ts → data/routes/webhooks.ts} +83 -81
- package/src/apps/docs/App.ts +136 -0
- package/src/apps/docs/index.ts +1 -0
- package/src/apps/index.ts +3 -0
- package/src/apps/metadata.ts +23 -0
- package/src/apps/platform/App.ts +130 -0
- package/src/apps/platform/index.ts +1 -0
- package/src/apps/platform/routes/api-keys.test.ts +330 -0
- package/src/apps/platform/routes/api-keys.ts +427 -0
- package/src/apps/platform/routes/invitations.test.ts +361 -0
- package/src/apps/platform/routes/invitations.ts +519 -0
- package/src/apps/platform/routes/me.test.ts +121 -0
- package/src/apps/platform/routes/me.ts +106 -0
- package/src/apps/platform/routes/members.test.ts +195 -0
- package/src/apps/platform/routes/members.ts +313 -0
- package/src/apps/platform/routes/orgs.test.ts +313 -0
- package/src/apps/platform/routes/orgs.ts +343 -0
- package/src/apps/platform/routes/projects.test.ts +181 -0
- package/src/apps/platform/routes/projects.ts +367 -0
- package/src/apps/relay/App.test.ts +484 -0
- package/src/apps/relay/App.ts +128 -0
- package/src/apps/relay/Sponsorships.test.ts +375 -0
- package/src/apps/relay/Sponsorships.ts +305 -0
- package/src/apps/relay/index.ts +2 -0
- package/src/{Cli.test.ts → cli/Cli.test.ts} +10 -9
- package/src/{Cli.ts → cli/Cli.ts} +16 -9
- package/src/cli/bin.ts +4 -0
- package/src/cli/commands/admin.test.ts +71 -0
- package/src/cli/commands/admin.ts +60 -0
- package/src/cloudflare.ts +10 -0
- package/src/db/Db.test.ts +84 -0
- package/src/db/Db.ts +542 -0
- package/src/db/Schema.ts +301 -0
- package/src/db/tables/invitations.ts +198 -0
- package/src/db/tables/memberships.ts +208 -0
- package/src/db/tables/organizations.test.ts +160 -0
- package/src/db/tables/organizations.ts +232 -0
- package/src/db/tables/projects.ts +107 -0
- package/src/db/tables/sponsoredTransactions.test.ts +218 -0
- package/src/db/tables/sponsoredTransactions.ts +236 -0
- package/src/db/tables/users.ts +85 -0
- package/src/db/tables/verifiedTokens.ts +111 -0
- package/src/db/tables/webhookDeliveries.ts +159 -0
- package/src/db/tables/webhookSubscriptions.ts +358 -0
- package/src/handlers/internal/executionError.ts +207 -0
- package/src/handlers/internal/multisig.test.ts +818 -0
- package/src/handlers/internal/multisig.ts +823 -0
- package/src/handlers/internal/requestListener.ts +273 -0
- package/src/handlers/internal/sponsorship.ts +245 -0
- package/src/handlers/internal/types.ts +378 -0
- package/src/handlers/internal/utils.ts +153 -0
- package/src/handlers/relay.test.ts +2035 -0
- package/src/handlers/relay.ts +1623 -0
- package/src/index.ts +4 -2
- package/src/internal/Auth.test.ts +337 -21
- package/src/internal/Auth.ts +783 -44
- package/src/internal/Cursor.test.ts +3 -2
- package/src/internal/Log.test.ts +1 -1
- package/src/internal/Log.ts +11 -3
- package/src/internal/MetricSink.ts +1 -1
- package/src/internal/OpenApi.ts +0 -59
- package/src/internal/RateLimit.test.ts +1 -1
- package/src/internal/RpcSchema.ts +10 -0
- package/src/internal/Store.test.ts +150 -10
- package/src/internal/Store.ts +136 -11
- package/src/internal/Tidx.local.test.ts +1 -1
- package/src/internal/VerifiedTokens.test.ts +99 -115
- package/src/internal/VerifiedTokens.ts +123 -175
- package/src/internal/WebhookDestination.test.ts +1 -1
- package/src/internal/Webhooks.test.ts +111 -111
- package/src/internal/Webhooks.ts +103 -221
- package/dist/ApiKeySource.d.ts +0 -166
- package/dist/ApiKeySource.d.ts.map +0 -1
- package/dist/ApiKeySource.js +0 -234
- package/dist/ApiKeySource.js.map +0 -1
- package/dist/Cli.d.ts.map +0 -1
- package/dist/Cli.js +0 -13
- package/dist/Cli.js.map +0 -1
- package/dist/apps/activities.js.map +0 -1
- package/dist/apps/addresses.js.map +0 -1
- package/dist/apps/balances.d.ts.map +0 -1
- package/dist/apps/balances.js.map +0 -1
- package/dist/apps/blocks.d.ts.map +0 -1
- package/dist/apps/blocks.js.map +0 -1
- package/dist/apps/coingecko.d.ts.map +0 -1
- package/dist/apps/coingecko.js.map +0 -1
- package/dist/apps/exchanges.d.ts.map +0 -1
- package/dist/apps/exchanges.js.map +0 -1
- package/dist/apps/fee-amm.d.ts.map +0 -1
- package/dist/apps/fee-amm.js.map +0 -1
- package/dist/apps/indexer.d.ts.map +0 -1
- package/dist/apps/indexer.js.map +0 -1
- package/dist/apps/receipts.d.ts.map +0 -1
- package/dist/apps/receipts.js.map +0 -1
- package/dist/apps/rpc.d.ts.map +0 -1
- package/dist/apps/rpc.js.map +0 -1
- package/dist/apps/tokenlist.d.ts.map +0 -1
- package/dist/apps/tokenlist.js.map +0 -1
- package/dist/apps/tokens.d.ts.map +0 -1
- package/dist/apps/tokens.js.map +0 -1
- package/dist/apps/transactions.d.ts.map +0 -1
- package/dist/apps/transactions.js.map +0 -1
- package/dist/apps/transfers.d.ts.map +0 -1
- package/dist/apps/transfers.js.map +0 -1
- package/dist/apps/verified-tokens.d.ts.map +0 -1
- package/dist/apps/verified-tokens.js.map +0 -1
- package/dist/apps/webhooks.js.map +0 -1
- package/dist/internal/OpenApi.fonts.d.ts.map +0 -1
- package/dist/internal/OpenApi.fonts.js.map +0 -1
- package/dist/internal/OpenApi.logo.d.ts.map +0 -1
- package/dist/internal/OpenApi.logo.js.map +0 -1
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000000.svg +0 -1
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000001.svg +0 -1
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000002.svg +0 -1
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000003.svg +0 -1
- package/seeds/31318/verified-tokens.json +0 -30
- package/seeds/4217/icons/0x20c0000000000000000000000000000000000000.svg +0 -25
- package/seeds/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000000a6da882d075a4c3.svg +0 -4
- package/seeds/4217/icons/0x20c000000000000000000000111111111e910f0f.svg +0 -30
- package/seeds/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000003158081efd85bfc2.svg +0 -11
- package/seeds/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg +0 -1
- package/seeds/4217/icons/0x20c00000000000000000000042109aef2f8b28e1.png +0 -0
- package/seeds/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000006fd9a167923ba194.png +0 -0
- package/seeds/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000008191667423f70e67.svg +0 -5
- package/seeds/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000c412ec89d0c08be5.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000f047dd7018e50367.png +0 -0
- package/seeds/4217/verified-tokens.json +0 -177
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000000.svg +0 -25
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000001.svg +0 -1
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000002.svg +0 -1
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000003.svg +0 -1
- package/seeds/42431/icons/0x20c0000000000000000000009e8d7eb59b783726.svg +0 -1
- package/seeds/42431/icons/0x20c000000000000000000000d72572838bbee59c.svg +0 -1
- package/seeds/42431/verified-tokens.json +0 -44
- package/seeds/openrpc/README.md +0 -27
- package/seeds/openrpc/execution-apis.json +0 -8989
- package/src/ApiKeySource.test-d.ts +0 -11
- package/src/ApiKeySource.test.ts +0 -295
- package/src/ApiKeySource.ts +0 -303
- /package/src/{internal/OpenApi.fonts.ts → apps/docs/fonts.ts} +0 -0
- /package/src/{internal/OpenApi.logo.ts → apps/docs/logo.ts} +0 -0
|
@@ -0,0 +1,2035 @@
|
|
|
1
|
+
import type { RpcRequest } from 'ox'
|
|
2
|
+
import { SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'
|
|
3
|
+
import { parseUnits, type Address, type BaseError } from 'viem'
|
|
4
|
+
import { fillTransaction, sendTransactionSync } from 'viem/actions'
|
|
5
|
+
import { Actions, Addresses, Capabilities, Tick, Transaction, VirtualAddress } from 'viem/tempo'
|
|
6
|
+
import { tempo, tempoModerato } from 'viem/tempo/chains'
|
|
7
|
+
|
|
8
|
+
import * as Relay from '../../test/Relay.js'
|
|
9
|
+
import * as Runtime from '../../test/runtime.js'
|
|
10
|
+
|
|
11
|
+
type Server = Relay.Server
|
|
12
|
+
|
|
13
|
+
const userAccount = Relay.accounts[9]!
|
|
14
|
+
const feePayerAccount = Relay.accounts[0]!
|
|
15
|
+
const recipient = Relay.accounts[7]!
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Tokens the relay handler probes for fee-token resolution. The default
|
|
19
|
+
* `resolveTokens` fetches `tokenlist.tempo.xyz`, which doesn't know about
|
|
20
|
+
* the localnet chain — so tests inject this list explicitly.
|
|
21
|
+
*/
|
|
22
|
+
const localnetTokens = [
|
|
23
|
+
{
|
|
24
|
+
address: '0x20c0000000000000000000000000000000000000',
|
|
25
|
+
decimals: 6,
|
|
26
|
+
name: 'pathUSD',
|
|
27
|
+
symbol: 'pathUSD',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
address: '0x20c0000000000000000000000000000000000001',
|
|
31
|
+
decimals: 6,
|
|
32
|
+
name: 'alphaUSD',
|
|
33
|
+
symbol: 'alphaUSD',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
address: '0x20c0000000000000000000000000000000000002',
|
|
37
|
+
decimals: 6,
|
|
38
|
+
name: 'betaUSD',
|
|
39
|
+
symbol: 'betaUSD',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
address: '0x20c0000000000000000000000000000000000003',
|
|
43
|
+
decimals: 6,
|
|
44
|
+
name: 'thetaUSD',
|
|
45
|
+
symbol: 'thetaUSD',
|
|
46
|
+
},
|
|
47
|
+
] as const
|
|
48
|
+
|
|
49
|
+
/** Case-insensitive lookup into balanceDiffs keyed by address. */
|
|
50
|
+
function findDiffs(
|
|
51
|
+
balanceDiffs: Capabilities.FillTransactionCapabilities['balanceDiffs'],
|
|
52
|
+
address: string,
|
|
53
|
+
) {
|
|
54
|
+
return Object.entries(balanceDiffs ?? {}).find(
|
|
55
|
+
([addr]) => addr.toLowerCase() === address.toLowerCase(),
|
|
56
|
+
)?.[1]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Extracts relay virtual-address metadata while viem's public type catches up. */
|
|
60
|
+
function virtualAddresses(capabilities: Capabilities.FillTransactionCapabilities | undefined) {
|
|
61
|
+
return (
|
|
62
|
+
capabilities as
|
|
63
|
+
| (Capabilities.FillTransactionCapabilities & {
|
|
64
|
+
virtualAddresses?: Record<Address, Address | null> | undefined
|
|
65
|
+
})
|
|
66
|
+
| undefined
|
|
67
|
+
)?.virtualAddresses
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Client used only to build typed calldata (`Actions.token.*.call`). */
|
|
71
|
+
const caller = Relay.getClient()
|
|
72
|
+
|
|
73
|
+
/** A simple transfer call for tests that just need a valid transaction. */
|
|
74
|
+
const transferCall = () =>
|
|
75
|
+
Actions.token.transfer.call(caller, {
|
|
76
|
+
token: Relay.addresses.alphaUsd,
|
|
77
|
+
to: recipient.address,
|
|
78
|
+
amount: 1n,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
beforeAll(async () => {
|
|
82
|
+
// Faucet-fund the accounts that pay fees or create fresh tokens. The faucet is
|
|
83
|
+
// permissionless, so no genesis privileges are required. Left unfunded on
|
|
84
|
+
// purpose: accounts[5]/[10] (balance-sensitive fee-resolution and
|
|
85
|
+
// insufficient-balance tests) and accounts[7] (recipient only).
|
|
86
|
+
await Promise.all(
|
|
87
|
+
[0, 2, 3, 4, 6, 8, 9].map((index) =>
|
|
88
|
+
Actions.faucet.fundSync(Relay.getClient(), {
|
|
89
|
+
account: Relay.accounts[index]!,
|
|
90
|
+
timeout: 60_000,
|
|
91
|
+
}),
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
// userAccount prefers alphaUsd (a faucet-funded genesis token) as its fee token.
|
|
95
|
+
await Actions.fee.setUserToken(Relay.getClient(), {
|
|
96
|
+
account: userAccount,
|
|
97
|
+
token: Relay.addresses.alphaUsd,
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe.skipIf(Runtime.get().mode !== 'localnet')('default', () => {
|
|
102
|
+
let client: ReturnType<typeof Relay.getClient>
|
|
103
|
+
let server: Server
|
|
104
|
+
|
|
105
|
+
beforeAll(async () => {
|
|
106
|
+
server = await Relay.createServer((await Relay.relay({})).listener)
|
|
107
|
+
client = Relay.getClient({ url: server.url })
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
afterAll(() => {
|
|
111
|
+
server.close()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('default: returns filled transaction with capabilities', async () => {
|
|
115
|
+
const { transaction } = await fillTransaction(client, {
|
|
116
|
+
account: userAccount.address,
|
|
117
|
+
calls: [transferCall()],
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
expect(transaction.gas).toBeDefined()
|
|
121
|
+
expect(transaction.nonce).toBeDefined()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('behavior: proxies other methods to RPC node', async () => {
|
|
125
|
+
const chainId = await client.request({ method: 'eth_chainId' })
|
|
126
|
+
expect(Number(chainId)).toMatchInlineSnapshot(`${Relay.chain.id}`)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('behavior: surfaces upstream RPC errors as JSON-RPC errors', async () => {
|
|
130
|
+
// grantRoles reverts with Unauthorized when caller is not an admin
|
|
131
|
+
// (eth_call defaults `from` to the zero address). We expect the relay to
|
|
132
|
+
// forward the revert as a structured JSON-RPC error response (HTTP 200
|
|
133
|
+
// with `error.code`/`error.data`), not a 500.
|
|
134
|
+
const call = Actions.token.grantRoles.call(caller, {
|
|
135
|
+
token: Relay.addresses.alphaUsd,
|
|
136
|
+
role: 'issuer',
|
|
137
|
+
to: recipient.address,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const response = await fetch(server.url, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: { 'Content-Type': 'application/json' },
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
jsonrpc: '2.0',
|
|
145
|
+
id: 1,
|
|
146
|
+
method: 'eth_call',
|
|
147
|
+
params: [{ to: call.to, data: call.data }, 'latest'],
|
|
148
|
+
}),
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
expect(response.status).toBe(200)
|
|
152
|
+
const body = (await response.json()) as {
|
|
153
|
+
id: number
|
|
154
|
+
jsonrpc: string
|
|
155
|
+
error: { code: number; message: string; data?: string }
|
|
156
|
+
}
|
|
157
|
+
// Drop `message` from the snapshot — it embeds the upstream RPC URL/port
|
|
158
|
+
// and viem version, which are nondeterministic.
|
|
159
|
+
const { message: _message, ...errorRest } = body.error
|
|
160
|
+
expect({ ...body, error: errorRest }).toMatchInlineSnapshot(`
|
|
161
|
+
{
|
|
162
|
+
"error": {
|
|
163
|
+
"code": 3,
|
|
164
|
+
"data": "0x82b42900",
|
|
165
|
+
},
|
|
166
|
+
"id": 1,
|
|
167
|
+
"jsonrpc": "2.0",
|
|
168
|
+
}
|
|
169
|
+
`)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('behavior: returns actionable error for eth_signRawTransaction without feePayer', async () => {
|
|
173
|
+
const response = await fetch(server.url, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: { 'Content-Type': 'application/json' },
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
jsonrpc: '2.0',
|
|
178
|
+
id: 1,
|
|
179
|
+
method: 'eth_signRawTransaction',
|
|
180
|
+
params: ['0x00'],
|
|
181
|
+
}),
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
expect(response.status).toBe(200)
|
|
185
|
+
const body = (await response.json()) as {
|
|
186
|
+
id: number
|
|
187
|
+
jsonrpc: string
|
|
188
|
+
error: { code: number; message: string }
|
|
189
|
+
}
|
|
190
|
+
expect(body.error.code).toBe(-32601)
|
|
191
|
+
expect(body.error.message).toContain('fee payer')
|
|
192
|
+
expect(body.error.message).toContain('Handler.relay()')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('behavior: handles JSON-RPC batch requests', async () => {
|
|
196
|
+
const response = await fetch(server.url, {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: { 'Content-Type': 'application/json' },
|
|
199
|
+
body: JSON.stringify([
|
|
200
|
+
{ jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] },
|
|
201
|
+
{ jsonrpc: '2.0', id: 2, method: 'eth_chainId', params: [] },
|
|
202
|
+
]),
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
expect(response.status).toBe(200)
|
|
206
|
+
const body = (await response.json()) as { id: number; result: string }[]
|
|
207
|
+
expect(Array.isArray(body)).toBe(true)
|
|
208
|
+
expect(body).toHaveLength(2)
|
|
209
|
+
expect(body[0]!.id).toBe(1)
|
|
210
|
+
expect(body[1]!.id).toBe(2)
|
|
211
|
+
expect(Number(body[0]!.result)).toBe(Relay.chain.id)
|
|
212
|
+
expect(Number(body[1]!.result)).toBe(Relay.chain.id)
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: with feePayer', () => {
|
|
217
|
+
let server: Server
|
|
218
|
+
let client: ReturnType<typeof Relay.getClient>
|
|
219
|
+
let requests: RpcRequest.RpcRequest[] = []
|
|
220
|
+
|
|
221
|
+
beforeAll(async () => {
|
|
222
|
+
server = await Relay.createServer(
|
|
223
|
+
(
|
|
224
|
+
await Relay.relay({
|
|
225
|
+
feePayer: {
|
|
226
|
+
account: feePayerAccount,
|
|
227
|
+
name: 'Test Sponsor',
|
|
228
|
+
url: 'https://test.com',
|
|
229
|
+
},
|
|
230
|
+
onRequest: async (request) => {
|
|
231
|
+
requests.push(request)
|
|
232
|
+
},
|
|
233
|
+
})
|
|
234
|
+
).listener,
|
|
235
|
+
)
|
|
236
|
+
client = Relay.getClient({ url: server.url })
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
afterAll(() => {
|
|
240
|
+
server.close()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
afterEach(() => {
|
|
244
|
+
requests = []
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('default: returns sponsored tx with feePayerSignature', async () => {
|
|
248
|
+
const { transaction } = await fillTransaction(client, {
|
|
249
|
+
account: userAccount.address,
|
|
250
|
+
calls: [transferCall()],
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
expect(transaction.feePayerSignature).toBeDefined()
|
|
254
|
+
expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
|
|
255
|
+
[
|
|
256
|
+
"eth_fillTransaction",
|
|
257
|
+
]
|
|
258
|
+
`)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test('behavior: returns sponsor capabilities', async () => {
|
|
262
|
+
const result = await fillTransaction(client, {
|
|
263
|
+
account: userAccount.address,
|
|
264
|
+
calls: [transferCall()],
|
|
265
|
+
})
|
|
266
|
+
const meta = result.capabilities
|
|
267
|
+
|
|
268
|
+
expect(meta?.['sponsored']).toBe(true)
|
|
269
|
+
expect(meta?.['sponsor']).toMatchInlineSnapshot(`
|
|
270
|
+
{
|
|
271
|
+
"address": "${feePayerAccount.address}",
|
|
272
|
+
"name": "Test Sponsor",
|
|
273
|
+
"url": "https://test.com",
|
|
274
|
+
}
|
|
275
|
+
`)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
test('behavior: sponsored tx can be signed and broadcast', async () => {
|
|
279
|
+
const { transaction } = await fillTransaction(client, {
|
|
280
|
+
account: userAccount.address,
|
|
281
|
+
calls: [transferCall()],
|
|
282
|
+
})
|
|
283
|
+
const signed = await userAccount.signTransaction(transaction as never)
|
|
284
|
+
const receipt = (await Relay.getClient().request({
|
|
285
|
+
method: 'eth_sendRawTransactionSync' as never,
|
|
286
|
+
params: [signed],
|
|
287
|
+
})) as { feePayer?: string | undefined }
|
|
288
|
+
|
|
289
|
+
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('behavior: missing from returns error capability when errors capability is enabled', async () => {
|
|
293
|
+
const result = await fillTransaction(client, {
|
|
294
|
+
calls: [transferCall()],
|
|
295
|
+
capabilities: { errors: true },
|
|
296
|
+
})
|
|
297
|
+
expect(result.capabilities).toMatchInlineSnapshot(`
|
|
298
|
+
{
|
|
299
|
+
"error": {
|
|
300
|
+
"errorName": "unknown",
|
|
301
|
+
"message": "unknown account",
|
|
302
|
+
},
|
|
303
|
+
"sponsored": false,
|
|
304
|
+
}
|
|
305
|
+
`)
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: with feePayer.feeToken', () => {
|
|
310
|
+
let server: Server
|
|
311
|
+
let client: ReturnType<typeof Relay.getClient>
|
|
312
|
+
|
|
313
|
+
const sponsorFeeToken = '0x20c0000000000000000000000000000000000000' as const // pathUSD
|
|
314
|
+
|
|
315
|
+
beforeAll(async () => {
|
|
316
|
+
server = await Relay.createServer(
|
|
317
|
+
(
|
|
318
|
+
await Relay.relay({
|
|
319
|
+
feePayer: {
|
|
320
|
+
account: feePayerAccount,
|
|
321
|
+
feeToken: sponsorFeeToken,
|
|
322
|
+
},
|
|
323
|
+
tokens: localnetTokens,
|
|
324
|
+
})
|
|
325
|
+
).listener,
|
|
326
|
+
)
|
|
327
|
+
client = Relay.getClient({ url: server.url })
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
afterAll(() => {
|
|
331
|
+
server.close()
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
test('default: sponsor.feeToken is used when request omits feeToken', async () => {
|
|
335
|
+
const { transaction } = await fillTransaction(client, {
|
|
336
|
+
account: userAccount.address,
|
|
337
|
+
calls: [transferCall()],
|
|
338
|
+
})
|
|
339
|
+
expect(transaction.feeToken?.toLowerCase()).toBe(sponsorFeeToken)
|
|
340
|
+
expect(transaction.feePayerSignature).toBeDefined()
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
test('behavior: sponsor.feeToken overrides request feeToken on sponsored fills', async () => {
|
|
344
|
+
const { transaction } = await fillTransaction(client, {
|
|
345
|
+
account: userAccount.address,
|
|
346
|
+
calls: [transferCall()],
|
|
347
|
+
feeToken: Relay.addresses.alphaUsd as Address,
|
|
348
|
+
})
|
|
349
|
+
expect(transaction.feeToken?.toLowerCase()).toBe(sponsorFeeToken)
|
|
350
|
+
expect(transaction.feePayerSignature).toBeDefined()
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
test('behavior: request feeToken wins when sponsorship is opted out via feePayer:false', async () => {
|
|
354
|
+
const { transaction } = await fillTransaction(client, {
|
|
355
|
+
account: userAccount.address,
|
|
356
|
+
calls: [transferCall()],
|
|
357
|
+
feePayer: false as never,
|
|
358
|
+
feeToken: Relay.addresses.alphaUsd as Address,
|
|
359
|
+
})
|
|
360
|
+
expect(transaction.feeToken?.toLowerCase()).toBe(Relay.addresses.alphaUsd.toLowerCase())
|
|
361
|
+
expect(transaction.feePayerSignature).toBeUndefined()
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
test('behavior: broadcast tx receipt records the sponsor.feeToken', async () => {
|
|
365
|
+
// Fill via relay (where override kicks in), then sign + broadcast manually.
|
|
366
|
+
// viem's `sendTransactionSync` with a hydrated account does local prep and
|
|
367
|
+
// would skip the relay's `eth_fillTransaction`, bypassing the override.
|
|
368
|
+
const { transaction } = await fillTransaction(client, {
|
|
369
|
+
account: userAccount.address,
|
|
370
|
+
calls: [transferCall()],
|
|
371
|
+
feeToken: Relay.addresses.alphaUsd as Address,
|
|
372
|
+
})
|
|
373
|
+
expect(transaction.feeToken?.toLowerCase()).toBe(sponsorFeeToken)
|
|
374
|
+
|
|
375
|
+
const signed = await userAccount.signTransaction(transaction as never)
|
|
376
|
+
const receipt = (await Relay.getClient().request({
|
|
377
|
+
method: 'eth_sendRawTransactionSync' as never,
|
|
378
|
+
params: [signed],
|
|
379
|
+
})) as { feePayer?: string | undefined; feeToken?: string | undefined }
|
|
380
|
+
|
|
381
|
+
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
|
382
|
+
expect(receipt.feeToken?.toLowerCase()).toBe(sponsorFeeToken)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
test('behavior: raw sponsor signing restores sponsor.feeToken when envelope omits feeToken', async () => {
|
|
386
|
+
const rpc = Relay.getClient({ account: userAccount })
|
|
387
|
+
const { transaction } = await fillTransaction(rpc, {
|
|
388
|
+
account: userAccount.address,
|
|
389
|
+
calls: [transferCall()],
|
|
390
|
+
feePayer: true as never,
|
|
391
|
+
})
|
|
392
|
+
const partial = await userAccount.signTransaction({ ...transaction, feePayer: true } as never)
|
|
393
|
+
expect([null, undefined]).toContain(
|
|
394
|
+
Transaction.deserialize(partial as `0x76${string}`).feeToken,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
const response = await fetch(server.url, {
|
|
398
|
+
method: 'POST',
|
|
399
|
+
headers: { 'Content-Type': 'application/json' },
|
|
400
|
+
body: JSON.stringify({
|
|
401
|
+
jsonrpc: '2.0',
|
|
402
|
+
id: 1,
|
|
403
|
+
method: 'eth_signRawTransaction',
|
|
404
|
+
params: [partial],
|
|
405
|
+
}),
|
|
406
|
+
})
|
|
407
|
+
const body = (await response.json()) as { result: `0x76${string}` }
|
|
408
|
+
const feeToken = Transaction.deserialize(body.result).feeToken as Address | undefined
|
|
409
|
+
|
|
410
|
+
expect(feeToken?.toLowerCase()).toBe(sponsorFeeToken)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test('behavior: raw sponsor signing restores token-list default before validation', async () => {
|
|
414
|
+
let feeToken_validated: Address | undefined
|
|
415
|
+
const customServer = await Relay.createServer(
|
|
416
|
+
(
|
|
417
|
+
await Relay.relay({
|
|
418
|
+
feePayer: {
|
|
419
|
+
account: feePayerAccount,
|
|
420
|
+
validate: (request) => {
|
|
421
|
+
feeToken_validated = request.feeToken as Address | undefined
|
|
422
|
+
return true
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
tokens: localnetTokens,
|
|
426
|
+
})
|
|
427
|
+
).listener,
|
|
428
|
+
)
|
|
429
|
+
const rpc = Relay.getClient({ account: userAccount })
|
|
430
|
+
const { transaction } = await fillTransaction(rpc, {
|
|
431
|
+
account: userAccount.address,
|
|
432
|
+
calls: [transferCall()],
|
|
433
|
+
feePayer: true as never,
|
|
434
|
+
})
|
|
435
|
+
const partial = await userAccount.signTransaction({ ...transaction, feePayer: true } as never)
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
const response = await fetch(customServer.url, {
|
|
439
|
+
method: 'POST',
|
|
440
|
+
headers: { 'Content-Type': 'application/json' },
|
|
441
|
+
body: JSON.stringify({
|
|
442
|
+
jsonrpc: '2.0',
|
|
443
|
+
id: 1,
|
|
444
|
+
method: 'eth_signRawTransaction',
|
|
445
|
+
params: [partial],
|
|
446
|
+
}),
|
|
447
|
+
})
|
|
448
|
+
const body = (await response.json()) as { result: `0x76${string}` }
|
|
449
|
+
const feeToken = Transaction.deserialize(body.result).feeToken as Address | undefined
|
|
450
|
+
|
|
451
|
+
expect(feeToken_validated?.toLowerCase()).toBe(sponsorFeeToken)
|
|
452
|
+
expect(feeToken?.toLowerCase()).toBe(sponsorFeeToken)
|
|
453
|
+
} finally {
|
|
454
|
+
customServer.close()
|
|
455
|
+
}
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
describe.skipIf(Runtime.get().mode !== 'localnet')(
|
|
460
|
+
'behavior: with app-provided feePayer URL',
|
|
461
|
+
() => {
|
|
462
|
+
let appServer: Server
|
|
463
|
+
let walletServer: Server
|
|
464
|
+
let client: ReturnType<typeof Relay.getClient>
|
|
465
|
+
|
|
466
|
+
beforeAll(async () => {
|
|
467
|
+
// App relay: has a fee payer account and signs transactions.
|
|
468
|
+
appServer = await Relay.createServer(
|
|
469
|
+
(
|
|
470
|
+
await Relay.relay({
|
|
471
|
+
feePayer: {
|
|
472
|
+
account: feePayerAccount,
|
|
473
|
+
name: 'App Sponsor',
|
|
474
|
+
url: 'https://app.example.com',
|
|
475
|
+
},
|
|
476
|
+
})
|
|
477
|
+
).listener,
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
// Wallet relay: no fee payer configured — proxies to app relay.
|
|
481
|
+
walletServer = await Relay.createServer(
|
|
482
|
+
(
|
|
483
|
+
await Relay.relay({
|
|
484
|
+
internal_allowUnsafeUrls: true,
|
|
485
|
+
})
|
|
486
|
+
).listener,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
client = Relay.getClient({ url: walletServer.url })
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
afterAll(() => {
|
|
493
|
+
appServer.close()
|
|
494
|
+
walletServer.close()
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
test('default: proxies fill to app relay and returns sponsored tx', async () => {
|
|
498
|
+
const { transaction } = await fillTransaction(client, {
|
|
499
|
+
account: userAccount.address,
|
|
500
|
+
calls: [transferCall()],
|
|
501
|
+
feePayer: appServer.url as never,
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
expect(transaction.feePayerSignature).toBeDefined()
|
|
505
|
+
expect(transaction.gas).toBeDefined()
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
test('behavior: relays sponsor metadata from app relay', async () => {
|
|
509
|
+
const result = await fillTransaction(client, {
|
|
510
|
+
account: userAccount.address,
|
|
511
|
+
calls: [transferCall()],
|
|
512
|
+
feePayer: appServer.url as never,
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
expect(result.capabilities?.['sponsored']).toBe(true)
|
|
516
|
+
expect(result.capabilities?.['sponsor']).toMatchInlineSnapshot(`
|
|
517
|
+
{
|
|
518
|
+
"address": "${feePayerAccount.address}",
|
|
519
|
+
"name": "App Sponsor",
|
|
520
|
+
"url": "https://app.example.com",
|
|
521
|
+
}
|
|
522
|
+
`)
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
test('behavior: sponsored tx from app relay can be signed and broadcast', async () => {
|
|
526
|
+
const { transaction } = await fillTransaction(client, {
|
|
527
|
+
account: userAccount.address,
|
|
528
|
+
calls: [transferCall()],
|
|
529
|
+
feePayer: appServer.url as never,
|
|
530
|
+
})
|
|
531
|
+
const signed = await userAccount.signTransaction(transaction as never)
|
|
532
|
+
const receipt = (await Relay.getClient().request({
|
|
533
|
+
method: 'eth_sendRawTransactionSync' as never,
|
|
534
|
+
params: [signed],
|
|
535
|
+
})) as { feePayer?: string | undefined }
|
|
536
|
+
|
|
537
|
+
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
|
538
|
+
})
|
|
539
|
+
},
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
describe.skipIf(Runtime.get().mode !== 'localnet')(
|
|
543
|
+
'behavior: with app-provided feePayer URL + autoSwap',
|
|
544
|
+
() => {
|
|
545
|
+
let appServer: Server
|
|
546
|
+
let walletServer: Server
|
|
547
|
+
let client: ReturnType<typeof Relay.getClient>
|
|
548
|
+
|
|
549
|
+
beforeAll(async () => {
|
|
550
|
+
// App relay sponsors fees AND has `features: 'all'` so it can recover
|
|
551
|
+
// from InsufficientBalance via autoSwap.
|
|
552
|
+
appServer = await Relay.createServer(
|
|
553
|
+
(
|
|
554
|
+
await Relay.relay({
|
|
555
|
+
features: 'all',
|
|
556
|
+
feePayer: {
|
|
557
|
+
account: feePayerAccount,
|
|
558
|
+
name: 'App Sponsor',
|
|
559
|
+
url: 'https://app.example.com',
|
|
560
|
+
},
|
|
561
|
+
})
|
|
562
|
+
).listener,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
// Wallet relay forwards to the app relay; also has features:'all' so its
|
|
566
|
+
// own fill() can detect upstream `capabilities.error` as InsufficientBalance.
|
|
567
|
+
walletServer = await Relay.createServer(
|
|
568
|
+
(
|
|
569
|
+
await Relay.relay({
|
|
570
|
+
features: 'all',
|
|
571
|
+
internal_allowUnsafeUrls: true,
|
|
572
|
+
})
|
|
573
|
+
).listener,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
client = Relay.getClient({ url: walletServer.url })
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
afterAll(() => {
|
|
580
|
+
appServer.close()
|
|
581
|
+
walletServer.close()
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
test('behavior: autoSwap recovers when external feePayer surfaces InsufficientBalance', async () => {
|
|
585
|
+
const sender = Relay.accounts[6]!
|
|
586
|
+
|
|
587
|
+
// Token pair + DEX liquidity. Use alphaUsd as the quote token so the
|
|
588
|
+
// relay can swap alphaUsd → base to cover the deficit.
|
|
589
|
+
const rpc = Relay.getClient({ account: Relay.accounts[0]! })
|
|
590
|
+
const { token: base } = await Actions.token.createSync(rpc, {
|
|
591
|
+
name: 'External Swap Base',
|
|
592
|
+
symbol: 'EXTBASE',
|
|
593
|
+
currency: 'USD',
|
|
594
|
+
quoteToken: Relay.addresses.alphaUsd,
|
|
595
|
+
})
|
|
596
|
+
await sendTransactionSync(rpc, {
|
|
597
|
+
calls: [
|
|
598
|
+
Actions.token.grantRoles.call(caller, {
|
|
599
|
+
token: base,
|
|
600
|
+
role: 'issuer',
|
|
601
|
+
to: rpc.account!.address,
|
|
602
|
+
}),
|
|
603
|
+
Actions.token.mint.call(caller, {
|
|
604
|
+
token: base,
|
|
605
|
+
to: rpc.account!.address,
|
|
606
|
+
amount: parseUnits('10000', 6),
|
|
607
|
+
}),
|
|
608
|
+
Actions.token.approve.call(caller, {
|
|
609
|
+
token: base,
|
|
610
|
+
spender: Addresses.stablecoinDex,
|
|
611
|
+
amount: parseUnits('10000', 6),
|
|
612
|
+
}),
|
|
613
|
+
Actions.token.approve.call(caller, {
|
|
614
|
+
token: Relay.addresses.alphaUsd,
|
|
615
|
+
spender: Addresses.stablecoinDex,
|
|
616
|
+
amount: parseUnits('10000', 6),
|
|
617
|
+
}),
|
|
618
|
+
],
|
|
619
|
+
})
|
|
620
|
+
await Actions.dex.createPairSync(rpc, { base })
|
|
621
|
+
await Actions.dex.placeSync(rpc, {
|
|
622
|
+
token: base,
|
|
623
|
+
amount: parseUnits('500', 6),
|
|
624
|
+
type: 'sell',
|
|
625
|
+
tick: Tick.fromPrice('1.001'),
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
// Sender has faucet alphaUsd (fee + swap source) but NO base tokens.
|
|
629
|
+
await Actions.fee.setUserToken(Relay.getClient({ account: sender }), {
|
|
630
|
+
token: Relay.addresses.alphaUsd,
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
// Sender attempts to transfer base via the wallet relay, which forwards
|
|
634
|
+
// to the app relay. The app relay returns 200 with capabilities.error =
|
|
635
|
+
// InsufficientBalance and a stub tx; the wallet relay must convert that
|
|
636
|
+
// into a synthetic throw so its own fill() autoSwap branch can recover.
|
|
637
|
+
const transferAmount = parseUnits('5', 6)
|
|
638
|
+
const result = await fillTransaction(client, {
|
|
639
|
+
account: sender.address,
|
|
640
|
+
...Actions.token.transfer.call(caller, {
|
|
641
|
+
token: base,
|
|
642
|
+
to: Relay.accounts[7]!.address,
|
|
643
|
+
amount: transferAmount,
|
|
644
|
+
}),
|
|
645
|
+
feePayer: appServer.url as never,
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
const { transaction, capabilities } = result
|
|
649
|
+
|
|
650
|
+
// Tx is filled with the swap calls prepended (approve + buy + transfer).
|
|
651
|
+
expect(transaction.calls).toHaveLength(3)
|
|
652
|
+
expect(transaction.feePayerSignature).toBeDefined()
|
|
653
|
+
|
|
654
|
+
// autoSwap metadata is surfaced.
|
|
655
|
+
expect(capabilities?.['autoSwap']?.slippage).toBe(0.05)
|
|
656
|
+
expect(capabilities?.['autoSwap']?.maxIn.symbol).toBe('AlphaUSD')
|
|
657
|
+
expect(capabilities?.['autoSwap']?.minOut.symbol).toBe('EXTBASE')
|
|
658
|
+
expect(capabilities?.['autoSwap']?.minOut.formatted).toBe('5')
|
|
659
|
+
})
|
|
660
|
+
},
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
describe.skipIf(Runtime.get().mode !== 'localnet')(
|
|
664
|
+
'behavior: app-provided feePayer URL bypasses wallet validate',
|
|
665
|
+
() => {
|
|
666
|
+
let appServer: Server
|
|
667
|
+
let walletServer: Server
|
|
668
|
+
let client: ReturnType<typeof Relay.getClient>
|
|
669
|
+
|
|
670
|
+
beforeAll(async () => {
|
|
671
|
+
// App relay is the authoritative sponsor — it has its own fee payer
|
|
672
|
+
// account and signs sponsored transactions.
|
|
673
|
+
appServer = await Relay.createServer(
|
|
674
|
+
(
|
|
675
|
+
await Relay.relay({
|
|
676
|
+
feePayer: {
|
|
677
|
+
account: feePayerAccount,
|
|
678
|
+
name: 'App Sponsor',
|
|
679
|
+
url: 'https://app.example.com',
|
|
680
|
+
},
|
|
681
|
+
})
|
|
682
|
+
).listener,
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
// Wallet relay has its own fee payer with a `validate` that ALWAYS
|
|
686
|
+
// rejects. This guards the wallet's own fee payer; it must NOT gate
|
|
687
|
+
// sponsorship when the dapp supplies its own external feePayer URL.
|
|
688
|
+
walletServer = await Relay.createServer(
|
|
689
|
+
(
|
|
690
|
+
await Relay.relay({
|
|
691
|
+
features: 'all',
|
|
692
|
+
internal_allowUnsafeUrls: true,
|
|
693
|
+
feePayer: {
|
|
694
|
+
account: feePayerAccount,
|
|
695
|
+
name: 'Wallet Sponsor',
|
|
696
|
+
validate: () => false,
|
|
697
|
+
},
|
|
698
|
+
})
|
|
699
|
+
).listener,
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
client = Relay.getClient({ url: walletServer.url })
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
afterAll(() => {
|
|
706
|
+
appServer.close()
|
|
707
|
+
walletServer.close()
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
test('behavior: external feePayer URL is sponsored even when wallet validate rejects', async () => {
|
|
711
|
+
const result = await fillTransaction(client, {
|
|
712
|
+
account: userAccount.address,
|
|
713
|
+
calls: [transferCall()],
|
|
714
|
+
feePayer: appServer.url as never,
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
expect(result.transaction.feePayerSignature).toBeDefined()
|
|
718
|
+
expect(result.transaction.maxFeePerGas).toBeDefined()
|
|
719
|
+
expect(result.transaction.maxFeePerGas).not.toBe(0n)
|
|
720
|
+
expect(result.capabilities?.['sponsored']).toBe(true)
|
|
721
|
+
expect(result.capabilities?.['sponsor']?.name).toBe('App Sponsor')
|
|
722
|
+
})
|
|
723
|
+
},
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: chainId path parameter', () => {
|
|
727
|
+
let server: Server
|
|
728
|
+
let client: ReturnType<typeof Relay.getClient>
|
|
729
|
+
|
|
730
|
+
beforeAll(async () => {
|
|
731
|
+
server = await Relay.createServer((await Relay.relay({})).listener)
|
|
732
|
+
client = Relay.getClient({ url: `${server.url}/${Relay.chain.id}` })
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
afterAll(() => {
|
|
736
|
+
server.close()
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
test('default: proxies RPC methods via /:chainId path', async () => {
|
|
740
|
+
const chainId = await client.request({ method: 'eth_chainId' })
|
|
741
|
+
expect(Number(chainId)).toMatchInlineSnapshot(`${Relay.chain.id}`)
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
test('behavior: fills transaction via /:chainId path', async () => {
|
|
745
|
+
const { transaction } = await fillTransaction(client, {
|
|
746
|
+
account: userAccount.address,
|
|
747
|
+
calls: [transferCall()],
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
expect(transaction.gas).toBeDefined()
|
|
751
|
+
expect(transaction.nonce).toBeDefined()
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
test('behavior: handles batch requests via /:chainId path', async () => {
|
|
755
|
+
const response = await fetch(`${server.url}/${Relay.chain.id}`, {
|
|
756
|
+
method: 'POST',
|
|
757
|
+
headers: { 'Content-Type': 'application/json' },
|
|
758
|
+
body: JSON.stringify([
|
|
759
|
+
{ jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] },
|
|
760
|
+
{ jsonrpc: '2.0', id: 2, method: 'eth_chainId', params: [] },
|
|
761
|
+
]),
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
expect(response.status).toBe(200)
|
|
765
|
+
const body = (await response.json()) as { id: number; result: string }[]
|
|
766
|
+
expect(body).toHaveLength(2)
|
|
767
|
+
expect(Number(body[0]!.result)).toBe(Relay.chain.id)
|
|
768
|
+
expect(Number(body[1]!.result)).toBe(Relay.chain.id)
|
|
769
|
+
})
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: capabilities', () => {
|
|
773
|
+
let server: Server
|
|
774
|
+
let client: ReturnType<typeof Relay.getClient>
|
|
775
|
+
|
|
776
|
+
beforeAll(async () => {
|
|
777
|
+
server = await Relay.createServer(
|
|
778
|
+
(
|
|
779
|
+
await Relay.relay({
|
|
780
|
+
features: 'all',
|
|
781
|
+
})
|
|
782
|
+
).listener,
|
|
783
|
+
)
|
|
784
|
+
client = Relay.getClient({ url: server.url })
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
afterAll(() => {
|
|
788
|
+
server.close()
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
test('default: returns fee and sponsored info', async () => {
|
|
792
|
+
const result = await fillTransaction(client, {
|
|
793
|
+
account: userAccount.address,
|
|
794
|
+
calls: [transferCall()],
|
|
795
|
+
})
|
|
796
|
+
const meta = result.capabilities
|
|
797
|
+
|
|
798
|
+
expect(meta?.['fee']).toBeDefined()
|
|
799
|
+
expect(meta?.['fee']?.decimals).toBe(6)
|
|
800
|
+
expect(meta?.['fee']?.symbol).toBe('AlphaUSD')
|
|
801
|
+
expect(meta?.['sponsored']).toBe(false)
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
test('behavior: token transfer produces balance diffs', async () => {
|
|
805
|
+
const sender = Relay.accounts[6]!
|
|
806
|
+
const recipient = Relay.accounts[7]!
|
|
807
|
+
const token = Relay.addresses.alphaUsd
|
|
808
|
+
|
|
809
|
+
// Sender is faucet-funded with alphaUsd (enough for transfer + fee).
|
|
810
|
+
// Set fee token so relay doesn't need pathUSD balance.
|
|
811
|
+
await Actions.fee.setUserToken(Relay.getClient(), { account: sender, token })
|
|
812
|
+
|
|
813
|
+
const { data, to: callTo } = Actions.token.transfer.call(caller, {
|
|
814
|
+
token,
|
|
815
|
+
to: recipient.address,
|
|
816
|
+
amount: 100n,
|
|
817
|
+
})
|
|
818
|
+
const result = await fillTransaction(client, {
|
|
819
|
+
account: sender.address,
|
|
820
|
+
to: callTo,
|
|
821
|
+
data,
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
const meta = result.capabilities
|
|
825
|
+
const senderDiffs = findDiffs(meta?.['balanceDiffs'], sender.address)!
|
|
826
|
+
const tokenDiff = senderDiffs.find((d) => d.address.toLowerCase() === token.toLowerCase())!
|
|
827
|
+
expect(tokenDiff.decimals).toBe(6)
|
|
828
|
+
expect(tokenDiff.direction).toBe('outgoing')
|
|
829
|
+
expect(tokenDiff.formatted).toBe('0.0001')
|
|
830
|
+
expect(tokenDiff.symbol).toBe('AlphaUSD')
|
|
831
|
+
expect(tokenDiff.value).toBe('0x64')
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
test('behavior: resolves direct virtual-address targets', async () => {
|
|
835
|
+
const virtualAddress = VirtualAddress.from({
|
|
836
|
+
masterId: '0xffffffff',
|
|
837
|
+
userTag: '0x000000000001',
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
const result = await fillTransaction(client, {
|
|
841
|
+
account: userAccount.address,
|
|
842
|
+
to: virtualAddress,
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
expect(virtualAddresses(result.capabilities)).toMatchInlineSnapshot(`
|
|
846
|
+
{
|
|
847
|
+
"0xfffffffffdfdfdfdfdfdfdfdfdfd000000000001": null,
|
|
848
|
+
}
|
|
849
|
+
`)
|
|
850
|
+
})
|
|
851
|
+
|
|
852
|
+
test('behavior: resolves TIP-20 memo transfer virtual-address recipients', async () => {
|
|
853
|
+
const virtualAddress = VirtualAddress.from({
|
|
854
|
+
masterId: '0xfffffffe',
|
|
855
|
+
userTag: '0x000000000002',
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
const result = await fillTransaction(client, {
|
|
859
|
+
account: userAccount.address,
|
|
860
|
+
calls: [
|
|
861
|
+
Actions.token.transfer.call(caller, {
|
|
862
|
+
amount: 1n,
|
|
863
|
+
memo: '0x01',
|
|
864
|
+
to: virtualAddress,
|
|
865
|
+
token: Relay.addresses.alphaUsd,
|
|
866
|
+
}),
|
|
867
|
+
],
|
|
868
|
+
capabilities: { errors: true },
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
expect(virtualAddresses(result.capabilities)).toMatchInlineSnapshot(`
|
|
872
|
+
{
|
|
873
|
+
"0xfffffffefdfdfdfdfdfdfdfdfdfd000000000002": null,
|
|
874
|
+
}
|
|
875
|
+
`)
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
test('behavior: approve + dex swap + transfer produces balance diffs', async () => {
|
|
879
|
+
const sender = Relay.accounts[8]!
|
|
880
|
+
const recipient = Relay.accounts[7]!
|
|
881
|
+
|
|
882
|
+
// Set up token pair + DEX liquidity.
|
|
883
|
+
const rpc = Relay.getClient({ account: Relay.accounts[0]! })
|
|
884
|
+
const { token: quote } = await Actions.token.createSync(rpc, {
|
|
885
|
+
name: 'Test Quote',
|
|
886
|
+
symbol: 'TQUOTE',
|
|
887
|
+
currency: 'USD',
|
|
888
|
+
})
|
|
889
|
+
const { token: base } = await Actions.token.createSync(rpc, {
|
|
890
|
+
name: 'Test Base',
|
|
891
|
+
symbol: 'TBASE',
|
|
892
|
+
currency: 'USD',
|
|
893
|
+
quoteToken: quote,
|
|
894
|
+
})
|
|
895
|
+
await sendTransactionSync(rpc, {
|
|
896
|
+
calls: [
|
|
897
|
+
Actions.token.grantRoles.call(caller, {
|
|
898
|
+
token: base,
|
|
899
|
+
role: 'issuer',
|
|
900
|
+
to: rpc.account!.address,
|
|
901
|
+
}),
|
|
902
|
+
Actions.token.grantRoles.call(caller, {
|
|
903
|
+
token: quote,
|
|
904
|
+
role: 'issuer',
|
|
905
|
+
to: rpc.account!.address,
|
|
906
|
+
}),
|
|
907
|
+
Actions.token.mint.call(caller, {
|
|
908
|
+
token: base,
|
|
909
|
+
to: rpc.account!.address,
|
|
910
|
+
amount: parseUnits('10000', 6),
|
|
911
|
+
}),
|
|
912
|
+
Actions.token.mint.call(caller, {
|
|
913
|
+
token: quote,
|
|
914
|
+
to: rpc.account!.address,
|
|
915
|
+
amount: parseUnits('10000', 6),
|
|
916
|
+
}),
|
|
917
|
+
Actions.token.approve.call(caller, {
|
|
918
|
+
token: base,
|
|
919
|
+
spender: Addresses.stablecoinDex,
|
|
920
|
+
amount: parseUnits('10000', 6),
|
|
921
|
+
}),
|
|
922
|
+
Actions.token.approve.call(caller, {
|
|
923
|
+
token: quote,
|
|
924
|
+
spender: Addresses.stablecoinDex,
|
|
925
|
+
amount: parseUnits('10000', 6),
|
|
926
|
+
}),
|
|
927
|
+
],
|
|
928
|
+
})
|
|
929
|
+
await Actions.dex.createPairSync(rpc, { base })
|
|
930
|
+
await Actions.dex.placeSync(rpc, {
|
|
931
|
+
token: base,
|
|
932
|
+
amount: parseUnits('500', 6),
|
|
933
|
+
type: 'sell',
|
|
934
|
+
tick: Tick.fromPrice('1.001'),
|
|
935
|
+
})
|
|
936
|
+
|
|
937
|
+
// Fund sender with quote tokens; alphaUsd fees come from the faucet.
|
|
938
|
+
await Actions.token.mintSync(rpc, {
|
|
939
|
+
token: quote,
|
|
940
|
+
amount: parseUnits('1000', 6),
|
|
941
|
+
to: sender.address,
|
|
942
|
+
})
|
|
943
|
+
await Actions.fee.setUserToken(Relay.getClient({ account: sender }), {
|
|
944
|
+
token: Relay.addresses.alphaUsd,
|
|
945
|
+
})
|
|
946
|
+
|
|
947
|
+
const buyAmount = parseUnits('10', 6)
|
|
948
|
+
const result = await fillTransaction(client, {
|
|
949
|
+
account: sender.address,
|
|
950
|
+
calls: [
|
|
951
|
+
Actions.token.approve.call(caller, {
|
|
952
|
+
token: quote,
|
|
953
|
+
spender: Addresses.stablecoinDex,
|
|
954
|
+
amount: parseUnits('100', 6),
|
|
955
|
+
}),
|
|
956
|
+
Actions.dex.buy.call({
|
|
957
|
+
tokenIn: quote,
|
|
958
|
+
tokenOut: base,
|
|
959
|
+
amountOut: buyAmount,
|
|
960
|
+
maxAmountIn: parseUnits('100', 6),
|
|
961
|
+
}),
|
|
962
|
+
Actions.token.transfer.call(caller, {
|
|
963
|
+
token: base,
|
|
964
|
+
to: recipient.address,
|
|
965
|
+
amount: buyAmount,
|
|
966
|
+
}),
|
|
967
|
+
],
|
|
968
|
+
})
|
|
969
|
+
const meta = result.capabilities
|
|
970
|
+
|
|
971
|
+
const diffs = findDiffs(meta?.['balanceDiffs'], sender.address)!
|
|
972
|
+
expect(diffs).toHaveLength(1)
|
|
973
|
+
|
|
974
|
+
const quoteDiff = diffs[0]!
|
|
975
|
+
expect(quoteDiff.address.toLowerCase()).toBe(quote.toLowerCase())
|
|
976
|
+
expect(quoteDiff.direction).toBe('outgoing')
|
|
977
|
+
expect(quoteDiff.symbol).toBe('TQUOTE')
|
|
978
|
+
expect(quoteDiff.name).toBe('Test Quote')
|
|
979
|
+
expect(quoteDiff.decimals).toBe(6)
|
|
980
|
+
// No base diff — bought and immediately transferred out (net zero).
|
|
981
|
+
expect(diffs.find((d) => d.address.toLowerCase() === base.toLowerCase())).toBeUndefined()
|
|
982
|
+
})
|
|
983
|
+
|
|
984
|
+
test('behavior: approval covered by transfer is suppressed', async () => {
|
|
985
|
+
const sender = Relay.accounts[6]!
|
|
986
|
+
const recipient = Relay.accounts[7]!
|
|
987
|
+
const token = Relay.addresses.alphaUsd
|
|
988
|
+
|
|
989
|
+
// approve(100) + transfer(100) to same spender → approval fully covered.
|
|
990
|
+
const result = await fillTransaction(client, {
|
|
991
|
+
account: sender.address,
|
|
992
|
+
calls: [
|
|
993
|
+
Actions.token.approve.call(caller, {
|
|
994
|
+
token,
|
|
995
|
+
spender: recipient.address,
|
|
996
|
+
amount: 100n,
|
|
997
|
+
}),
|
|
998
|
+
Actions.token.transfer.call(caller, {
|
|
999
|
+
token,
|
|
1000
|
+
to: recipient.address,
|
|
1001
|
+
amount: 100n,
|
|
1002
|
+
}),
|
|
1003
|
+
],
|
|
1004
|
+
})
|
|
1005
|
+
const meta = result.capabilities
|
|
1006
|
+
|
|
1007
|
+
const diffs = findDiffs(meta?.['balanceDiffs'], sender.address)!
|
|
1008
|
+
const tokenDiff = diffs.find((d) => d.address.toLowerCase() === token.toLowerCase())!
|
|
1009
|
+
// Only the transfer shows — approval is fully covered.
|
|
1010
|
+
expect(tokenDiff.value).toBe('0x64')
|
|
1011
|
+
expect(tokenDiff.direction).toBe('outgoing')
|
|
1012
|
+
})
|
|
1013
|
+
|
|
1014
|
+
test('behavior: uncovered approval shows as outgoing', async () => {
|
|
1015
|
+
const sender = Relay.accounts[6]!
|
|
1016
|
+
const spender = Relay.accounts[7]!
|
|
1017
|
+
const token = Relay.addresses.alphaUsd
|
|
1018
|
+
|
|
1019
|
+
// approve(200) + transfer(50) to same spender → 150 uncovered approval.
|
|
1020
|
+
const result = await fillTransaction(client, {
|
|
1021
|
+
account: sender.address,
|
|
1022
|
+
calls: [
|
|
1023
|
+
Actions.token.approve.call(caller, {
|
|
1024
|
+
token,
|
|
1025
|
+
spender: spender.address,
|
|
1026
|
+
amount: 200n,
|
|
1027
|
+
}),
|
|
1028
|
+
Actions.token.transfer.call(caller, {
|
|
1029
|
+
token,
|
|
1030
|
+
to: spender.address,
|
|
1031
|
+
amount: 50n,
|
|
1032
|
+
}),
|
|
1033
|
+
],
|
|
1034
|
+
})
|
|
1035
|
+
const meta = result.capabilities
|
|
1036
|
+
|
|
1037
|
+
const diffs = findDiffs(meta?.['balanceDiffs'], sender.address)!
|
|
1038
|
+
const tokenDiff = diffs.find((d) => d.address.toLowerCase() === token.toLowerCase())!
|
|
1039
|
+
// transfer(50) + uncovered approval(150) = 200 outgoing.
|
|
1040
|
+
expect(tokenDiff.value).toBe('0xc8')
|
|
1041
|
+
expect(tokenDiff.direction).toBe('outgoing')
|
|
1042
|
+
})
|
|
1043
|
+
})
|
|
1044
|
+
|
|
1045
|
+
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: AMM resolution', () => {
|
|
1046
|
+
let server: Server
|
|
1047
|
+
let client: ReturnType<typeof Relay.getClient>
|
|
1048
|
+
|
|
1049
|
+
beforeAll(async () => {
|
|
1050
|
+
server = await Relay.createServer(
|
|
1051
|
+
(
|
|
1052
|
+
await Relay.relay({
|
|
1053
|
+
features: 'all',
|
|
1054
|
+
})
|
|
1055
|
+
).listener,
|
|
1056
|
+
)
|
|
1057
|
+
client = Relay.getClient({ url: server.url })
|
|
1058
|
+
})
|
|
1059
|
+
|
|
1060
|
+
afterAll(() => {
|
|
1061
|
+
server.close()
|
|
1062
|
+
})
|
|
1063
|
+
|
|
1064
|
+
test('behavior: prepends swap calls on InsufficientBalance', async () => {
|
|
1065
|
+
const sender = Relay.accounts[4]!
|
|
1066
|
+
|
|
1067
|
+
// Set up token pair + DEX liquidity.
|
|
1068
|
+
// Use alphaUsd as the quote token so the relay can swap alphaUsd → base.
|
|
1069
|
+
const rpc = Relay.getClient({ account: Relay.accounts[0]! })
|
|
1070
|
+
const { token: base } = await Actions.token.createSync(rpc, {
|
|
1071
|
+
name: 'Swap Base',
|
|
1072
|
+
symbol: 'SWBASE',
|
|
1073
|
+
currency: 'USD',
|
|
1074
|
+
quoteToken: Relay.addresses.alphaUsd,
|
|
1075
|
+
})
|
|
1076
|
+
await sendTransactionSync(rpc, {
|
|
1077
|
+
calls: [
|
|
1078
|
+
Actions.token.grantRoles.call(caller, {
|
|
1079
|
+
token: base,
|
|
1080
|
+
role: 'issuer',
|
|
1081
|
+
to: rpc.account!.address,
|
|
1082
|
+
}),
|
|
1083
|
+
Actions.token.mint.call(caller, {
|
|
1084
|
+
token: base,
|
|
1085
|
+
to: rpc.account!.address,
|
|
1086
|
+
amount: parseUnits('10000', 6),
|
|
1087
|
+
}),
|
|
1088
|
+
Actions.token.approve.call(caller, {
|
|
1089
|
+
token: base,
|
|
1090
|
+
spender: Addresses.stablecoinDex,
|
|
1091
|
+
amount: parseUnits('10000', 6),
|
|
1092
|
+
}),
|
|
1093
|
+
Actions.token.approve.call(caller, {
|
|
1094
|
+
token: Relay.addresses.alphaUsd,
|
|
1095
|
+
spender: Addresses.stablecoinDex,
|
|
1096
|
+
amount: parseUnits('10000', 6),
|
|
1097
|
+
}),
|
|
1098
|
+
],
|
|
1099
|
+
})
|
|
1100
|
+
await Actions.dex.createPairSync(rpc, { base })
|
|
1101
|
+
await Actions.dex.placeSync(rpc, {
|
|
1102
|
+
token: base,
|
|
1103
|
+
amount: parseUnits('500', 6),
|
|
1104
|
+
type: 'sell',
|
|
1105
|
+
tick: Tick.fromPrice('1.001'),
|
|
1106
|
+
})
|
|
1107
|
+
|
|
1108
|
+
// Sender has faucet alphaUsd (fee token) but NO base tokens.
|
|
1109
|
+
await Actions.fee.setUserToken(Relay.getClient({ account: sender }), {
|
|
1110
|
+
token: Relay.addresses.alphaUsd,
|
|
1111
|
+
})
|
|
1112
|
+
|
|
1113
|
+
// Sender tries to transfer base tokens they don't have.
|
|
1114
|
+
// Relay should detect InsufficientBalance, swap alphaUsd → base via DEX, and retry.
|
|
1115
|
+
const transferAmount = parseUnits('5', 6)
|
|
1116
|
+
const result = await fillTransaction(client, {
|
|
1117
|
+
account: sender.address,
|
|
1118
|
+
...Actions.token.transfer.call(caller, {
|
|
1119
|
+
token: base,
|
|
1120
|
+
to: Relay.accounts[7]!.address,
|
|
1121
|
+
amount: transferAmount,
|
|
1122
|
+
}),
|
|
1123
|
+
})
|
|
1124
|
+
|
|
1125
|
+
// Should succeed — relay auto-swapped quote → base.
|
|
1126
|
+
const { transaction, capabilities } = result
|
|
1127
|
+
expect(transaction.gas).toBeDefined()
|
|
1128
|
+
expect(transaction.nonce).toBeDefined()
|
|
1129
|
+
expect(transaction.feeToken).toBe(Relay.addresses.alphaUsd)
|
|
1130
|
+
expect(transaction.calls).toHaveLength(3) // approve + swap + transfer
|
|
1131
|
+
|
|
1132
|
+
const m = capabilities
|
|
1133
|
+
expect(m?.['sponsored']).toBe(false)
|
|
1134
|
+
expect(m?.['fee']?.decimals).toBe(6)
|
|
1135
|
+
expect(m?.['fee']?.symbol).toBe('AlphaUSD')
|
|
1136
|
+
|
|
1137
|
+
// Balance diffs exclude swap tokens — only the user's transfer shows.
|
|
1138
|
+
const diffs = findDiffs(m?.['balanceDiffs'], sender.address)!
|
|
1139
|
+
expect(diffs).toHaveLength(1)
|
|
1140
|
+
expect(diffs[0]!.direction).toBe('outgoing')
|
|
1141
|
+
expect(diffs[0]!.formatted).toBe('5')
|
|
1142
|
+
expect(diffs[0]!.symbol).toBe('SWBASE')
|
|
1143
|
+
expect(diffs[0]!.address.toLowerCase()).toBe(base.toLowerCase())
|
|
1144
|
+
|
|
1145
|
+
// autoSwap reports the injected AMM swap.
|
|
1146
|
+
expect(m?.['autoSwap']?.slippage).toBe(0.05)
|
|
1147
|
+
expect(m?.['autoSwap']?.maxIn.formatted).toBe('5.25')
|
|
1148
|
+
expect(m?.['autoSwap']?.maxIn.symbol).toBe('AlphaUSD')
|
|
1149
|
+
expect(m?.['autoSwap']?.maxIn.token.toLowerCase()).toBe(Relay.addresses.alphaUsd.toLowerCase())
|
|
1150
|
+
expect(m?.['autoSwap']?.minOut.formatted).toBe('5')
|
|
1151
|
+
expect(m?.['autoSwap']?.minOut.symbol).toBe('SWBASE')
|
|
1152
|
+
expect(m?.['autoSwap']?.minOut.token.toLowerCase()).toBe(base.toLowerCase())
|
|
1153
|
+
})
|
|
1154
|
+
|
|
1155
|
+
test('behavior: custom slippage is applied to autoSwap', async () => {
|
|
1156
|
+
const sender = Relay.accounts[2]!
|
|
1157
|
+
|
|
1158
|
+
// Set up token pair + DEX liquidity.
|
|
1159
|
+
const rpc = Relay.getClient({ account: Relay.accounts[0]! })
|
|
1160
|
+
const { token: base } = await Actions.token.createSync(rpc, {
|
|
1161
|
+
name: 'Slippage Base',
|
|
1162
|
+
symbol: 'SLPBASE',
|
|
1163
|
+
currency: 'USD',
|
|
1164
|
+
quoteToken: Relay.addresses.alphaUsd,
|
|
1165
|
+
})
|
|
1166
|
+
await sendTransactionSync(rpc, {
|
|
1167
|
+
calls: [
|
|
1168
|
+
Actions.token.grantRoles.call(caller, {
|
|
1169
|
+
token: base,
|
|
1170
|
+
role: 'issuer',
|
|
1171
|
+
to: rpc.account!.address,
|
|
1172
|
+
}),
|
|
1173
|
+
Actions.token.mint.call(caller, {
|
|
1174
|
+
token: base,
|
|
1175
|
+
to: rpc.account!.address,
|
|
1176
|
+
amount: parseUnits('10000', 6),
|
|
1177
|
+
}),
|
|
1178
|
+
Actions.token.approve.call(caller, {
|
|
1179
|
+
token: base,
|
|
1180
|
+
spender: Addresses.stablecoinDex,
|
|
1181
|
+
amount: parseUnits('10000', 6),
|
|
1182
|
+
}),
|
|
1183
|
+
Actions.token.approve.call(caller, {
|
|
1184
|
+
token: Relay.addresses.alphaUsd,
|
|
1185
|
+
spender: Addresses.stablecoinDex,
|
|
1186
|
+
amount: parseUnits('10000', 6),
|
|
1187
|
+
}),
|
|
1188
|
+
],
|
|
1189
|
+
})
|
|
1190
|
+
await Actions.dex.createPairSync(rpc, { base })
|
|
1191
|
+
await Actions.dex.placeSync(rpc, {
|
|
1192
|
+
token: base,
|
|
1193
|
+
amount: parseUnits('500', 6),
|
|
1194
|
+
type: 'sell',
|
|
1195
|
+
tick: Tick.fromPrice('1.001'),
|
|
1196
|
+
})
|
|
1197
|
+
|
|
1198
|
+
// Sender has faucet alphaUsd but NO base tokens.
|
|
1199
|
+
await Actions.fee.setUserToken(Relay.getClient({ account: sender }), {
|
|
1200
|
+
token: Relay.addresses.alphaUsd,
|
|
1201
|
+
})
|
|
1202
|
+
|
|
1203
|
+
// Create relay with custom 2% slippage.
|
|
1204
|
+
const customServer = await Relay.createServer(
|
|
1205
|
+
(
|
|
1206
|
+
await Relay.relay({
|
|
1207
|
+
features: 'all',
|
|
1208
|
+
autoSwap: { slippage: 0.02 },
|
|
1209
|
+
})
|
|
1210
|
+
).listener,
|
|
1211
|
+
)
|
|
1212
|
+
const customClient = Relay.getClient({ url: customServer.url })
|
|
1213
|
+
|
|
1214
|
+
const result = await fillTransaction(customClient, {
|
|
1215
|
+
account: sender.address,
|
|
1216
|
+
...Actions.token.transfer.call(caller, {
|
|
1217
|
+
token: base,
|
|
1218
|
+
to: Relay.accounts[7]!.address,
|
|
1219
|
+
amount: parseUnits('10', 6),
|
|
1220
|
+
}),
|
|
1221
|
+
})
|
|
1222
|
+
customServer.close()
|
|
1223
|
+
|
|
1224
|
+
const m = result.capabilities
|
|
1225
|
+
expect(m?.['autoSwap']?.slippage).toBe(0.02)
|
|
1226
|
+
// 10 + 2% = 10.2
|
|
1227
|
+
expect(m?.['autoSwap']?.maxIn.formatted).toBe('10.2')
|
|
1228
|
+
expect(m?.['autoSwap']?.minOut.formatted).toBe('10')
|
|
1229
|
+
})
|
|
1230
|
+
|
|
1231
|
+
test('behavior: autoSwap disabled throws InsufficientBalance instead of swapping', async () => {
|
|
1232
|
+
const sender = Relay.accounts[3]!
|
|
1233
|
+
|
|
1234
|
+
// Set up token pair + DEX liquidity.
|
|
1235
|
+
const rpc = Relay.getClient({ account: Relay.accounts[0]! })
|
|
1236
|
+
const { token: base } = await Actions.token.createSync(rpc, {
|
|
1237
|
+
name: 'No Swap Base',
|
|
1238
|
+
symbol: 'NSWBASE',
|
|
1239
|
+
currency: 'USD',
|
|
1240
|
+
quoteToken: Relay.addresses.alphaUsd,
|
|
1241
|
+
})
|
|
1242
|
+
await sendTransactionSync(rpc, {
|
|
1243
|
+
calls: [
|
|
1244
|
+
Actions.token.grantRoles.call(caller, {
|
|
1245
|
+
token: base,
|
|
1246
|
+
role: 'issuer',
|
|
1247
|
+
to: rpc.account!.address,
|
|
1248
|
+
}),
|
|
1249
|
+
Actions.token.mint.call(caller, {
|
|
1250
|
+
token: base,
|
|
1251
|
+
to: rpc.account!.address,
|
|
1252
|
+
amount: parseUnits('10000', 6),
|
|
1253
|
+
}),
|
|
1254
|
+
Actions.token.approve.call(caller, {
|
|
1255
|
+
token: base,
|
|
1256
|
+
spender: Addresses.stablecoinDex,
|
|
1257
|
+
amount: parseUnits('10000', 6),
|
|
1258
|
+
}),
|
|
1259
|
+
],
|
|
1260
|
+
})
|
|
1261
|
+
await Actions.dex.createPairSync(rpc, { base })
|
|
1262
|
+
await Actions.dex.placeSync(rpc, {
|
|
1263
|
+
token: base,
|
|
1264
|
+
amount: parseUnits('500', 6),
|
|
1265
|
+
type: 'sell',
|
|
1266
|
+
tick: Tick.fromPrice('1.001'),
|
|
1267
|
+
})
|
|
1268
|
+
|
|
1269
|
+
// Sender has faucet alphaUsd but NO base tokens.
|
|
1270
|
+
await Actions.fee.setUserToken(Relay.getClient({ account: sender }), {
|
|
1271
|
+
token: Relay.addresses.alphaUsd,
|
|
1272
|
+
})
|
|
1273
|
+
|
|
1274
|
+
// Create relay with autoSwap disabled.
|
|
1275
|
+
const customServer = await Relay.createServer(
|
|
1276
|
+
(
|
|
1277
|
+
await Relay.relay({
|
|
1278
|
+
features: 'all',
|
|
1279
|
+
autoSwap: false,
|
|
1280
|
+
})
|
|
1281
|
+
).listener,
|
|
1282
|
+
)
|
|
1283
|
+
const customClient = Relay.getClient({ url: customServer.url })
|
|
1284
|
+
|
|
1285
|
+
// Should return error capability instead of auto-swapping.
|
|
1286
|
+
const result = await fillTransaction(customClient, {
|
|
1287
|
+
account: sender.address,
|
|
1288
|
+
calls: [
|
|
1289
|
+
Actions.token.transfer.call(caller, {
|
|
1290
|
+
token: base,
|
|
1291
|
+
to: Relay.accounts[7]!.address,
|
|
1292
|
+
amount: parseUnits('5', 6),
|
|
1293
|
+
}),
|
|
1294
|
+
],
|
|
1295
|
+
capabilities: { errors: true },
|
|
1296
|
+
})
|
|
1297
|
+
const error = result.capabilities?.['error']
|
|
1298
|
+
expect({ ...error, data: undefined }).toMatchInlineSnapshot(`
|
|
1299
|
+
{
|
|
1300
|
+
"abiItem": {
|
|
1301
|
+
"inputs": [
|
|
1302
|
+
{
|
|
1303
|
+
"name": "available",
|
|
1304
|
+
"type": "uint256",
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
"name": "required",
|
|
1308
|
+
"type": "uint256",
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
"name": "token",
|
|
1312
|
+
"type": "address",
|
|
1313
|
+
},
|
|
1314
|
+
],
|
|
1315
|
+
"name": "InsufficientBalance",
|
|
1316
|
+
"type": "error",
|
|
1317
|
+
},
|
|
1318
|
+
"data": undefined,
|
|
1319
|
+
"errorName": "InsufficientBalance",
|
|
1320
|
+
"message": "Insufficient balance. Required: 5000000, available: 0.",
|
|
1321
|
+
}
|
|
1322
|
+
`)
|
|
1323
|
+
customServer.close()
|
|
1324
|
+
})
|
|
1325
|
+
})
|
|
1326
|
+
|
|
1327
|
+
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: conditional sponsoring', () => {
|
|
1328
|
+
let server: Server
|
|
1329
|
+
let client: ReturnType<typeof Relay.getClient>
|
|
1330
|
+
|
|
1331
|
+
beforeAll(async () => {
|
|
1332
|
+
// accounts[3] is faucet-funded with alphaUsd so transfers succeed.
|
|
1333
|
+
await Actions.fee.setUserToken(Relay.getClient(), {
|
|
1334
|
+
account: Relay.accounts[3]!,
|
|
1335
|
+
token: Relay.addresses.alphaUsd,
|
|
1336
|
+
})
|
|
1337
|
+
|
|
1338
|
+
server = await Relay.createServer(
|
|
1339
|
+
(
|
|
1340
|
+
await Relay.relay({
|
|
1341
|
+
feePayer: {
|
|
1342
|
+
account: feePayerAccount,
|
|
1343
|
+
name: 'Test Sponsor',
|
|
1344
|
+
url: 'https://test.com',
|
|
1345
|
+
validate: (request) =>
|
|
1346
|
+
request.from?.toLowerCase() !== Relay.accounts[3]!.address.toLowerCase(),
|
|
1347
|
+
},
|
|
1348
|
+
})
|
|
1349
|
+
).listener,
|
|
1350
|
+
)
|
|
1351
|
+
client = Relay.getClient({ url: server.url })
|
|
1352
|
+
})
|
|
1353
|
+
|
|
1354
|
+
afterAll(() => {
|
|
1355
|
+
server.close()
|
|
1356
|
+
})
|
|
1357
|
+
|
|
1358
|
+
test('behavior: approved tx is sponsored and can be broadcast', async () => {
|
|
1359
|
+
const { transaction } = await fillTransaction(client, {
|
|
1360
|
+
account: userAccount.address,
|
|
1361
|
+
calls: [transferCall()],
|
|
1362
|
+
})
|
|
1363
|
+
expect(transaction.feePayerSignature).toBeDefined()
|
|
1364
|
+
|
|
1365
|
+
const signed = await userAccount.signTransaction(transaction as never)
|
|
1366
|
+
const receipt = (await Relay.getClient().request({
|
|
1367
|
+
method: 'eth_sendRawTransactionSync' as never,
|
|
1368
|
+
params: [signed],
|
|
1369
|
+
})) as { feePayer?: string | undefined }
|
|
1370
|
+
|
|
1371
|
+
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
|
1372
|
+
})
|
|
1373
|
+
|
|
1374
|
+
test('behavior: rejected tx is not sponsored and can be self-paid', async () => {
|
|
1375
|
+
const sender = Relay.accounts[3]!
|
|
1376
|
+
const result = await fillTransaction(client, {
|
|
1377
|
+
account: sender.address,
|
|
1378
|
+
calls: [transferCall()],
|
|
1379
|
+
})
|
|
1380
|
+
expect(result.transaction.feePayerSignature).toBeUndefined()
|
|
1381
|
+
|
|
1382
|
+
const meta = result.capabilities
|
|
1383
|
+
expect(meta?.['sponsored']).toBe(false)
|
|
1384
|
+
expect(meta?.['sponsor']).toBeUndefined()
|
|
1385
|
+
|
|
1386
|
+
const serialized = (await Transaction.serialize(result.transaction as never)) as `0x76${string}`
|
|
1387
|
+
const envelope = TxEnvelopeTempo.deserialize(serialized)
|
|
1388
|
+
const signature = await sender.sign({
|
|
1389
|
+
hash: TxEnvelopeTempo.getSignPayload(envelope),
|
|
1390
|
+
})
|
|
1391
|
+
const signed = TxEnvelopeTempo.serialize(envelope, {
|
|
1392
|
+
signature: SignatureEnvelope.from(signature),
|
|
1393
|
+
})
|
|
1394
|
+
const receipt = (await Relay.getClient().request({
|
|
1395
|
+
method: 'eth_sendRawTransactionSync' as never,
|
|
1396
|
+
params: [signed],
|
|
1397
|
+
})) as { feePayer?: string | undefined }
|
|
1398
|
+
|
|
1399
|
+
// Sender pays their own fee — no external fee payer.
|
|
1400
|
+
expect(receipt.feePayer).not.toBe(feePayerAccount.address.toLowerCase())
|
|
1401
|
+
})
|
|
1402
|
+
})
|
|
1403
|
+
|
|
1404
|
+
describe.skipIf(Runtime.get().mode !== 'localnet')(
|
|
1405
|
+
'behavior: path A — guaranteed sponsorship (no validate)',
|
|
1406
|
+
() => {
|
|
1407
|
+
let server: Server
|
|
1408
|
+
let client: ReturnType<typeof Relay.getClient>
|
|
1409
|
+
let requests: RpcRequest.RpcRequest[] = []
|
|
1410
|
+
|
|
1411
|
+
beforeAll(async () => {
|
|
1412
|
+
server = await Relay.createServer(
|
|
1413
|
+
(
|
|
1414
|
+
await Relay.relay({
|
|
1415
|
+
features: 'all',
|
|
1416
|
+
tokens: localnetTokens,
|
|
1417
|
+
feePayer: {
|
|
1418
|
+
account: feePayerAccount,
|
|
1419
|
+
name: 'Path A Sponsor',
|
|
1420
|
+
},
|
|
1421
|
+
onRequest: async (request) => {
|
|
1422
|
+
requests.push(request)
|
|
1423
|
+
},
|
|
1424
|
+
})
|
|
1425
|
+
).listener,
|
|
1426
|
+
)
|
|
1427
|
+
client = Relay.getClient({ url: server.url })
|
|
1428
|
+
})
|
|
1429
|
+
|
|
1430
|
+
afterAll(() => {
|
|
1431
|
+
server.close()
|
|
1432
|
+
})
|
|
1433
|
+
|
|
1434
|
+
afterEach(() => {
|
|
1435
|
+
requests = []
|
|
1436
|
+
})
|
|
1437
|
+
|
|
1438
|
+
test('behavior: skips fee token resolution and sponsors', async () => {
|
|
1439
|
+
const result = await fillTransaction(client, {
|
|
1440
|
+
account: userAccount.address,
|
|
1441
|
+
calls: [transferCall()],
|
|
1442
|
+
})
|
|
1443
|
+
|
|
1444
|
+
expect(result.transaction.feePayerSignature).toBeDefined()
|
|
1445
|
+
expect(result.capabilities?.['sponsored']).toBe(true)
|
|
1446
|
+
expect(result.capabilities?.['sponsor']?.name).toBe('Path A Sponsor')
|
|
1447
|
+
// Only one fill request — no fee token resolution round-trip.
|
|
1448
|
+
expect(requests.filter((r) => r.method === 'eth_fillTransaction')).toHaveLength(1)
|
|
1449
|
+
})
|
|
1450
|
+
|
|
1451
|
+
test('behavior: returns fee even when no feeToken in request', async () => {
|
|
1452
|
+
const result = await fillTransaction(client, {
|
|
1453
|
+
account: userAccount.address,
|
|
1454
|
+
calls: [transferCall()],
|
|
1455
|
+
})
|
|
1456
|
+
|
|
1457
|
+
expect(result.capabilities?.['fee']).toBeDefined()
|
|
1458
|
+
expect(result.capabilities?.['fee']?.decimals).toBeTypeOf('number')
|
|
1459
|
+
expect(result.capabilities?.['fee']?.symbol).toBeTypeOf('string')
|
|
1460
|
+
expect(result.capabilities?.['fee']?.formatted).toBeTypeOf('string')
|
|
1461
|
+
})
|
|
1462
|
+
|
|
1463
|
+
test('behavior: simulate and sign run concurrently with fill', async () => {
|
|
1464
|
+
const result = await fillTransaction(client, {
|
|
1465
|
+
account: userAccount.address,
|
|
1466
|
+
calls: [transferCall()],
|
|
1467
|
+
})
|
|
1468
|
+
|
|
1469
|
+
// All capabilities are present despite parallel execution.
|
|
1470
|
+
expect(result.transaction.feePayerSignature).toBeDefined()
|
|
1471
|
+
expect(result.capabilities?.['sponsored']).toBe(true)
|
|
1472
|
+
})
|
|
1473
|
+
|
|
1474
|
+
test('behavior: defaults feeToken to chain default when caller omits it', async () => {
|
|
1475
|
+
// Without the default, the broadcast envelope has no feeToken and
|
|
1476
|
+
// the chain falls back to the sender's account token, which often
|
|
1477
|
+
// lacks FeeAMM liquidity.
|
|
1478
|
+
const result = await fillTransaction(client, {
|
|
1479
|
+
account: userAccount.address,
|
|
1480
|
+
calls: [transferCall()],
|
|
1481
|
+
})
|
|
1482
|
+
|
|
1483
|
+
expect(result.transaction.feeToken?.toLowerCase()).toBe(
|
|
1484
|
+
localnetTokens[0]!.address.toLowerCase(),
|
|
1485
|
+
)
|
|
1486
|
+
})
|
|
1487
|
+
|
|
1488
|
+
test('behavior: preserves caller-supplied feeToken', async () => {
|
|
1489
|
+
const result = await fillTransaction(client, {
|
|
1490
|
+
account: userAccount.address,
|
|
1491
|
+
calls: [transferCall()],
|
|
1492
|
+
feeToken: Relay.addresses.alphaUsd as Address,
|
|
1493
|
+
})
|
|
1494
|
+
|
|
1495
|
+
expect(result.transaction.feeToken?.toLowerCase()).toBe(
|
|
1496
|
+
Relay.addresses.alphaUsd.toLowerCase(),
|
|
1497
|
+
)
|
|
1498
|
+
})
|
|
1499
|
+
},
|
|
1500
|
+
)
|
|
1501
|
+
|
|
1502
|
+
describe.skipIf(Runtime.get().mode !== 'localnet')(
|
|
1503
|
+
'behavior: path B — conditional sponsorship (validate)',
|
|
1504
|
+
() => {
|
|
1505
|
+
let server: Server
|
|
1506
|
+
let client: ReturnType<typeof Relay.getClient>
|
|
1507
|
+
let requests: RpcRequest.RpcRequest[] = []
|
|
1508
|
+
|
|
1509
|
+
// Reject accounts[3], approve everyone else.
|
|
1510
|
+
const rejectedSender = Relay.accounts[3]!
|
|
1511
|
+
|
|
1512
|
+
beforeAll(async () => {
|
|
1513
|
+
// rejectedSender is faucet-funded with alphaUsd so it can self-pay.
|
|
1514
|
+
await Actions.fee.setUserToken(Relay.getClient(), {
|
|
1515
|
+
account: rejectedSender,
|
|
1516
|
+
token: Relay.addresses.alphaUsd,
|
|
1517
|
+
})
|
|
1518
|
+
|
|
1519
|
+
server = await Relay.createServer(
|
|
1520
|
+
(
|
|
1521
|
+
await Relay.relay({
|
|
1522
|
+
features: 'all',
|
|
1523
|
+
feePayer: {
|
|
1524
|
+
account: feePayerAccount,
|
|
1525
|
+
name: 'Path B Sponsor',
|
|
1526
|
+
validate: (request) =>
|
|
1527
|
+
request.from?.toLowerCase() !== rejectedSender.address.toLowerCase(),
|
|
1528
|
+
},
|
|
1529
|
+
onRequest: async (request) => {
|
|
1530
|
+
requests.push(request)
|
|
1531
|
+
},
|
|
1532
|
+
})
|
|
1533
|
+
).listener,
|
|
1534
|
+
)
|
|
1535
|
+
client = Relay.getClient({ url: server.url })
|
|
1536
|
+
})
|
|
1537
|
+
|
|
1538
|
+
afterAll(() => {
|
|
1539
|
+
server.close()
|
|
1540
|
+
})
|
|
1541
|
+
|
|
1542
|
+
afterEach(() => {
|
|
1543
|
+
requests = []
|
|
1544
|
+
})
|
|
1545
|
+
|
|
1546
|
+
test('behavior: approved sender gets sponsorship with parallel fills', async () => {
|
|
1547
|
+
const result = await fillTransaction(client, {
|
|
1548
|
+
account: userAccount.address,
|
|
1549
|
+
calls: [transferCall()],
|
|
1550
|
+
})
|
|
1551
|
+
|
|
1552
|
+
expect(result.transaction.feePayerSignature).toBeDefined()
|
|
1553
|
+
expect(result.capabilities?.['sponsored']).toBe(true)
|
|
1554
|
+
expect(result.capabilities?.['sponsor']?.name).toBe('Path B Sponsor')
|
|
1555
|
+
})
|
|
1556
|
+
|
|
1557
|
+
test('behavior: rejected sender gets unsponsored tx from parallel fill', async () => {
|
|
1558
|
+
const result = await fillTransaction(client, {
|
|
1559
|
+
account: rejectedSender.address,
|
|
1560
|
+
calls: [transferCall()],
|
|
1561
|
+
})
|
|
1562
|
+
|
|
1563
|
+
expect(result.transaction.feePayerSignature).toBeUndefined()
|
|
1564
|
+
expect(result.capabilities?.['sponsored']).toBe(false)
|
|
1565
|
+
expect(result.capabilities?.['sponsor']).toBeUndefined()
|
|
1566
|
+
})
|
|
1567
|
+
|
|
1568
|
+
test('behavior: rejected tx can be signed and broadcast by sender', async () => {
|
|
1569
|
+
const result = await fillTransaction(client, {
|
|
1570
|
+
account: rejectedSender.address,
|
|
1571
|
+
calls: [transferCall()],
|
|
1572
|
+
})
|
|
1573
|
+
|
|
1574
|
+
const serialized = (await Transaction.serialize(
|
|
1575
|
+
result.transaction as never,
|
|
1576
|
+
)) as `0x76${string}`
|
|
1577
|
+
const envelope = TxEnvelopeTempo.deserialize(serialized)
|
|
1578
|
+
const signature = await rejectedSender.sign({
|
|
1579
|
+
hash: TxEnvelopeTempo.getSignPayload(envelope),
|
|
1580
|
+
})
|
|
1581
|
+
const signed = TxEnvelopeTempo.serialize(envelope, {
|
|
1582
|
+
signature: SignatureEnvelope.from(signature),
|
|
1583
|
+
})
|
|
1584
|
+
const receipt = (await Relay.getClient().request({
|
|
1585
|
+
method: 'eth_sendRawTransactionSync' as never,
|
|
1586
|
+
params: [signed],
|
|
1587
|
+
})) as { feePayer?: string | undefined }
|
|
1588
|
+
|
|
1589
|
+
expect(receipt.feePayer).not.toBe(feePayerAccount.address.toLowerCase())
|
|
1590
|
+
})
|
|
1591
|
+
},
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: path C — no sponsorship', () => {
|
|
1595
|
+
let server: Server
|
|
1596
|
+
let client: ReturnType<typeof Relay.getClient>
|
|
1597
|
+
let requests: RpcRequest.RpcRequest[] = []
|
|
1598
|
+
|
|
1599
|
+
beforeAll(async () => {
|
|
1600
|
+
server = await Relay.createServer(
|
|
1601
|
+
(
|
|
1602
|
+
await Relay.relay({
|
|
1603
|
+
features: 'all',
|
|
1604
|
+
onRequest: async (request) => {
|
|
1605
|
+
requests.push(request)
|
|
1606
|
+
},
|
|
1607
|
+
})
|
|
1608
|
+
).listener,
|
|
1609
|
+
)
|
|
1610
|
+
client = Relay.getClient({ url: server.url })
|
|
1611
|
+
})
|
|
1612
|
+
|
|
1613
|
+
afterAll(() => {
|
|
1614
|
+
server.close()
|
|
1615
|
+
})
|
|
1616
|
+
|
|
1617
|
+
afterEach(() => {
|
|
1618
|
+
requests = []
|
|
1619
|
+
})
|
|
1620
|
+
|
|
1621
|
+
test('behavior: resolves fee token and fills without sponsorship', async () => {
|
|
1622
|
+
const result = await fillTransaction(client, {
|
|
1623
|
+
account: userAccount.address,
|
|
1624
|
+
calls: [transferCall()],
|
|
1625
|
+
})
|
|
1626
|
+
|
|
1627
|
+
expect(result.transaction.feePayerSignature).toBeUndefined()
|
|
1628
|
+
expect(result.capabilities?.['sponsored']).toBe(false)
|
|
1629
|
+
expect(result.capabilities?.['fee']).toBeDefined()
|
|
1630
|
+
// Single fill — no speculative second fill.
|
|
1631
|
+
expect(requests.filter((r) => r.method === 'eth_fillTransaction')).toHaveLength(1)
|
|
1632
|
+
})
|
|
1633
|
+
|
|
1634
|
+
test('behavior: simulate and autoSwap metadata resolve concurrently', async () => {
|
|
1635
|
+
const result = await fillTransaction(client, {
|
|
1636
|
+
account: userAccount.address,
|
|
1637
|
+
calls: [transferCall()],
|
|
1638
|
+
})
|
|
1639
|
+
|
|
1640
|
+
// Capabilities are populated despite parallel execution.
|
|
1641
|
+
expect(result.transaction.gas).toBeDefined()
|
|
1642
|
+
expect(result.transaction.nonce).toBeDefined()
|
|
1643
|
+
expect(result.capabilities?.['fee']).toBeDefined()
|
|
1644
|
+
expect(result.capabilities?.['sponsored']).toBe(false)
|
|
1645
|
+
})
|
|
1646
|
+
})
|
|
1647
|
+
|
|
1648
|
+
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: fee token resolution', () => {
|
|
1649
|
+
const feeTokenAccount = Relay.accounts[0]!
|
|
1650
|
+
const preferredToken = Relay.addresses.alphaUsd
|
|
1651
|
+
let server: Server
|
|
1652
|
+
let client: ReturnType<typeof Relay.getClient>
|
|
1653
|
+
|
|
1654
|
+
beforeAll(async () => {
|
|
1655
|
+
// feeTokenAccount is faucet-funded with alphaUsd so the balance check passes.
|
|
1656
|
+
// Set on-chain fee token preference.
|
|
1657
|
+
await Actions.fee.setUserToken(Relay.getClient(), {
|
|
1658
|
+
account: feeTokenAccount,
|
|
1659
|
+
token: preferredToken,
|
|
1660
|
+
})
|
|
1661
|
+
|
|
1662
|
+
server = await Relay.createServer(
|
|
1663
|
+
(
|
|
1664
|
+
await Relay.relay({
|
|
1665
|
+
features: 'all',
|
|
1666
|
+
tokens: localnetTokens,
|
|
1667
|
+
})
|
|
1668
|
+
).listener,
|
|
1669
|
+
)
|
|
1670
|
+
client = Relay.getClient({ url: server.url })
|
|
1671
|
+
})
|
|
1672
|
+
|
|
1673
|
+
afterAll(() => {
|
|
1674
|
+
server.close()
|
|
1675
|
+
})
|
|
1676
|
+
|
|
1677
|
+
test('behavior: uses explicitly provided feeToken', async () => {
|
|
1678
|
+
const feeToken = '0x20c0000000000000000000000000000000000001'
|
|
1679
|
+
const { transaction } = await fillTransaction(client, {
|
|
1680
|
+
account: feeTokenAccount.address,
|
|
1681
|
+
calls: [transferCall()],
|
|
1682
|
+
feeToken,
|
|
1683
|
+
})
|
|
1684
|
+
|
|
1685
|
+
expect(transaction.feeToken).toBe(feeToken)
|
|
1686
|
+
})
|
|
1687
|
+
|
|
1688
|
+
test('behavior: resolves to onchain user token when it has balance', async () => {
|
|
1689
|
+
const { transaction } = await fillTransaction(client, {
|
|
1690
|
+
account: feeTokenAccount.address,
|
|
1691
|
+
calls: [transferCall()],
|
|
1692
|
+
})
|
|
1693
|
+
|
|
1694
|
+
expect(transaction.feeToken).toBe(preferredToken)
|
|
1695
|
+
})
|
|
1696
|
+
|
|
1697
|
+
test('behavior: resolves to highest-balance token from token list', async () => {
|
|
1698
|
+
// Create two fresh TIP-20s and mint different amounts to an account that is
|
|
1699
|
+
// NOT faucet-funded, so only these two balances influence resolution
|
|
1700
|
+
// (a faucet-funded account would carry genesis balances that dominate).
|
|
1701
|
+
// The relay's tokenlist is restricted to just these two so the
|
|
1702
|
+
// highest-balance resolver picks between them deterministically.
|
|
1703
|
+
const freshAccount = Relay.accounts[5]!
|
|
1704
|
+
const rpc = Relay.getClient({ account: Relay.accounts[0]! })
|
|
1705
|
+
const { token: lowUsd } = await Actions.token.createSync(rpc, {
|
|
1706
|
+
name: 'LowUSD',
|
|
1707
|
+
symbol: 'LowUSD',
|
|
1708
|
+
currency: 'USD',
|
|
1709
|
+
quoteToken: Relay.addresses.alphaUsd,
|
|
1710
|
+
})
|
|
1711
|
+
const { token: highUsd } = await Actions.token.createSync(rpc, {
|
|
1712
|
+
name: 'HighUSD',
|
|
1713
|
+
symbol: 'HighUSD',
|
|
1714
|
+
currency: 'USD',
|
|
1715
|
+
quoteToken: Relay.addresses.alphaUsd,
|
|
1716
|
+
})
|
|
1717
|
+
await sendTransactionSync(rpc, {
|
|
1718
|
+
calls: [
|
|
1719
|
+
Actions.token.grantRoles.call(caller, {
|
|
1720
|
+
token: lowUsd,
|
|
1721
|
+
role: 'issuer',
|
|
1722
|
+
to: rpc.account!.address,
|
|
1723
|
+
}),
|
|
1724
|
+
Actions.token.grantRoles.call(caller, {
|
|
1725
|
+
token: highUsd,
|
|
1726
|
+
role: 'issuer',
|
|
1727
|
+
to: rpc.account!.address,
|
|
1728
|
+
}),
|
|
1729
|
+
Actions.token.mint.call(caller, {
|
|
1730
|
+
token: lowUsd,
|
|
1731
|
+
amount: parseUnits('100', 6),
|
|
1732
|
+
to: freshAccount.address,
|
|
1733
|
+
}),
|
|
1734
|
+
Actions.token.mint.call(caller, {
|
|
1735
|
+
token: highUsd,
|
|
1736
|
+
amount: parseUnits('500', 6),
|
|
1737
|
+
to: freshAccount.address,
|
|
1738
|
+
}),
|
|
1739
|
+
],
|
|
1740
|
+
})
|
|
1741
|
+
|
|
1742
|
+
const customServer = await Relay.createServer(
|
|
1743
|
+
(
|
|
1744
|
+
await Relay.relay({
|
|
1745
|
+
features: 'all',
|
|
1746
|
+
tokens: [
|
|
1747
|
+
{ address: lowUsd, decimals: 6, name: 'LowUSD', symbol: 'LowUSD' },
|
|
1748
|
+
{ address: highUsd, decimals: 6, name: 'HighUSD', symbol: 'HighUSD' },
|
|
1749
|
+
],
|
|
1750
|
+
})
|
|
1751
|
+
).listener,
|
|
1752
|
+
)
|
|
1753
|
+
const customClient = Relay.getClient({ url: customServer.url })
|
|
1754
|
+
|
|
1755
|
+
const { transaction } = await fillTransaction(customClient, {
|
|
1756
|
+
account: freshAccount.address,
|
|
1757
|
+
calls: [
|
|
1758
|
+
Actions.token.transfer.call(caller, { token: highUsd, to: recipient.address, amount: 1n }),
|
|
1759
|
+
],
|
|
1760
|
+
})
|
|
1761
|
+
customServer.close()
|
|
1762
|
+
|
|
1763
|
+
// highUsd has the higher balance (500 > 100).
|
|
1764
|
+
expect(transaction.feeToken?.toLowerCase()).toBe(highUsd.toLowerCase())
|
|
1765
|
+
})
|
|
1766
|
+
|
|
1767
|
+
test('behavior: falls back to pathUSD when no preference or balances', async () => {
|
|
1768
|
+
const freshAccount = Relay.accounts[10]!
|
|
1769
|
+
const { transaction } = await fillTransaction(client, {
|
|
1770
|
+
account: freshAccount.address,
|
|
1771
|
+
calls: [
|
|
1772
|
+
Actions.token.transfer.call(caller, {
|
|
1773
|
+
token: Relay.addresses.alphaUsd,
|
|
1774
|
+
to: freshAccount.address,
|
|
1775
|
+
amount: 0n,
|
|
1776
|
+
}),
|
|
1777
|
+
],
|
|
1778
|
+
})
|
|
1779
|
+
|
|
1780
|
+
expect(transaction.feeToken).toBeUndefined()
|
|
1781
|
+
})
|
|
1782
|
+
})
|
|
1783
|
+
|
|
1784
|
+
describe.skipIf(Runtime.get().mode !== 'localnet')('behavior: error capabilities', () => {
|
|
1785
|
+
let server: Server
|
|
1786
|
+
let client: ReturnType<typeof Relay.getClient>
|
|
1787
|
+
|
|
1788
|
+
beforeAll(async () => {
|
|
1789
|
+
server = await Relay.createServer(
|
|
1790
|
+
(
|
|
1791
|
+
await Relay.relay({
|
|
1792
|
+
features: 'all',
|
|
1793
|
+
getClient: Relay.readClient('https://rpc.moderato.tempo.xyz', tempoModerato.id),
|
|
1794
|
+
})
|
|
1795
|
+
).listener,
|
|
1796
|
+
)
|
|
1797
|
+
client = Relay.getClient({ url: server.url })
|
|
1798
|
+
})
|
|
1799
|
+
|
|
1800
|
+
afterAll(() => {
|
|
1801
|
+
server.close()
|
|
1802
|
+
})
|
|
1803
|
+
|
|
1804
|
+
// Skipped: this asserts an inline snapshot of the sender's live moderato
|
|
1805
|
+
// `balanceDiffs`, which depends on that account's balances on the hosted
|
|
1806
|
+
// moderato testnet at capture time — not reproducible in the disposable
|
|
1807
|
+
// localnet harness. The sibling error/JSON-RPC error tests below cover the
|
|
1808
|
+
// same relay error-capability behavior without a live-balance snapshot.
|
|
1809
|
+
test.skip('behavior: returns requireFunds on InsufficientBalance when errors capability is enabled', async () => {
|
|
1810
|
+
const sender = Relay.accounts[10]!
|
|
1811
|
+
|
|
1812
|
+
const result = await fillTransaction(client, {
|
|
1813
|
+
account: sender.address,
|
|
1814
|
+
calls: [
|
|
1815
|
+
Actions.token.transfer.call(caller, {
|
|
1816
|
+
token: Relay.addresses.alphaUsd,
|
|
1817
|
+
to: recipient.address,
|
|
1818
|
+
amount: parseUnits('100', 6),
|
|
1819
|
+
}),
|
|
1820
|
+
],
|
|
1821
|
+
capabilities: { errors: true },
|
|
1822
|
+
})
|
|
1823
|
+
|
|
1824
|
+
expect(result.capabilities).toMatchInlineSnapshot(`
|
|
1825
|
+
{
|
|
1826
|
+
"balanceDiffs": {
|
|
1827
|
+
"0x0eB552e73e6f8E0922749e0fB08af2a71ECb2b7F": [
|
|
1828
|
+
{
|
|
1829
|
+
"address": "0x20c0000000000000000000000000000000000001",
|
|
1830
|
+
"decimals": 6,
|
|
1831
|
+
"direction": "outgoing",
|
|
1832
|
+
"formatted": "100",
|
|
1833
|
+
"name": "AlphaUSD",
|
|
1834
|
+
"recipients": [
|
|
1835
|
+
"0xAF4311d557fBC876059e39306ec1f3343753df29",
|
|
1836
|
+
],
|
|
1837
|
+
"symbol": "AlphaUSD",
|
|
1838
|
+
"value": "0x5f5e100",
|
|
1839
|
+
},
|
|
1840
|
+
],
|
|
1841
|
+
},
|
|
1842
|
+
"error": {
|
|
1843
|
+
"abiItem": {
|
|
1844
|
+
"inputs": [
|
|
1845
|
+
{
|
|
1846
|
+
"name": "available",
|
|
1847
|
+
"type": "uint256",
|
|
1848
|
+
},
|
|
1849
|
+
{
|
|
1850
|
+
"name": "required",
|
|
1851
|
+
"type": "uint256",
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
"name": "token",
|
|
1855
|
+
"type": "address",
|
|
1856
|
+
},
|
|
1857
|
+
],
|
|
1858
|
+
"name": "InsufficientBalance",
|
|
1859
|
+
"type": "error",
|
|
1860
|
+
},
|
|
1861
|
+
"data": "0x832f98b500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000020c0000000000000000000000000000000000001",
|
|
1862
|
+
"errorName": "InsufficientBalance",
|
|
1863
|
+
"message": "Insufficient balance. Required: 100000000, available: 0.",
|
|
1864
|
+
},
|
|
1865
|
+
"requireFunds": {
|
|
1866
|
+
"amount": "0x5f5e100",
|
|
1867
|
+
"decimals": 6,
|
|
1868
|
+
"formatted": "100",
|
|
1869
|
+
"symbol": "AlphaUSD",
|
|
1870
|
+
"token": "0x20C0000000000000000000000000000000000001",
|
|
1871
|
+
},
|
|
1872
|
+
"sponsored": false,
|
|
1873
|
+
}
|
|
1874
|
+
`)
|
|
1875
|
+
})
|
|
1876
|
+
|
|
1877
|
+
test('behavior: returns error capability on generic revert when errors capability is enabled', async () => {
|
|
1878
|
+
const sender = Relay.accounts[10]!
|
|
1879
|
+
|
|
1880
|
+
const result = await fillTransaction(client, {
|
|
1881
|
+
account: sender.address,
|
|
1882
|
+
calls: [
|
|
1883
|
+
Actions.token.grantRoles.call(caller, {
|
|
1884
|
+
token: Relay.addresses.alphaUsd,
|
|
1885
|
+
role: 'issuer',
|
|
1886
|
+
to: sender.address,
|
|
1887
|
+
}),
|
|
1888
|
+
],
|
|
1889
|
+
capabilities: { errors: true },
|
|
1890
|
+
})
|
|
1891
|
+
|
|
1892
|
+
expect(result.capabilities).toMatchInlineSnapshot(`
|
|
1893
|
+
{
|
|
1894
|
+
"error": {
|
|
1895
|
+
"abiItem": {
|
|
1896
|
+
"inputs": [],
|
|
1897
|
+
"name": "Unauthorized",
|
|
1898
|
+
"type": "error",
|
|
1899
|
+
},
|
|
1900
|
+
"data": "0x82b42900",
|
|
1901
|
+
"errorName": "Unauthorized",
|
|
1902
|
+
"message": "Unauthorized.",
|
|
1903
|
+
},
|
|
1904
|
+
"sponsored": false,
|
|
1905
|
+
}
|
|
1906
|
+
`)
|
|
1907
|
+
})
|
|
1908
|
+
|
|
1909
|
+
test('default: throws JSON-RPC error on InsufficientBalance', async () => {
|
|
1910
|
+
const sender = Relay.accounts[10]!
|
|
1911
|
+
const error = await fillTransaction(client, {
|
|
1912
|
+
account: sender.address,
|
|
1913
|
+
calls: [
|
|
1914
|
+
Actions.token.transfer.call(caller, {
|
|
1915
|
+
token: Relay.addresses.alphaUsd,
|
|
1916
|
+
to: recipient.address,
|
|
1917
|
+
amount: parseUnits('100', 6),
|
|
1918
|
+
}),
|
|
1919
|
+
],
|
|
1920
|
+
}).then(
|
|
1921
|
+
() => undefined,
|
|
1922
|
+
(e: BaseError) => e,
|
|
1923
|
+
)
|
|
1924
|
+
expect(error).toBeDefined()
|
|
1925
|
+
// Walk the viem error chain to the underlying RPC error. The default path
|
|
1926
|
+
// surfaces the chain revert as a JSON-RPC error (`code: 3`) with the
|
|
1927
|
+
// ABI-encoded `InsufficientBalance(uint256, uint256, address)` selector.
|
|
1928
|
+
const rpc = error?.walk(isRpcError) as { data?: string } | undefined
|
|
1929
|
+
expect(rpc?.data?.startsWith('0x832f98b5')).toBe(true)
|
|
1930
|
+
})
|
|
1931
|
+
|
|
1932
|
+
test('default: throws JSON-RPC error on generic revert', async () => {
|
|
1933
|
+
const sender = Relay.accounts[10]!
|
|
1934
|
+
const error = await fillTransaction(client, {
|
|
1935
|
+
account: sender.address,
|
|
1936
|
+
calls: [
|
|
1937
|
+
Actions.token.grantRoles.call(caller, {
|
|
1938
|
+
token: Relay.addresses.alphaUsd,
|
|
1939
|
+
role: 'issuer',
|
|
1940
|
+
to: sender.address,
|
|
1941
|
+
}),
|
|
1942
|
+
],
|
|
1943
|
+
}).then(
|
|
1944
|
+
() => undefined,
|
|
1945
|
+
(e: BaseError) => e,
|
|
1946
|
+
)
|
|
1947
|
+
expect(error).toBeDefined()
|
|
1948
|
+
const rpc = error?.walk(isRpcError) as { data?: string } | undefined
|
|
1949
|
+
// ABI-encoded `Unauthorized()`.
|
|
1950
|
+
expect(rpc?.data).toBe('0x82b42900')
|
|
1951
|
+
})
|
|
1952
|
+
|
|
1953
|
+
test('behavior: explicit `errors: false` matches default JSON-RPC error behavior', async () => {
|
|
1954
|
+
const sender = Relay.accounts[10]!
|
|
1955
|
+
const error = await fillTransaction(client, {
|
|
1956
|
+
account: sender.address,
|
|
1957
|
+
calls: [
|
|
1958
|
+
Actions.token.transfer.call(caller, {
|
|
1959
|
+
token: Relay.addresses.alphaUsd,
|
|
1960
|
+
to: recipient.address,
|
|
1961
|
+
amount: parseUnits('100', 6),
|
|
1962
|
+
}),
|
|
1963
|
+
],
|
|
1964
|
+
capabilities: { errors: false } as never,
|
|
1965
|
+
}).then(
|
|
1966
|
+
() => undefined,
|
|
1967
|
+
(e: BaseError) => e,
|
|
1968
|
+
)
|
|
1969
|
+
expect(error).toBeDefined()
|
|
1970
|
+
const rpc = error?.walk(isRpcError) as { data?: string } | undefined
|
|
1971
|
+
expect(rpc?.data?.startsWith('0x832f98b5')).toBe(true)
|
|
1972
|
+
})
|
|
1973
|
+
})
|
|
1974
|
+
|
|
1975
|
+
function isRpcError(e: unknown): boolean {
|
|
1976
|
+
return typeof e === 'object' && e !== null && 'code' in e && (e as { code: unknown }).code === 3
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// Skipped: this drives a real mainnet fill against the hosted presto RPC using
|
|
1980
|
+
// a hard-coded sender that must hold live USDC.e (and no PathUSD) for autoSwap
|
|
1981
|
+
// to trigger — external state that isn't reproducible in the localnet harness.
|
|
1982
|
+
describe.skip('behavior: mainnet autoSwap with USDC.e → PathUSD', () => {
|
|
1983
|
+
let server: Server
|
|
1984
|
+
let mainnetClient: ReturnType<typeof Relay.getClient>
|
|
1985
|
+
|
|
1986
|
+
const mainnetUsdce = '0x20c000000000000000000000b9537d11c60e8b50' as const
|
|
1987
|
+
const mainnetPathUsd = '0x20c0000000000000000000000000000000000000' as const
|
|
1988
|
+
const mainnetSender = '0xb472f3ca15f34Db22d43FA503043F1e6541AC085' as const
|
|
1989
|
+
|
|
1990
|
+
beforeAll(async () => {
|
|
1991
|
+
server = await Relay.createServer(
|
|
1992
|
+
(
|
|
1993
|
+
await Relay.relay({
|
|
1994
|
+
features: 'all',
|
|
1995
|
+
getClient: Relay.readClient('https://rpc.presto.tempo.xyz', tempo.id),
|
|
1996
|
+
})
|
|
1997
|
+
).listener,
|
|
1998
|
+
)
|
|
1999
|
+
mainnetClient = Relay.getClient({ url: server.url })
|
|
2000
|
+
})
|
|
2001
|
+
|
|
2002
|
+
afterAll(() => {
|
|
2003
|
+
server.close()
|
|
2004
|
+
})
|
|
2005
|
+
|
|
2006
|
+
test('behavior: auto-swaps USDC.e → PathUSD when sender has USDC.e but no PathUSD', async () => {
|
|
2007
|
+
const result = await fillTransaction(mainnetClient, {
|
|
2008
|
+
account: mainnetSender,
|
|
2009
|
+
calls: [
|
|
2010
|
+
Actions.token.transfer.call(caller, {
|
|
2011
|
+
token: mainnetPathUsd,
|
|
2012
|
+
to: recipient.address,
|
|
2013
|
+
amount: parseUnits('0.5', 6),
|
|
2014
|
+
}),
|
|
2015
|
+
],
|
|
2016
|
+
})
|
|
2017
|
+
|
|
2018
|
+
// Expected: relay detects InsufficientBalance for PathUSD, auto-swaps
|
|
2019
|
+
// USDC.e → PathUSD via DEX, and returns a filled transaction with swap calls.
|
|
2020
|
+
const { transaction, capabilities } = result
|
|
2021
|
+
expect(transaction.gas).toBeDefined()
|
|
2022
|
+
expect(transaction.nonce).toBeDefined()
|
|
2023
|
+
expect(transaction.calls!.length).toBeGreaterThanOrEqual(3) // approve + swap + transfer
|
|
2024
|
+
|
|
2025
|
+
expect(capabilities?.['autoSwap']).toBeDefined()
|
|
2026
|
+
expect(capabilities?.['autoSwap']?.maxIn.token.toLowerCase()).toBe(mainnetUsdce.toLowerCase())
|
|
2027
|
+
expect(capabilities?.['autoSwap']?.minOut.token.toLowerCase()).toBe(
|
|
2028
|
+
mainnetPathUsd.toLowerCase(),
|
|
2029
|
+
)
|
|
2030
|
+
expect(capabilities?.['sponsored']).toBe(false)
|
|
2031
|
+
|
|
2032
|
+
// Should NOT have requireFunds — autoSwap should handle it.
|
|
2033
|
+
expect((capabilities as Record<string, unknown>)?.['requireFunds']).toBeUndefined()
|
|
2034
|
+
})
|
|
2035
|
+
})
|