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,1623 @@
|
|
|
1
|
+
import { AbiEvent, Hex, RpcRequest, RpcResponse } from 'ox'
|
|
2
|
+
import { Transaction as core_Transaction } from 'ox/tempo'
|
|
3
|
+
import {
|
|
4
|
+
type Address,
|
|
5
|
+
type Call,
|
|
6
|
+
type Client,
|
|
7
|
+
createClient,
|
|
8
|
+
decodeFunctionData,
|
|
9
|
+
formatUnits,
|
|
10
|
+
http,
|
|
11
|
+
isAddress,
|
|
12
|
+
type Log,
|
|
13
|
+
parseEventLogs,
|
|
14
|
+
zeroAddress,
|
|
15
|
+
} from 'viem'
|
|
16
|
+
import type { LocalAccount } from 'viem/accounts'
|
|
17
|
+
import { simulateCalls } from 'viem/actions'
|
|
18
|
+
import { Abis, Actions, Addresses, Capabilities, VirtualAddress } from 'viem/tempo'
|
|
19
|
+
|
|
20
|
+
import * as ExecutionError from './internal/executionError.js'
|
|
21
|
+
import type * as RpcSchema from '../internal/RpcSchema.js'
|
|
22
|
+
import { type Handler, from } from '../Handler.js'
|
|
23
|
+
import * as Store from '../internal/Store.js'
|
|
24
|
+
import * as Multisig from './internal/multisig.js'
|
|
25
|
+
import * as Sponsorship from './internal/sponsorship.js'
|
|
26
|
+
import * as Utils from './internal/utils.js'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Instantiates a relay handler that proxies `eth_fillTransaction`
|
|
30
|
+
* with wallet-aware enrichment (fee token resolution, simulation,
|
|
31
|
+
* sponsorship, AMM resolution).
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { Handler } from 'tempo-api/server'
|
|
36
|
+
*
|
|
37
|
+
* const handler = Handler.relay({ getClient })
|
|
38
|
+
*
|
|
39
|
+
* // Plug handler into your server framework of choice:
|
|
40
|
+
* createServer(handler.listener) // Node.js
|
|
41
|
+
* Bun.serve(handler) // Bun
|
|
42
|
+
* Deno.serve(handler) // Deno
|
|
43
|
+
* app.use(handler.listener) // Express
|
|
44
|
+
* app.all('*', c => handler.fetch(c.req.raw)) // Hono
|
|
45
|
+
* export const GET = handler.fetch // Next.js
|
|
46
|
+
* export const POST = handler.fetch // Next.js
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* With sponsorship
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { privateKeyToAccount } from 'viem/accounts'
|
|
54
|
+
* import { Handler } from 'tempo-api/server'
|
|
55
|
+
*
|
|
56
|
+
* const handler = Handler.relay({
|
|
57
|
+
* getClient,
|
|
58
|
+
* feePayer: {
|
|
59
|
+
* account: privateKeyToAccount('0x...'),
|
|
60
|
+
* },
|
|
61
|
+
* })
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @param options - Options.
|
|
65
|
+
* @returns Request handler.
|
|
66
|
+
*/
|
|
67
|
+
export function relay(options: relay.Options): Handler {
|
|
68
|
+
const {
|
|
69
|
+
getClient,
|
|
70
|
+
internal_allowUnsafeUrls = false,
|
|
71
|
+
onRequest,
|
|
72
|
+
path = '/',
|
|
73
|
+
resolveTokens,
|
|
74
|
+
...rest
|
|
75
|
+
} = options
|
|
76
|
+
const feePayerOptions = options.feePayer
|
|
77
|
+
// Adapt the state store into the multisig operation store once, up front.
|
|
78
|
+
const multisigOptions = options.multisig
|
|
79
|
+
? { ...options.multisig, store: Multisig.fromStore(options.multisig.store) }
|
|
80
|
+
: undefined
|
|
81
|
+
|
|
82
|
+
// Resolves the chain's fee-token candidates through the caller-provided
|
|
83
|
+
// resolver. An unconfigured resolver (or a chain it has no list for) yields
|
|
84
|
+
// an empty candidate set.
|
|
85
|
+
const getTokens = async (chainId: number | undefined): Promise<readonly Address[]> => {
|
|
86
|
+
if (!resolveTokens || chainId === undefined) return []
|
|
87
|
+
return await resolveTokens(chainId)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Memoize cache for TIP-20 metadata and user fee-token preferences. Kept
|
|
91
|
+
// opt-in: an always-on cache would pin a raced null preference (set, then
|
|
92
|
+
// fill before the set mines) for the whole TTL.
|
|
93
|
+
const store = options.cache
|
|
94
|
+
|
|
95
|
+
const features = {
|
|
96
|
+
autoSwap: options.autoSwap ?? options.features === 'all',
|
|
97
|
+
feeTokenResolution: options.features === 'all' || Boolean(resolveTokens),
|
|
98
|
+
simulate: options.features === 'all',
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const autoSwap = (() => {
|
|
102
|
+
if (!features.autoSwap) return undefined
|
|
103
|
+
if (options.autoSwap === false) return undefined
|
|
104
|
+
return {
|
|
105
|
+
slippage:
|
|
106
|
+
(typeof options.autoSwap === 'object' ? options.autoSwap?.slippage : undefined) ?? 0.05,
|
|
107
|
+
}
|
|
108
|
+
})()
|
|
109
|
+
|
|
110
|
+
async function handleRequest(
|
|
111
|
+
request: RpcRequest.RpcRequest<RpcSchema.Ox>,
|
|
112
|
+
options?: { chainId?: number | undefined },
|
|
113
|
+
) {
|
|
114
|
+
await onRequest?.(request)
|
|
115
|
+
|
|
116
|
+
// Resolve chainId from: 1) explicit option (URL path), 2) first param object, 3) default chain.
|
|
117
|
+
const params = 'params' in request && Array.isArray(request.params) ? request.params : []
|
|
118
|
+
const first =
|
|
119
|
+
typeof params[0] === 'object' && params[0]
|
|
120
|
+
? (params[0] as Record<string, unknown>)
|
|
121
|
+
: undefined
|
|
122
|
+
const chainId =
|
|
123
|
+
Utils.resolveChainId(first?.['chainId']) ?? options?.chainId ?? getClient().chain?.id
|
|
124
|
+
const client = getClient(chainId)
|
|
125
|
+
|
|
126
|
+
switch (request.method) {
|
|
127
|
+
case 'eth_fillTransaction': {
|
|
128
|
+
const parameters = params[0] as Record<string, unknown>
|
|
129
|
+
const capabilities = (parameters['capabilities'] ?? {}) as Record<string, unknown>
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const from =
|
|
133
|
+
typeof parameters['from'] === 'string' ? (parameters['from'] as Address) : undefined
|
|
134
|
+
const requestFeeToken =
|
|
135
|
+
typeof parameters['feeToken'] === 'string'
|
|
136
|
+
? (parameters['feeToken'] as Address)
|
|
137
|
+
: undefined
|
|
138
|
+
const externalFeePayerUrl =
|
|
139
|
+
typeof parameters['feePayer'] === 'string'
|
|
140
|
+
? normalizeExternalFeePayerUrl(parameters['feePayer'], {
|
|
141
|
+
allowUnsafe: internal_allowUnsafeUrls,
|
|
142
|
+
})
|
|
143
|
+
: undefined
|
|
144
|
+
const requestsSponsorship =
|
|
145
|
+
(!!feePayerOptions || !!externalFeePayerUrl) && parameters['feePayer'] !== false
|
|
146
|
+
// Default to `true`. Dapps that don't render diffs can pass
|
|
147
|
+
// `capabilities.balanceDiffs: false` to skip the post-fill
|
|
148
|
+
// `tempo_simulateV1` round trip (~250-400ms).
|
|
149
|
+
const requireBalanceDiffs = capabilities['balanceDiffs'] !== false
|
|
150
|
+
|
|
151
|
+
const { feePayer: _feePayer, ...normalized } =
|
|
152
|
+
Utils.normalizeFillTransactionRequest(parameters)
|
|
153
|
+
const hasMultisigHints =
|
|
154
|
+
(typeof normalized['multisig'] === 'object' && normalized['multisig'] !== null) ||
|
|
155
|
+
(typeof normalized['multisigInit'] === 'object' &&
|
|
156
|
+
normalized['multisigInit'] !== null) ||
|
|
157
|
+
typeof normalized['multisigSignatureCount'] !== 'undefined'
|
|
158
|
+
const shouldDeferFeePayerSignature =
|
|
159
|
+
feePayerOptions && multisigOptions && hasMultisigHints
|
|
160
|
+
|
|
161
|
+
// When sponsoring, the sponsor's preferred fee token (if set)
|
|
162
|
+
// overrides whatever the consumer asked for — the sponsor is
|
|
163
|
+
// paying, so the sponsor picks. Falls back to the request's
|
|
164
|
+
// feeToken otherwise.
|
|
165
|
+
const sponsoredFeeToken = requestsSponsorship
|
|
166
|
+
? (feePayerOptions?.feeToken ?? requestFeeToken)
|
|
167
|
+
: requestFeeToken
|
|
168
|
+
|
|
169
|
+
const baseTx = {
|
|
170
|
+
...normalized,
|
|
171
|
+
...(typeof chainId !== 'undefined' ? { chainId } : {}),
|
|
172
|
+
...(sponsoredFeeToken ? { feeToken: sponsoredFeeToken } : {}),
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let filled: Awaited<ReturnType<typeof fill>>
|
|
176
|
+
let sponsored = false
|
|
177
|
+
let feeToken = sponsoredFeeToken
|
|
178
|
+
|
|
179
|
+
// Lazily resolve a swap source token when autoSwap needs one.
|
|
180
|
+
const resolveFeeTokenForSwap = from
|
|
181
|
+
? async (insufficientToken: Address) =>
|
|
182
|
+
resolveFeeToken(client, {
|
|
183
|
+
account: from,
|
|
184
|
+
feeToken: undefined,
|
|
185
|
+
store,
|
|
186
|
+
tokens: (await getTokens(chainId)).filter(
|
|
187
|
+
(t) => t.toLowerCase() !== insufficientToken.toLowerCase(),
|
|
188
|
+
),
|
|
189
|
+
})
|
|
190
|
+
: undefined
|
|
191
|
+
|
|
192
|
+
// When no sponsor will pay, prefer fee tokens the user actually
|
|
193
|
+
// holds. Extend the configured token list with any TIP20 token
|
|
194
|
+
// the transaction is calling — typically the token being
|
|
195
|
+
// transferred — so a user transferring USDC.e can pay gas in
|
|
196
|
+
// USDC.e even when the configured list defaults to pathUSD.
|
|
197
|
+
const configuredTokens = await getTokens(chainId)
|
|
198
|
+
const unsponsoredTokens = [
|
|
199
|
+
...configuredTokens,
|
|
200
|
+
...callTargetTokens(baseTx).filter(
|
|
201
|
+
(t) => !configuredTokens.some((rt) => rt.toLowerCase() === t.toLowerCase()),
|
|
202
|
+
),
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
// When the app provides its own fee payer URL, route the fill
|
|
206
|
+
// through that service so it can sign the transaction.
|
|
207
|
+
const fillClient = externalFeePayerUrl
|
|
208
|
+
? createClient({
|
|
209
|
+
chain: client.chain,
|
|
210
|
+
batch: { multicall: { deployless: true } },
|
|
211
|
+
transport: http(externalFeePayerUrl),
|
|
212
|
+
})
|
|
213
|
+
: client
|
|
214
|
+
|
|
215
|
+
if (requestsSponsorship && (externalFeePayerUrl || !feePayerOptions?.validate)) {
|
|
216
|
+
// Path A: sponsorship guaranteed — skip fee token resolution,
|
|
217
|
+
// fill once with feePayer, then parallelize the rest. Taken when:
|
|
218
|
+
// - no validate is configured (no gating), OR
|
|
219
|
+
// - the app supplied its own external fee payer URL (that
|
|
220
|
+
// relay is the sponsorship authority — the wallet's own
|
|
221
|
+
// `validate` only governs the wallet's own fee payer).
|
|
222
|
+
// Default to the chain's first token so the broadcast envelope
|
|
223
|
+
// always carries a feeToken the chain can charge.
|
|
224
|
+
if (!feeToken) feeToken = (await getTokens(chainId))[0]
|
|
225
|
+
const transaction = {
|
|
226
|
+
...baseTx,
|
|
227
|
+
feePayer: true,
|
|
228
|
+
...(feeToken ? { feeToken } : {}),
|
|
229
|
+
}
|
|
230
|
+
if (Sponsorship.isPreparedTransaction(transaction)) {
|
|
231
|
+
filled = {
|
|
232
|
+
transaction: Utils.normalizeTempoTransaction(transaction),
|
|
233
|
+
sponsor: undefined,
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
filled = await fill(fillClient, {
|
|
237
|
+
autoSwap,
|
|
238
|
+
feeToken,
|
|
239
|
+
store,
|
|
240
|
+
resolveFeeToken: resolveFeeTokenForSwap,
|
|
241
|
+
transaction,
|
|
242
|
+
})
|
|
243
|
+
// The chain echoes feeToken: null for sponsored fills served
|
|
244
|
+
// by viem clients that strip feeToken pre-sign; re-inject
|
|
245
|
+
// before the spread in mergeCallsFromRequest drops it.
|
|
246
|
+
if (
|
|
247
|
+
feeToken &&
|
|
248
|
+
filled?.transaction &&
|
|
249
|
+
(filled.transaction as { feeToken?: Address | null }).feeToken == null
|
|
250
|
+
)
|
|
251
|
+
(filled.transaction as { feeToken?: Address }).feeToken = feeToken
|
|
252
|
+
}
|
|
253
|
+
sponsored = true
|
|
254
|
+
} else if (requestsSponsorship && feePayerOptions?.validate) {
|
|
255
|
+
// Path B: sponsorship possible but may be rejected.
|
|
256
|
+
// Speculatively fill the sponsored variant, validate, and only
|
|
257
|
+
// fall back to the unsponsored path on rejection. The accept
|
|
258
|
+
// case (common) avoids the upfront `resolveFeeToken` round
|
|
259
|
+
// trips and the second `eth_fillTransaction`.
|
|
260
|
+
const sponsoredTx = { ...baseTx, feePayer: true }
|
|
261
|
+
|
|
262
|
+
if (Sponsorship.isPreparedTransaction(sponsoredTx)) {
|
|
263
|
+
// Already prepared — skip fills, just validate sponsorship.
|
|
264
|
+
const prepared = {
|
|
265
|
+
transaction: Utils.normalizeTempoTransaction(sponsoredTx),
|
|
266
|
+
sponsor: undefined,
|
|
267
|
+
}
|
|
268
|
+
sponsored = await Sponsorship.shouldSponsor({
|
|
269
|
+
sender: from,
|
|
270
|
+
transaction: prepared.transaction,
|
|
271
|
+
validate: feePayerOptions!.validate,
|
|
272
|
+
})
|
|
273
|
+
filled = prepared
|
|
274
|
+
} else {
|
|
275
|
+
const options = {
|
|
276
|
+
autoSwap,
|
|
277
|
+
store,
|
|
278
|
+
resolveFeeToken: resolveFeeTokenForSwap,
|
|
279
|
+
}
|
|
280
|
+
const fill_sponsored = await fill(fillClient, {
|
|
281
|
+
...options,
|
|
282
|
+
feeToken,
|
|
283
|
+
transaction: sponsoredTx,
|
|
284
|
+
})
|
|
285
|
+
sponsored = await Sponsorship.shouldSponsor({
|
|
286
|
+
sender: from,
|
|
287
|
+
transaction: fill_sponsored.transaction,
|
|
288
|
+
validate: feePayerOptions!.validate,
|
|
289
|
+
})
|
|
290
|
+
if (sponsored) {
|
|
291
|
+
filled = fill_sponsored
|
|
292
|
+
} else {
|
|
293
|
+
// Sponsor rejected — resolve fee token and fill unsponsored.
|
|
294
|
+
const feeToken_unsponsored = features.feeTokenResolution
|
|
295
|
+
? await resolveFeeToken(client, {
|
|
296
|
+
account: from,
|
|
297
|
+
feeToken: requestFeeToken,
|
|
298
|
+
store,
|
|
299
|
+
tokens: unsponsoredTokens,
|
|
300
|
+
})
|
|
301
|
+
: requestFeeToken
|
|
302
|
+
const tx_unsponsored = {
|
|
303
|
+
...baseTx,
|
|
304
|
+
...(feeToken_unsponsored ? { feeToken: feeToken_unsponsored } : {}),
|
|
305
|
+
}
|
|
306
|
+
filled = await fill(client, {
|
|
307
|
+
...options,
|
|
308
|
+
feeToken: feeToken_unsponsored,
|
|
309
|
+
transaction: tx_unsponsored,
|
|
310
|
+
})
|
|
311
|
+
feeToken = feeToken_unsponsored
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
// Path C: no sponsorship configured — resolve fee token, fill once.
|
|
316
|
+
feeToken = features.feeTokenResolution
|
|
317
|
+
? await resolveFeeToken(client, {
|
|
318
|
+
account: from,
|
|
319
|
+
feeToken: requestFeeToken,
|
|
320
|
+
store,
|
|
321
|
+
tokens: unsponsoredTokens,
|
|
322
|
+
})
|
|
323
|
+
: requestFeeToken
|
|
324
|
+
const transaction = { ...baseTx, ...(feeToken ? { feeToken } : {}) }
|
|
325
|
+
filled = await fill(client, {
|
|
326
|
+
autoSwap,
|
|
327
|
+
feeToken,
|
|
328
|
+
store,
|
|
329
|
+
resolveFeeToken: resolveFeeTokenForSwap,
|
|
330
|
+
transaction,
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const transaction_filled = filled.transaction
|
|
335
|
+
const swap = 'swap' in filled ? filled.swap : undefined
|
|
336
|
+
if (!feeToken)
|
|
337
|
+
feeToken =
|
|
338
|
+
(transaction_filled.feeToken as Address | undefined) ?? (await getTokens(chainId))[0]
|
|
339
|
+
|
|
340
|
+
// Parallelize: simulate, fee payer signing, and autoSwap metadata.
|
|
341
|
+
const alreadySigned =
|
|
342
|
+
'feePayerSignature' in transaction_filled &&
|
|
343
|
+
transaction_filled.feePayerSignature != null
|
|
344
|
+
|
|
345
|
+
const calls = extractCalls(transaction_filled)
|
|
346
|
+
const [simulation, transaction_final, autoSwap_, virtualAddresses] = await Promise.all([
|
|
347
|
+
// Simulate and compute balance diffs + fee.
|
|
348
|
+
// When `capabilities.balanceDiffs` is `false`, skip simulate
|
|
349
|
+
// and compute fee directly from cached metadata.
|
|
350
|
+
(async () => {
|
|
351
|
+
if (!features.simulate) return { balanceDiffs: undefined, fee: undefined }
|
|
352
|
+
if (requireBalanceDiffs)
|
|
353
|
+
return simulateAndParseDiffs(client, {
|
|
354
|
+
account: from,
|
|
355
|
+
calls,
|
|
356
|
+
swap,
|
|
357
|
+
feeToken,
|
|
358
|
+
gas: transaction_filled.gas,
|
|
359
|
+
store,
|
|
360
|
+
maxFeePerGas: transaction_filled.maxFeePerGas,
|
|
361
|
+
})
|
|
362
|
+
const fee = await computeFee(client, {
|
|
363
|
+
feeToken,
|
|
364
|
+
gas: transaction_filled.gas,
|
|
365
|
+
store,
|
|
366
|
+
maxFeePerGas: transaction_filled.maxFeePerGas,
|
|
367
|
+
}).catch(() => undefined)
|
|
368
|
+
return { balanceDiffs: undefined, fee }
|
|
369
|
+
})(),
|
|
370
|
+
// Sign as fee payer (if sponsored and not already signed).
|
|
371
|
+
(async () => {
|
|
372
|
+
// Multisig finalization changes the sender signature. Sign after quorum.
|
|
373
|
+
if (shouldDeferFeePayerSignature) return transaction_filled
|
|
374
|
+
// Never sign with the managed fee payer for an app-provided
|
|
375
|
+
// external fee payer URL: that relay is the sponsorship authority,
|
|
376
|
+
// and signing here would bypass this fee payer's `validate` gate.
|
|
377
|
+
if (!(sponsored && feePayerOptions && !externalFeePayerUrl && !alreadySigned))
|
|
378
|
+
return transaction_filled
|
|
379
|
+
return Sponsorship.sign({
|
|
380
|
+
account: feePayerOptions.account,
|
|
381
|
+
onSponsored: feePayerOptions.onSponsored,
|
|
382
|
+
sender: from,
|
|
383
|
+
transaction: transaction_filled,
|
|
384
|
+
})
|
|
385
|
+
})(),
|
|
386
|
+
// Resolve autoSwap metadata (when AMM path was taken).
|
|
387
|
+
resolveAutoSwapMetadata(client, { autoSwap, store, swap }),
|
|
388
|
+
// Resolve virtual-address recipients so wallets can show the
|
|
389
|
+
// eventual master address before signing.
|
|
390
|
+
resolveVirtualAddresses(client, { calls }),
|
|
391
|
+
])
|
|
392
|
+
const { balanceDiffs, fee } = simulation
|
|
393
|
+
|
|
394
|
+
const sponsor = (() => {
|
|
395
|
+
if (!sponsored) return undefined
|
|
396
|
+
// App-provided fee payer: relay back the sponsor from the upstream response.
|
|
397
|
+
if (externalFeePayerUrl) return filled.sponsor
|
|
398
|
+
if (feePayerOptions) return Sponsorship.getSponsor(feePayerOptions)
|
|
399
|
+
return filled.sponsor
|
|
400
|
+
})()
|
|
401
|
+
|
|
402
|
+
return RpcResponse.from(
|
|
403
|
+
{
|
|
404
|
+
result: {
|
|
405
|
+
...(sponsor ? { sponsor } : {}),
|
|
406
|
+
tx: core_Transaction.toRpc(transaction_final as core_Transaction.Transaction),
|
|
407
|
+
capabilities: {
|
|
408
|
+
balanceDiffs,
|
|
409
|
+
fee,
|
|
410
|
+
sponsored: !!sponsor,
|
|
411
|
+
...(sponsor ? { sponsor } : {}),
|
|
412
|
+
...(autoSwap_ ? { autoSwap: autoSwap_ } : {}),
|
|
413
|
+
...(virtualAddresses ? { virtualAddresses } : {}),
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
{ request },
|
|
418
|
+
)
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (!(error instanceof Error)) return Utils.rpcErrorJson(request, error)
|
|
421
|
+
if (capabilities['errors'] !== true) return Utils.rpcErrorJson(request, error)
|
|
422
|
+
|
|
423
|
+
const revert = ExecutionError.parse(error)
|
|
424
|
+
|
|
425
|
+
const parameters = request.params?.[0] as Record<string, unknown>
|
|
426
|
+
const stub = {
|
|
427
|
+
from: parameters['from'],
|
|
428
|
+
to: parameters['to'] ?? null,
|
|
429
|
+
gas: '0x0',
|
|
430
|
+
nonce: '0x0',
|
|
431
|
+
value: '0x0',
|
|
432
|
+
maxFeePerGas: '0x0',
|
|
433
|
+
maxPriorityFeePerGas: '0x0',
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (revert?.errorName === 'InsufficientBalance') {
|
|
437
|
+
const args = revert.args as [bigint, bigint, Address]
|
|
438
|
+
const [available, required, token] = args
|
|
439
|
+
|
|
440
|
+
const normalized = Utils.normalizeFillTransactionRequest(parameters)
|
|
441
|
+
|
|
442
|
+
// Simulate from zero address for optimistic balance diffs.
|
|
443
|
+
const optimisticCalls = normalized ? extractCalls(normalized) : undefined
|
|
444
|
+
const [{ balanceDiffs }, virtualAddresses] = optimisticCalls
|
|
445
|
+
? await Promise.all([
|
|
446
|
+
simulateAndParseDiffs(client, {
|
|
447
|
+
account: zeroAddress,
|
|
448
|
+
calls: optimisticCalls,
|
|
449
|
+
store,
|
|
450
|
+
}),
|
|
451
|
+
resolveVirtualAddresses(client, { calls: optimisticCalls }),
|
|
452
|
+
])
|
|
453
|
+
: [{ balanceDiffs: undefined }, undefined]
|
|
454
|
+
|
|
455
|
+
// Re-key balance diffs from zero address to the real sender.
|
|
456
|
+
const senderDiffs =
|
|
457
|
+
parameters['from'] && balanceDiffs
|
|
458
|
+
? { [parameters['from'] as Address]: balanceDiffs[zeroAddress] ?? [] }
|
|
459
|
+
: balanceDiffs
|
|
460
|
+
|
|
461
|
+
const metadata = await resolveTokenMetadata(client, { token, store }).catch(
|
|
462
|
+
() => undefined,
|
|
463
|
+
)
|
|
464
|
+
const deficit = required - available
|
|
465
|
+
return RpcResponse.from(
|
|
466
|
+
{
|
|
467
|
+
result: {
|
|
468
|
+
tx: stub,
|
|
469
|
+
capabilities: {
|
|
470
|
+
balanceDiffs: senderDiffs,
|
|
471
|
+
error: ExecutionError.serialize(revert),
|
|
472
|
+
requireFunds: metadata
|
|
473
|
+
? {
|
|
474
|
+
amount: Hex.fromNumber(deficit) as `0x${string}`,
|
|
475
|
+
decimals: metadata.decimals,
|
|
476
|
+
formatted: formatUnits(deficit, metadata.decimals),
|
|
477
|
+
token,
|
|
478
|
+
symbol: metadata.symbol,
|
|
479
|
+
}
|
|
480
|
+
: undefined,
|
|
481
|
+
sponsored: false,
|
|
482
|
+
...(virtualAddresses ? { virtualAddresses } : {}),
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
{ request },
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const normalized = Utils.normalizeFillTransactionRequest(parameters)
|
|
491
|
+
const virtualAddresses = normalized
|
|
492
|
+
? await resolveVirtualAddresses(client, { calls: extractCalls(normalized) }).catch(
|
|
493
|
+
() => undefined,
|
|
494
|
+
)
|
|
495
|
+
: undefined
|
|
496
|
+
|
|
497
|
+
return RpcResponse.from(
|
|
498
|
+
{
|
|
499
|
+
result: {
|
|
500
|
+
tx: stub,
|
|
501
|
+
capabilities: {
|
|
502
|
+
error: ExecutionError.serialize(revert),
|
|
503
|
+
sponsored: false,
|
|
504
|
+
...(virtualAddresses ? { virtualAddresses } : {}),
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
{ request },
|
|
509
|
+
)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
case 'eth_signRawTransaction':
|
|
514
|
+
case 'eth_sendRawTransaction':
|
|
515
|
+
case 'eth_sendRawTransactionSync': {
|
|
516
|
+
try {
|
|
517
|
+
const serialized = params[0]
|
|
518
|
+
if (
|
|
519
|
+
multisigOptions &&
|
|
520
|
+
(request.method === 'eth_sendRawTransaction' ||
|
|
521
|
+
request.method === 'eth_sendRawTransactionSync') &&
|
|
522
|
+
typeof serialized === 'string' &&
|
|
523
|
+
Multisig.isMultisigTransaction(serialized as Hex.Hex)
|
|
524
|
+
) {
|
|
525
|
+
const result = await Multisig.handleRawTransaction({
|
|
526
|
+
claimTtl: multisigOptions.claimTtl,
|
|
527
|
+
finalize: multisigOptions.finalize,
|
|
528
|
+
getClient,
|
|
529
|
+
method: request.method,
|
|
530
|
+
request: { params: 'params' in request ? request.params : undefined },
|
|
531
|
+
resolveConfig: multisigOptions.resolveConfig,
|
|
532
|
+
sponsor: feePayerOptions
|
|
533
|
+
? {
|
|
534
|
+
account: feePayerOptions.account,
|
|
535
|
+
feeToken: feePayerOptions.feeToken,
|
|
536
|
+
onSponsored: feePayerOptions.onSponsored,
|
|
537
|
+
resolveFeeToken: async (chainId) => (await getTokens(chainId))[0],
|
|
538
|
+
validate: feePayerOptions.validate,
|
|
539
|
+
}
|
|
540
|
+
: undefined,
|
|
541
|
+
store: multisigOptions.store,
|
|
542
|
+
})
|
|
543
|
+
return RpcResponse.from({ result } as never, { request } as never)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (!feePayerOptions) {
|
|
547
|
+
// eth_signRawTransaction is a signing method that only the
|
|
548
|
+
// relay can fulfill — forwarding it to the RPC node returns
|
|
549
|
+
// an opaque "Method not found". Return an actionable error.
|
|
550
|
+
if ((request as { method: string }).method === 'eth_signRawTransaction') {
|
|
551
|
+
return RpcResponse.from(
|
|
552
|
+
{
|
|
553
|
+
error: {
|
|
554
|
+
code: -32601,
|
|
555
|
+
message:
|
|
556
|
+
'eth_signRawTransaction requires a fee payer to be configured on the relay. ' +
|
|
557
|
+
'Set the `feePayer` option in `Handler.relay()` to enable transaction sponsorship.',
|
|
558
|
+
},
|
|
559
|
+
} as never,
|
|
560
|
+
{ request } as never,
|
|
561
|
+
)
|
|
562
|
+
}
|
|
563
|
+
const result = await client.request(request as never)
|
|
564
|
+
return RpcResponse.from({ result }, { request })
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (
|
|
568
|
+
typeof serialized !== 'string' ||
|
|
569
|
+
!Sponsorship.requestsRawSponsorship(serialized as `0x${string}`)
|
|
570
|
+
) {
|
|
571
|
+
const result = await client.request(request as never)
|
|
572
|
+
return RpcResponse.from({ result }, { request })
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const result = await Sponsorship.handleRawTransaction({
|
|
576
|
+
account: feePayerOptions.account,
|
|
577
|
+
feeToken: feePayerOptions.feeToken,
|
|
578
|
+
getFeeToken: async (chainId) => (await getTokens(chainId))[0],
|
|
579
|
+
getClient,
|
|
580
|
+
method: request.method as Sponsorship.handleRawTransaction.Options['method'],
|
|
581
|
+
onSponsored: feePayerOptions.onSponsored,
|
|
582
|
+
request: { params: 'params' in request ? request.params : undefined },
|
|
583
|
+
validate: feePayerOptions.validate,
|
|
584
|
+
})
|
|
585
|
+
return RpcResponse.from({ result } as never, { request } as never)
|
|
586
|
+
} catch (error) {
|
|
587
|
+
return Utils.rpcErrorJson(request, error)
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
case 'eth_getTransactionByHash':
|
|
592
|
+
case 'eth_getTransactionReceipt': {
|
|
593
|
+
try {
|
|
594
|
+
if (multisigOptions) {
|
|
595
|
+
const aliased = await Multisig.handleGetTransaction({
|
|
596
|
+
getClient,
|
|
597
|
+
method: request.method,
|
|
598
|
+
request: { params: 'params' in request ? request.params : undefined },
|
|
599
|
+
store: multisigOptions.store,
|
|
600
|
+
})
|
|
601
|
+
if (aliased)
|
|
602
|
+
return RpcResponse.from({ result: aliased.result } as never, { request } as never)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const result = await client.request(request as never)
|
|
606
|
+
return RpcResponse.from({ result }, { request })
|
|
607
|
+
} catch (error) {
|
|
608
|
+
return Utils.rpcErrorJson(request, error)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
default: {
|
|
613
|
+
try {
|
|
614
|
+
const result = await client.request(request as never)
|
|
615
|
+
return RpcResponse.from({ result }, { request })
|
|
616
|
+
} catch (error) {
|
|
617
|
+
return Utils.rpcErrorJson(request, error)
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const router = from(rest)
|
|
624
|
+
|
|
625
|
+
async function handlePost(c: { req: { raw: Request; param: (key: string) => string } }) {
|
|
626
|
+
const chainId = Utils.resolveChainId(c.req.param('chainId'))
|
|
627
|
+
const body = await c.req.raw.json()
|
|
628
|
+
const isBatch = Array.isArray(body)
|
|
629
|
+
|
|
630
|
+
if (!isBatch) {
|
|
631
|
+
const request = RpcRequest.from(body as never) as RpcRequest.RpcRequest<RpcSchema.Ox>
|
|
632
|
+
return Response.json(await handleRequest(request, { chainId }))
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const responses = await Promise.all(
|
|
636
|
+
(body as unknown[]).map((item) =>
|
|
637
|
+
handleRequest(RpcRequest.from(item as never) as RpcRequest.RpcRequest<RpcSchema.Ox>, {
|
|
638
|
+
chainId,
|
|
639
|
+
}),
|
|
640
|
+
),
|
|
641
|
+
)
|
|
642
|
+
return Response.json(responses)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
router.post(path, handlePost as never)
|
|
646
|
+
router.post(`${path === '/' ? '' : path}/:chainId`, handlePost as never)
|
|
647
|
+
|
|
648
|
+
return router
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export namespace relay {
|
|
652
|
+
export type Options = from.Options & {
|
|
653
|
+
/**
|
|
654
|
+
* Auto-swap options.
|
|
655
|
+
*/
|
|
656
|
+
autoSwap?:
|
|
657
|
+
| false
|
|
658
|
+
| {
|
|
659
|
+
/** Slippage tolerance (e.g. 0.05 = 5%). @default 0.05 */
|
|
660
|
+
slippage?: number | undefined
|
|
661
|
+
}
|
|
662
|
+
| undefined
|
|
663
|
+
/**
|
|
664
|
+
* Resolves a viem client for a chain id. The handler resolves the client
|
|
665
|
+
* from the `chainId` in the incoming transaction (falling back to the
|
|
666
|
+
* client's default chain). Called with no argument for the default client.
|
|
667
|
+
*/
|
|
668
|
+
getClient: (chainId?: number) => Client
|
|
669
|
+
/**
|
|
670
|
+
* Fee payer / sponsor configuration. When provided, the relay will
|
|
671
|
+
* sign `feePayerSignature` on the filled transaction.
|
|
672
|
+
*/
|
|
673
|
+
feePayer?:
|
|
674
|
+
| {
|
|
675
|
+
/** Account to use as the fee payer. */
|
|
676
|
+
account: LocalAccount
|
|
677
|
+
/** Preferred fee token the sponsor wants to pay fees in. */
|
|
678
|
+
feeToken?: Address | undefined
|
|
679
|
+
/**
|
|
680
|
+
* Called once the fee payer has signed a raw transaction.
|
|
681
|
+
*/
|
|
682
|
+
onSponsored?: Sponsorship.handleRawTransaction.Options['onSponsored']
|
|
683
|
+
/**
|
|
684
|
+
* Validates whether to sponsor the transaction. When omitted, all
|
|
685
|
+
* transactions are sponsored. Return `false` to reject sponsorship.
|
|
686
|
+
*/
|
|
687
|
+
validate?: Sponsorship.Validate | undefined
|
|
688
|
+
/** Sponsor display name returned from `eth_fillTransaction`. */
|
|
689
|
+
name?: string | undefined
|
|
690
|
+
/** Sponsor URL returned from `eth_fillTransaction`. */
|
|
691
|
+
url?: string | undefined
|
|
692
|
+
}
|
|
693
|
+
| undefined
|
|
694
|
+
/**
|
|
695
|
+
* Native multisig configuration. When provided, the relay collects owner
|
|
696
|
+
* approvals from multisig raw transaction submissions and broadcasts once
|
|
697
|
+
* collected owner weight meets the configured threshold.
|
|
698
|
+
*/
|
|
699
|
+
multisig?: Multisig.Options | undefined
|
|
700
|
+
/**
|
|
701
|
+
* Relay features.
|
|
702
|
+
*
|
|
703
|
+
* - `'all'` — enables fee token resolution, auto-swap,
|
|
704
|
+
* fee payer, and simulation (balance diffs + fee breakdown).
|
|
705
|
+
* - `undefined` (default) — only fee payers.
|
|
706
|
+
*/
|
|
707
|
+
features?: 'all' | undefined
|
|
708
|
+
/**
|
|
709
|
+
* Allows app-provided external fee payer URLs to use non-HTTPS protocols
|
|
710
|
+
* or local/private hosts. Intended only for trusted development setups.
|
|
711
|
+
* @default false
|
|
712
|
+
*/
|
|
713
|
+
internal_allowUnsafeUrls?: boolean | undefined
|
|
714
|
+
/**
|
|
715
|
+
* In-process cache backing TIP-20 metadata and user fee-token preference
|
|
716
|
+
* memoization. Omit to read upstream on every fill.
|
|
717
|
+
*/
|
|
718
|
+
cache?: Store.Store | undefined
|
|
719
|
+
/** Function to call before handling the request. */
|
|
720
|
+
onRequest?: ((request: RpcRequest.RpcRequest) => Promise<void>) | undefined
|
|
721
|
+
/** Path to use for the handler. @default "/" */
|
|
722
|
+
path?: string | undefined
|
|
723
|
+
/**
|
|
724
|
+
* Resolves the known fee-token candidates for a chain. The relay checks
|
|
725
|
+
* the caller's balance for each candidate and picks the highest during
|
|
726
|
+
* fee-token resolution. Omit to disable fee-token resolution.
|
|
727
|
+
*/
|
|
728
|
+
resolveTokens?:
|
|
729
|
+
| ((chainId: number) => readonly Address[] | Promise<readonly Address[]>)
|
|
730
|
+
| undefined
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// TODO: cleanup
|
|
735
|
+
async function fill(client: Client, options: fill.Options) {
|
|
736
|
+
const { autoSwap, feeToken, store, transaction: request } = options
|
|
737
|
+
|
|
738
|
+
// Skip re-formatting if already in RPC format (e.g. from viem's fillTransaction).
|
|
739
|
+
const format = (value: Record<string, unknown>) =>
|
|
740
|
+
value['type'] === '0x76' ? value : Utils.formatFillTransactionRequest(client, value)
|
|
741
|
+
|
|
742
|
+
// Re-fill the transaction with prepended swap calls so the user can
|
|
743
|
+
// mint the missing `insufficientToken` (typically the fee token) from
|
|
744
|
+
// a token they already hold. Returns null if no source token is
|
|
745
|
+
// available or autoSwap is disabled.
|
|
746
|
+
async function fillWithSwap(insufficientToken: Address, deficit: bigint) {
|
|
747
|
+
if (!autoSwap) return null
|
|
748
|
+
const sourceToken =
|
|
749
|
+
feeToken && feeToken.toLowerCase() !== insufficientToken.toLowerCase()
|
|
750
|
+
? feeToken
|
|
751
|
+
: await options.resolveFeeToken?.(insufficientToken)
|
|
752
|
+
if (!sourceToken || sourceToken.toLowerCase() === insufficientToken.toLowerCase()) return null
|
|
753
|
+
|
|
754
|
+
const maxAmountIn = deficit + (deficit * BigInt(Math.round(autoSwap.slippage * 1000))) / 1000n
|
|
755
|
+
const originalCalls = (request['calls'] as Call[] | undefined) ?? []
|
|
756
|
+
const swapCalls = buildSwapCalls(client, sourceToken, insufficientToken, deficit, maxAmountIn)
|
|
757
|
+
|
|
758
|
+
const result = await client.request({
|
|
759
|
+
method: 'eth_fillTransaction',
|
|
760
|
+
params: [
|
|
761
|
+
format({
|
|
762
|
+
...request,
|
|
763
|
+
calls: [...swapCalls, ...originalCalls],
|
|
764
|
+
}) as never,
|
|
765
|
+
],
|
|
766
|
+
})
|
|
767
|
+
const sponsor = (result as Record<string, any>)['capabilities']?.sponsor as
|
|
768
|
+
| { address: Address; name?: string; url?: string }
|
|
769
|
+
| undefined
|
|
770
|
+
const mergedTx = mergeCallsFromRequest(result.tx as Record<string, unknown>, {
|
|
771
|
+
...request,
|
|
772
|
+
calls: [...swapCalls, ...originalCalls],
|
|
773
|
+
})
|
|
774
|
+
return {
|
|
775
|
+
transaction: Utils.normalizeTempoTransaction(mergedTx),
|
|
776
|
+
sponsor,
|
|
777
|
+
swap: {
|
|
778
|
+
calls: swapCalls,
|
|
779
|
+
tokenIn: sourceToken,
|
|
780
|
+
tokenOut: insufficientToken,
|
|
781
|
+
amountOut: deficit,
|
|
782
|
+
maxAmountIn,
|
|
783
|
+
},
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
try {
|
|
788
|
+
const formatted = format(request)
|
|
789
|
+
const result = await client.request({
|
|
790
|
+
method: 'eth_fillTransaction',
|
|
791
|
+
params: [formatted as never],
|
|
792
|
+
})
|
|
793
|
+
// FIXME: node estimates gas with secp256k1 dummy sig + null feePayerSignature.
|
|
794
|
+
// Actual tx has larger keychain/webAuthn sigs + real fee payer sig, costing
|
|
795
|
+
// more intrinsic gas. Mirror the bump from viem's tempo chainConfig.
|
|
796
|
+
// Skip if another relay already bumped (indicated by feePayerSignature).
|
|
797
|
+
// @ts-expect-error
|
|
798
|
+
if (result.tx.gas && request.feePayer && !result.tx.feePayerSignature)
|
|
799
|
+
result.tx.gas = Hex.fromNumber(BigInt(result.tx.gas) + 20_000n)
|
|
800
|
+
const upstreamCapabilities = (result as { capabilities?: Record<string, unknown> }).capabilities
|
|
801
|
+
const sponsor = upstreamCapabilities?.['sponsor'] as
|
|
802
|
+
| { address: Address; name?: string; url?: string }
|
|
803
|
+
| undefined
|
|
804
|
+
// External fee-payer relays surface chain reverts (e.g. InsufficientBalance,
|
|
805
|
+
// InsufficientAllowance) inside `capabilities.error` with a stub `tx`
|
|
806
|
+
// instead of throwing. Re-throw all upstream errors so the outer handler
|
|
807
|
+
// either recovers via autoSwap (InsufficientBalance) or propagates the
|
|
808
|
+
// error to the caller. Without this, the wallet would otherwise sign the
|
|
809
|
+
// zero-stub tx with its own fee payer and broadcast garbage.
|
|
810
|
+
const upstreamError = upstreamCapabilities?.['error'] as
|
|
811
|
+
| { errorName?: string; message?: string; data?: `0x${string}` }
|
|
812
|
+
| undefined
|
|
813
|
+
if (upstreamError) {
|
|
814
|
+
const synthetic = new Error(
|
|
815
|
+
upstreamError.message ?? upstreamError.errorName ?? 'UpstreamRevert',
|
|
816
|
+
)
|
|
817
|
+
synthetic.name = 'UpstreamRevertError'
|
|
818
|
+
;(synthetic as { data?: `0x${string}` | undefined }).data = upstreamError.data
|
|
819
|
+
throw synthetic
|
|
820
|
+
}
|
|
821
|
+
// Reconstruct a `swap` shape from upstream's `capabilities.autoSwap` so the
|
|
822
|
+
// wallet relay's outer code can re-resolve autoSwap metadata locally —
|
|
823
|
+
// otherwise upstream-driven swaps are silently dropped from the response.
|
|
824
|
+
const swap = extractSwapFromCapabilities(upstreamCapabilities?.['autoSwap'])
|
|
825
|
+
// The chain's `eth_fillTransaction` doesn't echo back `calls`, so merge
|
|
826
|
+
// them in from the original request before normalizing — otherwise the
|
|
827
|
+
// typed envelope built for sponsorship signing throws CallsEmptyError.
|
|
828
|
+
const mergedTx = mergeCallsFromRequest(result.tx as Record<string, unknown>, request)
|
|
829
|
+
|
|
830
|
+
// The node's `eth_fillTransaction` may pick a fee token the user
|
|
831
|
+
// doesn't yet hold (e.g. one this transaction will mint via a
|
|
832
|
+
// swap). Tempo's gas check runs before the calls execute, so the
|
|
833
|
+
// tx would revert at broadcast with `have 0 want N`. Validate the
|
|
834
|
+
// sender's pre-tx balance against the computed gas cost and, if
|
|
835
|
+
// short, autoSwap a token they do hold into the fee token.
|
|
836
|
+
if (autoSwap && !swap) {
|
|
837
|
+
const fromAddress = request['from'] as Address | undefined
|
|
838
|
+
const resolvedFeeToken = ((mergedTx['feeToken'] as Address | undefined) ?? feeToken) as
|
|
839
|
+
| Address
|
|
840
|
+
| undefined
|
|
841
|
+
const gas = mergedTx['gas'] ? BigInt(mergedTx['gas'] as `0x${string}`) : 0n
|
|
842
|
+
const maxFeePerGas = mergedTx['maxFeePerGas']
|
|
843
|
+
? BigInt(mergedTx['maxFeePerGas'] as `0x${string}`)
|
|
844
|
+
: 0n
|
|
845
|
+
if (fromAddress && resolvedFeeToken && gas > 0n && maxFeePerGas > 0n) {
|
|
846
|
+
const [balance, metadata] = await Promise.all([
|
|
847
|
+
Actions.token
|
|
848
|
+
.getBalance(client, { account: fromAddress, token: resolvedFeeToken })
|
|
849
|
+
.then((balance) => balance.amount)
|
|
850
|
+
.catch(() => 0n),
|
|
851
|
+
resolveTokenMetadata(client, { token: resolvedFeeToken, store }).catch(() => undefined),
|
|
852
|
+
])
|
|
853
|
+
if (metadata) {
|
|
854
|
+
const requiredFee =
|
|
855
|
+
(gas * maxFeePerGas) / 10n ** BigInt(Math.max(0, 18 - metadata.decimals))
|
|
856
|
+
if (balance < requiredFee) {
|
|
857
|
+
// Best-effort: if the swap itself can't be filled (e.g. the
|
|
858
|
+
// source token also has insufficient balance), fall through
|
|
859
|
+
// and return the original tx with the resolved feeToken
|
|
860
|
+
// rather than failing the whole request.
|
|
861
|
+
const swapResult = await fillWithSwap(resolvedFeeToken, requiredFee - balance).catch(
|
|
862
|
+
() => null,
|
|
863
|
+
)
|
|
864
|
+
if (swapResult) return swapResult
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return {
|
|
871
|
+
transaction: Utils.normalizeTempoTransaction(mergedTx),
|
|
872
|
+
sponsor,
|
|
873
|
+
...(swap ? { swap } : {}),
|
|
874
|
+
}
|
|
875
|
+
} catch (error) {
|
|
876
|
+
if (!(error instanceof Error)) throw error
|
|
877
|
+
if (!autoSwap) throw error
|
|
878
|
+
|
|
879
|
+
const revert = ExecutionError.parse(error)
|
|
880
|
+
if (revert?.errorName !== 'InsufficientBalance') throw error
|
|
881
|
+
|
|
882
|
+
const [available, required, token] = revert.args
|
|
883
|
+
if (typeof available === 'undefined' || typeof required === 'undefined' || !token) throw error
|
|
884
|
+
|
|
885
|
+
const swapResult = await fillWithSwap(token as Address, required - available)
|
|
886
|
+
if (!swapResult) throw error
|
|
887
|
+
return swapResult
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
declare namespace fill {
|
|
892
|
+
type Options = {
|
|
893
|
+
autoSwap?: { slippage: number } | undefined
|
|
894
|
+
feeToken?: Address | undefined
|
|
895
|
+
store?: Store.Store | undefined
|
|
896
|
+
resolveFeeToken?: ((insufficientToken: Address) => Promise<Address | undefined>) | undefined
|
|
897
|
+
transaction: Record<string, unknown>
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async function resolveFeeToken(
|
|
902
|
+
client: Client,
|
|
903
|
+
options: resolveFeeToken.Options,
|
|
904
|
+
): Promise<Address | undefined> {
|
|
905
|
+
const { feeToken, account, store, tokens } = options
|
|
906
|
+
if (feeToken) return feeToken
|
|
907
|
+
if (!account) return undefined
|
|
908
|
+
|
|
909
|
+
// Cache the user's preferred fee token for `userTokenCacheTtl` seconds.
|
|
910
|
+
// The on-chain preference rarely changes; a short TTL avoids the
|
|
911
|
+
// ~150-250ms `userTokens` multicall on every unsponsored fill while
|
|
912
|
+
// still picking up updates within a minute. The cached value is just a
|
|
913
|
+
// hint — if the user's balance is 0 we fall back to the highest-balance
|
|
914
|
+
// token from the configured list.
|
|
915
|
+
const getUserToken = () => Actions.fee.getUserToken(client, { account }).catch(() => null)
|
|
916
|
+
const userTokenPromise = store
|
|
917
|
+
? Store.memoize(
|
|
918
|
+
async () => {
|
|
919
|
+
const result = await getUserToken()
|
|
920
|
+
return result ? { address: result.address } : null
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
key: `fee.userToken:${client.chain?.id ?? 0}:${account.toLowerCase()}`,
|
|
924
|
+
store,
|
|
925
|
+
ttl: (options.userTokenCacheTtl ?? 60) * 1000,
|
|
926
|
+
},
|
|
927
|
+
)
|
|
928
|
+
: getUserToken()
|
|
929
|
+
|
|
930
|
+
const [userToken, balances] = await Promise.all([
|
|
931
|
+
userTokenPromise,
|
|
932
|
+
tokens
|
|
933
|
+
? Promise.all(
|
|
934
|
+
tokens.map(async (token) => ({
|
|
935
|
+
address: token,
|
|
936
|
+
balance: await Actions.token
|
|
937
|
+
.getBalance(client, { account, token })
|
|
938
|
+
.then((balance) => balance.amount)
|
|
939
|
+
.catch(() => 0n),
|
|
940
|
+
})),
|
|
941
|
+
)
|
|
942
|
+
: [],
|
|
943
|
+
])
|
|
944
|
+
|
|
945
|
+
// If on-chain preference is set and user has balance, use it.
|
|
946
|
+
if (userToken) {
|
|
947
|
+
const match = balances.find(
|
|
948
|
+
(b: { address: Address; balance: bigint }) =>
|
|
949
|
+
b.address.toLowerCase() === userToken.address.toLowerCase() && b.balance > 0n,
|
|
950
|
+
)
|
|
951
|
+
if (match) return userToken.address
|
|
952
|
+
|
|
953
|
+
// Token list may not include the preference — check on-chain directly.
|
|
954
|
+
if (!match) {
|
|
955
|
+
try {
|
|
956
|
+
const { amount: balance } = await Actions.token.getBalance(client, {
|
|
957
|
+
account,
|
|
958
|
+
token: userToken.address,
|
|
959
|
+
})
|
|
960
|
+
if (balance > 0n) return userToken.address
|
|
961
|
+
} catch {}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Pick the token with the highest balance.
|
|
966
|
+
let best: { address: Address; balance: bigint } | undefined
|
|
967
|
+
for (const asset of balances) {
|
|
968
|
+
if (asset.balance <= 0n) continue
|
|
969
|
+
if (!best || asset.balance > best.balance) best = asset
|
|
970
|
+
}
|
|
971
|
+
if (best) return best.address
|
|
972
|
+
return undefined
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
declare namespace resolveFeeToken {
|
|
976
|
+
type Options = {
|
|
977
|
+
feeToken?: Address | undefined
|
|
978
|
+
account?: Address | undefined
|
|
979
|
+
store?: Store.Store | undefined
|
|
980
|
+
tokens?: readonly Address[] | undefined
|
|
981
|
+
/** TTL in seconds for the cached `userTokens` lookup. @default 60 */
|
|
982
|
+
userTokenCacheTtl?: number | undefined
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Extracts unique TIP20 token addresses (Tempo `0x20c0…` prefix) that the
|
|
988
|
+
* transaction's calls target. Used as fallback fee-token candidates so a
|
|
989
|
+
* user transferring a token they hold can pay gas in that token even when
|
|
990
|
+
* the configured fee-token list defaults to one they don't hold.
|
|
991
|
+
*/
|
|
992
|
+
function callTargetTokens(transaction: Record<string, unknown>): readonly Address[] {
|
|
993
|
+
const calls = transaction['calls'] as readonly { to?: Address }[] | undefined
|
|
994
|
+
if (!calls) return []
|
|
995
|
+
const out: Address[] = []
|
|
996
|
+
const seen = new Set<string>()
|
|
997
|
+
for (const c of calls) {
|
|
998
|
+
if (!c.to) continue
|
|
999
|
+
const lower = c.to.toLowerCase()
|
|
1000
|
+
if (!lower.startsWith('0x20c0')) continue
|
|
1001
|
+
if (seen.has(lower)) continue
|
|
1002
|
+
seen.add(lower)
|
|
1003
|
+
out.push(c.to)
|
|
1004
|
+
}
|
|
1005
|
+
return out
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// TODO: Replace with viem's Tempo fillTransaction capability type once released.
|
|
1009
|
+
type VirtualAddresses = Record<Address, Address | null>
|
|
1010
|
+
|
|
1011
|
+
async function resolveVirtualAddresses(
|
|
1012
|
+
client: Client,
|
|
1013
|
+
options: { calls: readonly Call[] },
|
|
1014
|
+
): Promise<VirtualAddresses | undefined> {
|
|
1015
|
+
const targets = getVirtualAddressTargets(options.calls)
|
|
1016
|
+
if (targets.length === 0) return undefined
|
|
1017
|
+
|
|
1018
|
+
const masters = new Map<string, { addresses: Address[]; masterId: Hex.Hex }>()
|
|
1019
|
+
for (const address of targets) {
|
|
1020
|
+
const { masterId } = VirtualAddress.parse(address)
|
|
1021
|
+
const lower = masterId.toLowerCase()
|
|
1022
|
+
const entry = masters.get(lower) ?? { addresses: [], masterId }
|
|
1023
|
+
entry.addresses.push(address)
|
|
1024
|
+
masters.set(lower, entry)
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const entries = await Promise.all(
|
|
1028
|
+
[...masters.values()].map(async ({ addresses, masterId }) => {
|
|
1029
|
+
const master = await Actions.virtualAddress.getMasterAddress(client, { masterId })
|
|
1030
|
+
return addresses.map((address) => [address, master] as const)
|
|
1031
|
+
}),
|
|
1032
|
+
)
|
|
1033
|
+
return Object.fromEntries(entries.flat()) as VirtualAddresses
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function getVirtualAddressTargets(calls: readonly Call[]): readonly Address[] {
|
|
1037
|
+
const targets = new Set<Address>()
|
|
1038
|
+
for (const call of calls) {
|
|
1039
|
+
for (const address of [call.to, decodeTransferRecipient(call.data)]) {
|
|
1040
|
+
if (!address || !isAddress(address) || !VirtualAddress.isVirtual(address)) continue
|
|
1041
|
+
targets.add(address.toLowerCase() as Address)
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return [...targets]
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function decodeTransferRecipient(data?: string): Address | undefined {
|
|
1048
|
+
if (!data) return undefined
|
|
1049
|
+
const selector = data.slice(0, 10).toLowerCase()
|
|
1050
|
+
if (!transferSelectors.has(selector)) return undefined
|
|
1051
|
+
try {
|
|
1052
|
+
const { args, functionName } = decodeFunctionData({
|
|
1053
|
+
abi: Abis.tip20,
|
|
1054
|
+
data: data as Hex.Hex,
|
|
1055
|
+
})
|
|
1056
|
+
if (
|
|
1057
|
+
(functionName === 'transfer' || functionName === 'transferWithMemo') &&
|
|
1058
|
+
typeof args[0] === 'string'
|
|
1059
|
+
)
|
|
1060
|
+
return args[0]
|
|
1061
|
+
if (
|
|
1062
|
+
(functionName === 'transferFrom' || functionName === 'transferFromWithMemo') &&
|
|
1063
|
+
typeof args[1] === 'string'
|
|
1064
|
+
)
|
|
1065
|
+
return args[1]
|
|
1066
|
+
} catch {}
|
|
1067
|
+
return undefined
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const transferSelectors = new Set(['0xa9059cbb', '0x95777d59', '0x23b872dd', '0x929c2539'])
|
|
1071
|
+
|
|
1072
|
+
// TODO: cleanup/remove
|
|
1073
|
+
function extractCalls(transaction: Record<string, unknown>): readonly Call[] {
|
|
1074
|
+
const calls = transaction['calls'] as readonly Call[] | undefined
|
|
1075
|
+
if (calls && calls.length > 0)
|
|
1076
|
+
return calls.map((c) => ({
|
|
1077
|
+
...(c.to ? { to: c.to } : {}),
|
|
1078
|
+
...(c.data ? { data: c.data } : {}),
|
|
1079
|
+
...(c.value ? { value: c.value } : {}),
|
|
1080
|
+
})) as readonly Call[]
|
|
1081
|
+
return [
|
|
1082
|
+
{
|
|
1083
|
+
...(transaction['to'] ? { to: transaction['to'] as Address } : {}),
|
|
1084
|
+
...(transaction['data'] ? { data: transaction['data'] as `0x${string}` } : {}),
|
|
1085
|
+
...(transaction['value'] ? { value: transaction['value'] as bigint } : {}),
|
|
1086
|
+
},
|
|
1087
|
+
] as readonly Call[]
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// TODO: cleanup/remove
|
|
1091
|
+
async function simulate(client: Client, options: simulate.Options) {
|
|
1092
|
+
const { account, calls } = options
|
|
1093
|
+
try {
|
|
1094
|
+
return await Actions.simulate.simulateCalls(client, {
|
|
1095
|
+
...(account ? { account } : {}),
|
|
1096
|
+
calls: calls as Call[],
|
|
1097
|
+
traceTransfers: true,
|
|
1098
|
+
})
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
// TODO: Remove fallback once all nodes support tempo_simulateV1.
|
|
1101
|
+
// Fall back to viem's simulateCalls (eth_simulateV1) if the Tempo
|
|
1102
|
+
// method (tempo_simulateV1) is not supported.
|
|
1103
|
+
const code =
|
|
1104
|
+
(error as { code?: number | undefined }).code ??
|
|
1105
|
+
(error as { cause?: { code?: number | undefined } | undefined }).cause?.code
|
|
1106
|
+
if (code !== -32601) throw error
|
|
1107
|
+
const { results } = await simulateCalls(client, {
|
|
1108
|
+
...(account ? { account } : {}),
|
|
1109
|
+
calls: calls as Call[],
|
|
1110
|
+
})
|
|
1111
|
+
return { results, tokenMetadata: undefined }
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
declare namespace simulate {
|
|
1116
|
+
type Options = {
|
|
1117
|
+
account?: Address | undefined
|
|
1118
|
+
calls: readonly Call[]
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async function simulateAndParseDiffs(client: Client, options: simulateAndParseDiffs.Options) {
|
|
1123
|
+
const { account, calls, swap, feeToken, gas, store, maxFeePerGas } = options
|
|
1124
|
+
|
|
1125
|
+
try {
|
|
1126
|
+
const { results, tokenMetadata } = await simulate(client, {
|
|
1127
|
+
account: account === zeroAddress ? undefined : account,
|
|
1128
|
+
calls,
|
|
1129
|
+
})
|
|
1130
|
+
|
|
1131
|
+
// Collect all logs across all call results.
|
|
1132
|
+
const logs: (typeof results)[number]['logs'] = []
|
|
1133
|
+
for (const result of results as { logs?: (typeof logs)[number][] | undefined }[])
|
|
1134
|
+
if (result.logs) logs.push(...result.logs)
|
|
1135
|
+
|
|
1136
|
+
// Build per-token balance diffs relative to the sender.
|
|
1137
|
+
const balanceDiffs = account
|
|
1138
|
+
? await buildBalanceDiffs(client, {
|
|
1139
|
+
account,
|
|
1140
|
+
store,
|
|
1141
|
+
logs,
|
|
1142
|
+
swap,
|
|
1143
|
+
tokenMetadata: tokenMetadata as never,
|
|
1144
|
+
})
|
|
1145
|
+
: {}
|
|
1146
|
+
|
|
1147
|
+
// Compute fee breakdown.
|
|
1148
|
+
const fee = await computeFee(client, {
|
|
1149
|
+
feeToken,
|
|
1150
|
+
gas,
|
|
1151
|
+
store,
|
|
1152
|
+
maxFeePerGas,
|
|
1153
|
+
tokenMetadata: tokenMetadata as never,
|
|
1154
|
+
}).catch(() => undefined)
|
|
1155
|
+
|
|
1156
|
+
return { balanceDiffs, fee }
|
|
1157
|
+
} catch {
|
|
1158
|
+
// Simulation failures should not block the fill response —
|
|
1159
|
+
// return empty diffs with fee computed from transaction fields.
|
|
1160
|
+
const fee = await computeFee(client, { feeToken, gas, store, maxFeePerGas })
|
|
1161
|
+
return { balanceDiffs: undefined, fee }
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
declare namespace simulateAndParseDiffs {
|
|
1166
|
+
type Options = {
|
|
1167
|
+
account?: Address | undefined
|
|
1168
|
+
calls: readonly Call[]
|
|
1169
|
+
swap?: { tokenIn: Address; tokenOut: Address } | undefined
|
|
1170
|
+
feeToken?: Address | undefined
|
|
1171
|
+
gas?: bigint | undefined
|
|
1172
|
+
store?: Store.Store | undefined
|
|
1173
|
+
maxFeePerGas?: bigint | undefined
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
async function buildBalanceDiffs(client: Client, options: buildBalanceDiffs.Options) {
|
|
1178
|
+
const { account, store, logs, swap, tokenMetadata } = options
|
|
1179
|
+
const accountLower = account.toLowerCase()
|
|
1180
|
+
const dexLower = Addresses.stablecoinDex.toLowerCase()
|
|
1181
|
+
const swapTokenIn = swap?.tokenIn.toLowerCase()
|
|
1182
|
+
const swapTokenOut = swap?.tokenOut.toLowerCase()
|
|
1183
|
+
|
|
1184
|
+
const transferLogs = parseEventLogs({
|
|
1185
|
+
abi: [AbiEvent.fromAbi(Abis.tip20, 'Transfer')],
|
|
1186
|
+
eventName: 'Transfer',
|
|
1187
|
+
logs,
|
|
1188
|
+
})
|
|
1189
|
+
const approvalLogs = parseEventLogs({
|
|
1190
|
+
abi: [AbiEvent.fromAbi(Abis.tip20, 'Approval')],
|
|
1191
|
+
eventName: 'Approval',
|
|
1192
|
+
logs,
|
|
1193
|
+
})
|
|
1194
|
+
|
|
1195
|
+
// Track net movement per token: incoming vs outgoing.
|
|
1196
|
+
const tokenMap = new Map<
|
|
1197
|
+
string,
|
|
1198
|
+
{ incoming: bigint; outgoing: bigint; recipients: Set<Address>; token: Address }
|
|
1199
|
+
>()
|
|
1200
|
+
|
|
1201
|
+
// Track total transferred per (token, spender) so we can suppress covered approvals.
|
|
1202
|
+
const transferredBySpender = new Map<string, bigint>()
|
|
1203
|
+
|
|
1204
|
+
for (const log of transferLogs) {
|
|
1205
|
+
const token = log.address.toLowerCase()
|
|
1206
|
+
const fromLower = log.args.from.toLowerCase()
|
|
1207
|
+
const toLower = log.args.to.toLowerCase()
|
|
1208
|
+
|
|
1209
|
+
// Skip swap-related transfers (reported in capabilities.autoSwap instead).
|
|
1210
|
+
if (swap) {
|
|
1211
|
+
if (token === swapTokenIn && fromLower === accountLower && toLower === dexLower) continue
|
|
1212
|
+
if (token === swapTokenOut && fromLower === dexLower && toLower === accountLower) continue
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const entry = tokenMap.get(token) ?? {
|
|
1216
|
+
incoming: 0n,
|
|
1217
|
+
outgoing: 0n,
|
|
1218
|
+
recipients: new Set<Address>(),
|
|
1219
|
+
token: log.address,
|
|
1220
|
+
}
|
|
1221
|
+
if (fromLower === accountLower) {
|
|
1222
|
+
entry.outgoing += log.args.amount
|
|
1223
|
+
entry.recipients.add(log.args.to)
|
|
1224
|
+
const key = `${token}:${toLower}`
|
|
1225
|
+
transferredBySpender.set(key, (transferredBySpender.get(key) ?? 0n) + log.args.amount)
|
|
1226
|
+
}
|
|
1227
|
+
if (toLower === accountLower) entry.incoming += log.args.amount
|
|
1228
|
+
tokenMap.set(token, entry)
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// Treat approvals as outgoing unless the spender already transferred >= approval amount.
|
|
1232
|
+
for (const log of approvalLogs) {
|
|
1233
|
+
if (log.args.owner.toLowerCase() !== accountLower) continue
|
|
1234
|
+
const token = log.address.toLowerCase()
|
|
1235
|
+
|
|
1236
|
+
// Skip swap-related approvals (reported in capabilities.autoSwap instead).
|
|
1237
|
+
if (swap && token === swapTokenIn && log.args.spender.toLowerCase() === dexLower) continue
|
|
1238
|
+
|
|
1239
|
+
const spenderKey = `${token}:${log.args.spender.toLowerCase()}`
|
|
1240
|
+
const transferred = transferredBySpender.get(spenderKey) ?? 0n
|
|
1241
|
+
if (log.args.amount <= transferred) continue
|
|
1242
|
+
|
|
1243
|
+
const entry = tokenMap.get(token) ?? {
|
|
1244
|
+
incoming: 0n,
|
|
1245
|
+
outgoing: 0n,
|
|
1246
|
+
recipients: new Set<Address>(),
|
|
1247
|
+
token: log.address,
|
|
1248
|
+
}
|
|
1249
|
+
entry.outgoing += log.args.amount - transferred
|
|
1250
|
+
entry.recipients.add(log.args.spender)
|
|
1251
|
+
tokenMap.set(token, entry)
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Collect unique tokens that need decimals.
|
|
1255
|
+
const entries = [...tokenMap.values()].filter((e) => {
|
|
1256
|
+
const net = e.outgoing > e.incoming ? e.outgoing - e.incoming : e.incoming - e.outgoing
|
|
1257
|
+
return net > 0n
|
|
1258
|
+
})
|
|
1259
|
+
if (entries.length === 0) return {}
|
|
1260
|
+
|
|
1261
|
+
// Resolve metadata for all tokens in parallel (simulation metadata first, RPC fallback).
|
|
1262
|
+
const metadataMap = new Map<string, { decimals: number; symbol: string; name: string }>()
|
|
1263
|
+
await Promise.all(
|
|
1264
|
+
entries.map(async (entry) => {
|
|
1265
|
+
try {
|
|
1266
|
+
const metadata = await resolveTokenMetadata(client, {
|
|
1267
|
+
token: entry.token,
|
|
1268
|
+
tokenMetadata,
|
|
1269
|
+
store,
|
|
1270
|
+
})
|
|
1271
|
+
metadataMap.set(entry.token.toLowerCase(), metadata)
|
|
1272
|
+
} catch {}
|
|
1273
|
+
}),
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1276
|
+
// Build the diff array for this account.
|
|
1277
|
+
const diffs: Capabilities.BalanceDiff[] = []
|
|
1278
|
+
for (const entry of entries) {
|
|
1279
|
+
const net =
|
|
1280
|
+
entry.outgoing > entry.incoming
|
|
1281
|
+
? entry.outgoing - entry.incoming
|
|
1282
|
+
: entry.incoming - entry.outgoing
|
|
1283
|
+
|
|
1284
|
+
const direction = entry.outgoing > entry.incoming ? 'outgoing' : 'incoming'
|
|
1285
|
+
const meta = metadataMap.get(entry.token.toLowerCase())
|
|
1286
|
+
const decimals = meta?.decimals ?? 0
|
|
1287
|
+
diffs.push({
|
|
1288
|
+
address: entry.token,
|
|
1289
|
+
decimals,
|
|
1290
|
+
direction,
|
|
1291
|
+
formatted: formatUnits(net, decimals),
|
|
1292
|
+
name: meta?.name ?? '',
|
|
1293
|
+
symbol: meta?.symbol ?? '',
|
|
1294
|
+
recipients: [...entry.recipients] as Address[],
|
|
1295
|
+
value: Hex.fromNumber(net) as `0x${string}`,
|
|
1296
|
+
})
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
return { [account]: diffs }
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
declare namespace buildBalanceDiffs {
|
|
1303
|
+
type Options = {
|
|
1304
|
+
account: Address
|
|
1305
|
+
store?: Store.Store | undefined
|
|
1306
|
+
logs: Log[]
|
|
1307
|
+
swap?: { tokenIn: Address; tokenOut: Address } | undefined
|
|
1308
|
+
tokenMetadata: Record<Address, { name: string; symbol: string; currency: string }>
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
async function resolveTokenMetadata(client: Client, options: resolveTokenMetadata.Options) {
|
|
1313
|
+
const { token, tokenMetadata, store } = options
|
|
1314
|
+
const meta = tokenMetadata?.[token] ?? tokenMetadata?.[token.toLowerCase() as Address]
|
|
1315
|
+
// TIP-20 metadata (decimals/symbol/name) is immutable per token, so cache
|
|
1316
|
+
// long-term. Skips the multicall RPC on cache hits.
|
|
1317
|
+
const fetcher = () => Actions.token.getMetadata(client, { token })
|
|
1318
|
+
const fallback = store
|
|
1319
|
+
? await Store.memoize(fetcher, {
|
|
1320
|
+
key: `tokenMetadata:${client.chain?.id ?? 0}:${token.toLowerCase()}`,
|
|
1321
|
+
store,
|
|
1322
|
+
ttl: 24 * 60 * 60 * 1000,
|
|
1323
|
+
})
|
|
1324
|
+
: await fetcher()
|
|
1325
|
+
return {
|
|
1326
|
+
decimals: fallback.decimals ?? 6,
|
|
1327
|
+
symbol: meta?.symbol || fallback.symbol,
|
|
1328
|
+
name: meta?.name || fallback.name,
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
declare namespace resolveTokenMetadata {
|
|
1333
|
+
type Options = {
|
|
1334
|
+
token: Address
|
|
1335
|
+
tokenMetadata?: Record<Address, { name: string; symbol: string; currency: string }> | undefined
|
|
1336
|
+
store?: Store.Store | undefined
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
async function resolveAutoSwapMetadata(client: Client, options: resolveAutoSwapMetadata.Options) {
|
|
1341
|
+
const { autoSwap, store, swap } = options
|
|
1342
|
+
if (!autoSwap || !swap) return undefined
|
|
1343
|
+
const [inMeta, outMeta] = await Promise.all([
|
|
1344
|
+
resolveTokenMetadata(client, { token: swap.tokenIn, store }).catch(() => undefined),
|
|
1345
|
+
resolveTokenMetadata(client, { token: swap.tokenOut, store }).catch(() => undefined),
|
|
1346
|
+
])
|
|
1347
|
+
if (!inMeta || !outMeta) return undefined
|
|
1348
|
+
return {
|
|
1349
|
+
calls: swap.calls.map((c) => ({ to: c.to, data: c.data })),
|
|
1350
|
+
slippage: autoSwap.slippage,
|
|
1351
|
+
maxIn: {
|
|
1352
|
+
token: swap.tokenIn,
|
|
1353
|
+
value: Hex.fromNumber(swap.maxAmountIn) as `0x${string}`,
|
|
1354
|
+
formatted: formatUnits(swap.maxAmountIn, inMeta.decimals),
|
|
1355
|
+
decimals: inMeta.decimals,
|
|
1356
|
+
symbol: inMeta.symbol,
|
|
1357
|
+
name: inMeta.name,
|
|
1358
|
+
},
|
|
1359
|
+
minOut: {
|
|
1360
|
+
token: swap.tokenOut,
|
|
1361
|
+
value: Hex.fromNumber(swap.amountOut) as `0x${string}`,
|
|
1362
|
+
formatted: formatUnits(swap.amountOut, outMeta.decimals),
|
|
1363
|
+
decimals: outMeta.decimals,
|
|
1364
|
+
symbol: outMeta.symbol,
|
|
1365
|
+
name: outMeta.name,
|
|
1366
|
+
},
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
declare namespace resolveAutoSwapMetadata {
|
|
1371
|
+
type Options = {
|
|
1372
|
+
autoSwap?: { slippage: number } | undefined
|
|
1373
|
+
store?: Store.Store | undefined
|
|
1374
|
+
swap?:
|
|
1375
|
+
| {
|
|
1376
|
+
calls: readonly { to: Address; data: `0x${string}` }[]
|
|
1377
|
+
tokenIn: Address
|
|
1378
|
+
tokenOut: Address
|
|
1379
|
+
amountOut: bigint
|
|
1380
|
+
maxAmountIn: bigint
|
|
1381
|
+
}
|
|
1382
|
+
| undefined
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
async function computeFee(client: Client, options: computeFee.Options) {
|
|
1387
|
+
const { feeToken, gas, store, maxFeePerGas, tokenMetadata } = options
|
|
1388
|
+
if (!feeToken || !gas || !maxFeePerGas) return undefined
|
|
1389
|
+
|
|
1390
|
+
try {
|
|
1391
|
+
const metadata = await resolveTokenMetadata(client, { token: feeToken, tokenMetadata, store })
|
|
1392
|
+
const raw = gas * maxFeePerGas
|
|
1393
|
+
const amount = raw / 10n ** BigInt(18 - metadata.decimals)
|
|
1394
|
+
return {
|
|
1395
|
+
amount: Hex.fromNumber(amount) as `0x${string}`,
|
|
1396
|
+
decimals: metadata.decimals,
|
|
1397
|
+
formatted: formatUnits(amount, metadata.decimals),
|
|
1398
|
+
symbol: metadata.symbol,
|
|
1399
|
+
}
|
|
1400
|
+
} catch {
|
|
1401
|
+
return undefined
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
declare namespace computeFee {
|
|
1406
|
+
type Options = {
|
|
1407
|
+
feeToken?: Address | undefined
|
|
1408
|
+
gas?: bigint | undefined
|
|
1409
|
+
store?: Store.Store | undefined
|
|
1410
|
+
maxFeePerGas?: bigint | undefined
|
|
1411
|
+
tokenMetadata?: Record<Address, { name: string; symbol: string; currency: string }> | undefined
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function buildSwapCalls(
|
|
1416
|
+
client: Client,
|
|
1417
|
+
sourceToken: Address,
|
|
1418
|
+
targetToken: Address,
|
|
1419
|
+
deficit: bigint,
|
|
1420
|
+
maxAmountIn: bigint,
|
|
1421
|
+
) {
|
|
1422
|
+
const approve = Actions.token.approve.call(client, {
|
|
1423
|
+
token: sourceToken,
|
|
1424
|
+
spender: Addresses.stablecoinDex,
|
|
1425
|
+
amount: maxAmountIn,
|
|
1426
|
+
})
|
|
1427
|
+
const buy = Actions.dex.buy.call({
|
|
1428
|
+
tokenIn: sourceToken,
|
|
1429
|
+
tokenOut: targetToken,
|
|
1430
|
+
amountOut: deficit,
|
|
1431
|
+
maxAmountIn,
|
|
1432
|
+
})
|
|
1433
|
+
return [
|
|
1434
|
+
{ to: approve.to, data: approve.data, value: 0n },
|
|
1435
|
+
{ to: buy.to, data: buy.data, value: 0n },
|
|
1436
|
+
] as const
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Merges the original fill request into the result tx. The chain's
|
|
1441
|
+
* `eth_fillTransaction` returns only the "filled" gas/nonce/fee fields and
|
|
1442
|
+
* omits envelope inputs like `calls`, `chainId`, `validBefore`, `nonceKey`,
|
|
1443
|
+
* `keyData`, `keyType`, `feePayer`. Without these the typed Tempo envelope
|
|
1444
|
+
* built for sponsorship signing throws `CallsEmptyError` or
|
|
1445
|
+
* `Cannot convert undefined to a BigInt` when serializing.
|
|
1446
|
+
*
|
|
1447
|
+
* Result fields take precedence (they are the chain's authoritative filled
|
|
1448
|
+
* values); request fields fill in everything else. Calls are normalized
|
|
1449
|
+
* separately so legacy `to`/`data`/`value` requests are also supported.
|
|
1450
|
+
*/
|
|
1451
|
+
function mergeCallsFromRequest(
|
|
1452
|
+
resultTx: Record<string, unknown>,
|
|
1453
|
+
request: Record<string, unknown>,
|
|
1454
|
+
): Record<string, unknown> {
|
|
1455
|
+
const merged: Record<string, unknown> = { ...request, ...resultTx }
|
|
1456
|
+
const resultCalls = resultTx['calls']
|
|
1457
|
+
if (Array.isArray(resultCalls) && resultCalls.length > 0) return merged
|
|
1458
|
+
|
|
1459
|
+
const reqCalls = request['calls']
|
|
1460
|
+
if (Array.isArray(reqCalls) && reqCalls.length > 0) {
|
|
1461
|
+
merged['calls'] = reqCalls
|
|
1462
|
+
return merged
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
const { to, data, value } = request
|
|
1466
|
+
if (typeof to === 'undefined' && typeof data === 'undefined' && typeof value === 'undefined')
|
|
1467
|
+
return merged
|
|
1468
|
+
|
|
1469
|
+
merged['calls'] = [
|
|
1470
|
+
{
|
|
1471
|
+
...(typeof to !== 'undefined' ? { to } : {}),
|
|
1472
|
+
...(typeof data !== 'undefined' ? { data } : {}),
|
|
1473
|
+
...(typeof value !== 'undefined' ? { value } : {}),
|
|
1474
|
+
},
|
|
1475
|
+
]
|
|
1476
|
+
return merged
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/** Normalizes external fee-payer relay URLs before forwarding fill payloads. */
|
|
1480
|
+
function normalizeExternalFeePayerUrl(
|
|
1481
|
+
value: string,
|
|
1482
|
+
options: normalizeExternalFeePayerUrl.Options = {},
|
|
1483
|
+
): string {
|
|
1484
|
+
let url: URL
|
|
1485
|
+
try {
|
|
1486
|
+
url = new URL(value)
|
|
1487
|
+
} catch {
|
|
1488
|
+
throw new RpcResponse.InvalidParamsError({ message: 'Invalid fee payer URL.' })
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
const allowUnsafe = options.allowUnsafe === true
|
|
1492
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
1493
|
+
throw new RpcResponse.InvalidParamsError({ message: 'Invalid fee payer URL protocol.' })
|
|
1494
|
+
if (!allowUnsafe && url.protocol !== 'https:')
|
|
1495
|
+
throw new RpcResponse.InvalidParamsError({ message: 'Invalid fee payer URL protocol.' })
|
|
1496
|
+
if (!allowUnsafe && isUnsafeHostname(url.hostname))
|
|
1497
|
+
throw new RpcResponse.InvalidParamsError({ message: 'Invalid fee payer URL host.' })
|
|
1498
|
+
|
|
1499
|
+
url.username = ''
|
|
1500
|
+
url.password = ''
|
|
1501
|
+
url.hash = ''
|
|
1502
|
+
return url.href
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
declare namespace normalizeExternalFeePayerUrl {
|
|
1506
|
+
type Options = {
|
|
1507
|
+
/** Whether to allow non-HTTPS or local/private external fee payer URLs. */
|
|
1508
|
+
allowUnsafe?: boolean | undefined
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function isUnsafeHostname(hostname: string): boolean {
|
|
1513
|
+
const host = normalizeHostname(hostname)
|
|
1514
|
+
if (host === 'localhost' || host.endsWith('.localhost')) return true
|
|
1515
|
+
const ipv4 = parseIpv4(host)
|
|
1516
|
+
if (ipv4) return isUnsafeIpv4(ipv4)
|
|
1517
|
+
if (!host.includes(':')) return false
|
|
1518
|
+
const ipv6 = parseIpv6(host)
|
|
1519
|
+
if (!ipv6) return true
|
|
1520
|
+
return isUnsafeIpv6(ipv6)
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function normalizeHostname(hostname: string): string {
|
|
1524
|
+
return hostname
|
|
1525
|
+
.toLowerCase()
|
|
1526
|
+
.replace(/^\[|\]$/g, '')
|
|
1527
|
+
.split('%')[0]!
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function parseIpv4(value: string): [number, number, number, number] | undefined {
|
|
1531
|
+
const parts = value.split('.')
|
|
1532
|
+
if (parts.length !== 4 || parts.some((part) => !/^\d+$/.test(part))) return undefined
|
|
1533
|
+
const numbers = parts.map((part) => Number(part))
|
|
1534
|
+
if (numbers.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return undefined
|
|
1535
|
+
return numbers as [number, number, number, number]
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
function isUnsafeIpv4(parts: [number, number, number, number]): boolean {
|
|
1539
|
+
const [a, b] = parts
|
|
1540
|
+
if (a === 0 || a === 10 || a === 127) return true
|
|
1541
|
+
if (a === 100 && b >= 64 && b <= 127) return true
|
|
1542
|
+
if (a === 169 && b === 254) return true
|
|
1543
|
+
if (a === 172 && b >= 16 && b <= 31) return true
|
|
1544
|
+
if (a === 192 && b === 168) return true
|
|
1545
|
+
if (a >= 224) return true
|
|
1546
|
+
return false
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function parseIpv6(
|
|
1550
|
+
value: string,
|
|
1551
|
+
): [number, number, number, number, number, number, number, number] | undefined {
|
|
1552
|
+
const halves = value.split('::')
|
|
1553
|
+
if (halves.length > 2) return undefined
|
|
1554
|
+
|
|
1555
|
+
const left = halves[0] ? halves[0].split(':') : []
|
|
1556
|
+
const right = halves[1] ? halves[1].split(':') : []
|
|
1557
|
+
const missing = halves.length === 2 ? 8 - left.length - right.length : 0
|
|
1558
|
+
if (missing < 0) return undefined
|
|
1559
|
+
|
|
1560
|
+
const parts = [...left, ...Array(missing).fill('0'), ...right]
|
|
1561
|
+
if (parts.length !== 8) return undefined
|
|
1562
|
+
|
|
1563
|
+
const numbers = parts.map((part) =>
|
|
1564
|
+
/^[\da-f]{1,4}$/i.test(part) ? Number.parseInt(part, 16) : NaN,
|
|
1565
|
+
)
|
|
1566
|
+
if (numbers.some((part) => !Number.isInteger(part) || part < 0 || part > 0xffff)) return undefined
|
|
1567
|
+
return numbers as [number, number, number, number, number, number, number, number]
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function isUnsafeIpv6(parts: [number, number, number, number, number, number, number, number]) {
|
|
1571
|
+
const [a] = parts
|
|
1572
|
+
if (parts.every((part) => part === 0)) return true
|
|
1573
|
+
if (parts.slice(0, 7).every((part) => part === 0) && parts[7] === 1) return true
|
|
1574
|
+
if ((a & 0xfe00) === 0xfc00) return true
|
|
1575
|
+
if ((a & 0xffc0) === 0xfe80) return true
|
|
1576
|
+
if ((a & 0xffc0) === 0xfec0) return true
|
|
1577
|
+
if ((a & 0xff00) === 0xff00) return true
|
|
1578
|
+
|
|
1579
|
+
const ipv4 = ipv4FromIpv6(parts)
|
|
1580
|
+
if (ipv4) return isUnsafeIpv4(ipv4)
|
|
1581
|
+
return false
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function ipv4FromIpv6(
|
|
1585
|
+
parts: [number, number, number, number, number, number, number, number],
|
|
1586
|
+
): [number, number, number, number] | undefined {
|
|
1587
|
+
const mapped = parts.slice(0, 5).every((part) => part === 0) && parts[5] === 0xffff
|
|
1588
|
+
const compatible = parts.slice(0, 6).every((part) => part === 0)
|
|
1589
|
+
if (!mapped && !compatible) return undefined
|
|
1590
|
+
return [parts[6]! >> 8, parts[6]! & 0xff, parts[7]! >> 8, parts[7]! & 0xff]
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* Reconstructs a `swap` shape (matching the inner autoSwap branch's return
|
|
1595
|
+
* value) from an upstream relay's `capabilities.autoSwap`. Used so a wallet
|
|
1596
|
+
* relay forwarding to an external feePayer URL can re-emit autoSwap metadata
|
|
1597
|
+
* locally without losing track of the upstream's swap.
|
|
1598
|
+
*/
|
|
1599
|
+
function extractSwapFromCapabilities(autoSwap: unknown):
|
|
1600
|
+
| {
|
|
1601
|
+
calls: readonly { to: Address; data: `0x${string}` }[]
|
|
1602
|
+
tokenIn: Address
|
|
1603
|
+
tokenOut: Address
|
|
1604
|
+
amountOut: bigint
|
|
1605
|
+
maxAmountIn: bigint
|
|
1606
|
+
}
|
|
1607
|
+
| undefined {
|
|
1608
|
+
if (!autoSwap || typeof autoSwap !== 'object') return undefined
|
|
1609
|
+
const a = autoSwap as {
|
|
1610
|
+
calls?: readonly { to: Address; data: `0x${string}` }[]
|
|
1611
|
+
maxIn?: { token?: Address; value?: `0x${string}` }
|
|
1612
|
+
minOut?: { token?: Address; value?: `0x${string}` }
|
|
1613
|
+
}
|
|
1614
|
+
if (!a.calls || !a.maxIn?.token || !a.maxIn.value || !a.minOut?.token || !a.minOut.value)
|
|
1615
|
+
return undefined
|
|
1616
|
+
return {
|
|
1617
|
+
calls: a.calls,
|
|
1618
|
+
tokenIn: a.maxIn.token,
|
|
1619
|
+
tokenOut: a.minOut.token,
|
|
1620
|
+
amountOut: BigInt(a.minOut.value),
|
|
1621
|
+
maxAmountIn: BigInt(a.maxIn.value),
|
|
1622
|
+
}
|
|
1623
|
+
}
|