accounts 0.8.0 → 0.8.2

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cli/Provider.js +1 -1
  3. package/dist/cli/Provider.js.map +1 -1
  4. package/dist/core/Adapter.d.ts +6 -0
  5. package/dist/core/Adapter.d.ts.map +1 -1
  6. package/dist/core/Adapter.js.map +1 -1
  7. package/dist/core/Client.d.ts.map +1 -1
  8. package/dist/core/Client.js +16 -2
  9. package/dist/core/Client.js.map +1 -1
  10. package/dist/core/Provider.d.ts.map +1 -1
  11. package/dist/core/Provider.js +15 -0
  12. package/dist/core/Provider.js.map +1 -1
  13. package/dist/core/Remote.d.ts.map +1 -1
  14. package/dist/core/Remote.js +0 -6
  15. package/dist/core/Remote.js.map +1 -1
  16. package/dist/core/Schema.d.ts +85 -0
  17. package/dist/core/Schema.d.ts.map +1 -1
  18. package/dist/core/Schema.js +1 -0
  19. package/dist/core/Schema.js.map +1 -1
  20. package/dist/core/adapters/dialog.d.ts.map +1 -1
  21. package/dist/core/adapters/dialog.js +8 -1
  22. package/dist/core/adapters/dialog.js.map +1 -1
  23. package/dist/core/adapters/local.d.ts.map +1 -1
  24. package/dist/core/adapters/local.js +19 -5
  25. package/dist/core/adapters/local.js.map +1 -1
  26. package/dist/core/zod/rpc.d.ts +71 -0
  27. package/dist/core/zod/rpc.d.ts.map +1 -1
  28. package/dist/core/zod/rpc.js +25 -0
  29. package/dist/core/zod/rpc.js.map +1 -1
  30. package/dist/server/internal/handlers/relay.js +87 -4
  31. package/dist/server/internal/handlers/relay.js.map +1 -1
  32. package/package.json +4 -4
  33. package/src/cli/Provider.ts +1 -1
  34. package/src/core/Adapter.ts +12 -0
  35. package/src/core/Client.ts +14 -2
  36. package/src/core/Provider.test.ts +103 -4
  37. package/src/core/Provider.ts +19 -0
  38. package/src/core/Remote.ts +0 -7
  39. package/src/core/Schema.test-d.ts +18 -1
  40. package/src/core/Schema.ts +1 -0
  41. package/src/core/adapters/dialog.ts +9 -1
  42. package/src/core/adapters/local.ts +22 -5
  43. package/src/core/zod/rpc.ts +28 -0
  44. package/src/react-native/Provider.test.ts +1 -1
  45. package/src/server/internal/handlers/relay.test.ts +121 -0
  46. package/src/server/internal/handlers/relay.ts +107 -4
@@ -281,6 +281,127 @@ describe('behavior: with app-provided feePayer URL', () => {
281
281
  })
282
282
  })
283
283
 
284
+ describe('behavior: with app-provided feePayer URL + autoSwap', () => {
285
+ let appServer: Server
286
+ let walletServer: Server
287
+ let client: ReturnType<typeof getClient<typeof chain>>
288
+
289
+ beforeAll(async () => {
290
+ // App relay sponsors fees AND has `features: 'all'` so it can recover
291
+ // from InsufficientBalance via autoSwap.
292
+ appServer = await createServer(
293
+ relay({
294
+ chains: [chain],
295
+ features: 'all',
296
+ transports: { [chain.id]: http() },
297
+ feePayer: {
298
+ account: feePayerAccount,
299
+ name: 'App Sponsor',
300
+ url: 'https://app.example.com',
301
+ },
302
+ }).listener,
303
+ )
304
+
305
+ // Wallet relay forwards to the app relay; also has features:'all' so its
306
+ // own fill() can detect upstream `capabilities.error` as InsufficientBalance.
307
+ walletServer = await createServer(
308
+ relay({
309
+ chains: [chain],
310
+ features: 'all',
311
+ transports: { [chain.id]: http() },
312
+ }).listener,
313
+ )
314
+
315
+ client = getClient({ transport: http(walletServer.url) })
316
+ })
317
+
318
+ afterAll(() => {
319
+ appServer.close()
320
+ walletServer.close()
321
+ })
322
+
323
+ test('behavior: autoSwap recovers when external feePayer surfaces InsufficientBalance', async () => {
324
+ const sender = accounts[6]!
325
+
326
+ // Token pair + DEX liquidity. Use alphaUsd as the quote token so the
327
+ // relay can swap alphaUsd → base to cover the deficit.
328
+ const rpc = getClient({ account: accounts[0]! })
329
+ const { token: base } = await Actions.token.createSync(rpc, {
330
+ name: 'External Swap Base',
331
+ symbol: 'EXTBASE',
332
+ currency: 'USD',
333
+ quoteToken: addresses.alphaUsd,
334
+ })
335
+ await sendTransactionSync(rpc, {
336
+ calls: [
337
+ Actions.token.grantRoles.call({ token: base, role: 'issuer', to: rpc.account!.address }),
338
+ Actions.token.mint.call({
339
+ token: base,
340
+ to: rpc.account!.address,
341
+ amount: parseUnits('10000', 6),
342
+ }),
343
+ Actions.token.mint.call({
344
+ token: addresses.alphaUsd,
345
+ to: rpc.account!.address,
346
+ amount: parseUnits('10000', 6),
347
+ }),
348
+ Actions.token.approve.call({
349
+ token: base,
350
+ spender: Addresses.stablecoinDex,
351
+ amount: parseUnits('10000', 6),
352
+ }),
353
+ Actions.token.approve.call({
354
+ token: addresses.alphaUsd,
355
+ spender: Addresses.stablecoinDex,
356
+ amount: parseUnits('10000', 6),
357
+ }),
358
+ ],
359
+ })
360
+ await Actions.dex.createPairSync(rpc, { base })
361
+ await Actions.dex.placeSync(rpc, {
362
+ token: base,
363
+ amount: parseUnits('500', 6),
364
+ type: 'sell',
365
+ tick: Tick.fromPrice('1.001'),
366
+ })
367
+
368
+ // Give sender alphaUsd (fee + swap source) but NO base tokens.
369
+ await Actions.token.mintSync(rpc, {
370
+ token: addresses.alphaUsd,
371
+ amount: parseUnits('1000', 6),
372
+ to: sender.address,
373
+ })
374
+ await Actions.fee.setUserToken(getClient({ account: sender }), { token: addresses.alphaUsd })
375
+
376
+ // Sender attempts to transfer base via the wallet relay, which forwards
377
+ // to the app relay. The app relay returns 200 with capabilities.error =
378
+ // InsufficientBalance and a stub tx; the wallet relay must convert that
379
+ // into a synthetic throw so its own fill() autoSwap branch can recover.
380
+ const transferAmount = parseUnits('5', 6)
381
+ const result = await fillTransaction(client, {
382
+ account: sender.address,
383
+ ...Actions.token.transfer.call({
384
+ token: base,
385
+ to: accounts[7]!.address,
386
+ amount: transferAmount,
387
+ }),
388
+ feePayer: appServer.url as never,
389
+ })
390
+
391
+ const { transaction, capabilities } = result
392
+
393
+ // Tx is filled with the swap calls prepended (approve + buy + transfer).
394
+ expect(transaction.calls).toHaveLength(3)
395
+ expect(transaction.feePayerSignature).toBeDefined()
396
+
397
+ // autoSwap metadata is surfaced.
398
+ expect(capabilities?.autoSwap?.slippage).toBe(0.05)
399
+ expect(capabilities?.autoSwap?.maxIn.symbol).toBe('AlphaUSD')
400
+ expect(capabilities?.autoSwap?.minOut.symbol).toBe('EXTBASE')
401
+ expect(capabilities?.autoSwap?.minOut.formatted).toBe('5')
402
+ })
403
+ })
404
+
284
405
  describe('behavior: chainId path parameter', () => {
285
406
  let server: Server
286
407
  let client: ReturnType<typeof getClient<typeof chain>>
@@ -569,13 +569,40 @@ async function fill(
569
569
  // FIXME: node estimates gas with secp256k1 dummy sig + null feePayerSignature.
570
570
  // Actual tx has larger keychain/webAuthn sigs + real fee payer sig, costing
571
571
  // more intrinsic gas. Mirror the bump from viem's tempo chainConfig.
572
+ // Skip if another relay already bumped (indicated by feePayerSignature).
572
573
  // @ts-expect-error
573
- if (result.tx.gas && result.tx.feePayer)
574
+ if (result.tx.gas && request.feePayer && !result.tx.feePayerSignature)
574
575
  result.tx.gas = Hex.fromNumber(BigInt(result.tx.gas) + 20_000n)
575
- const sponsor = (result as Record<string, any>).capabilities?.sponsor as
576
+ const upstreamCapabilities = (result as { capabilities?: Record<string, unknown> }).capabilities
577
+ const sponsor = upstreamCapabilities?.sponsor as
576
578
  | { address: Address; name?: string; url?: string }
577
579
  | undefined
578
- return { transaction: Utils.normalizeTempoTransaction(result.tx), sponsor }
580
+ // External fee-payer relays surface chain reverts (e.g. InsufficientBalance)
581
+ // inside `capabilities.error` with a stub `tx` instead of throwing. Detect
582
+ // that here and re-throw so the autoSwap branch below can recover the same
583
+ // way it does for the direct-chain path.
584
+ const upstreamError = upstreamCapabilities?.error as
585
+ | { errorName?: string; message?: string; data?: `0x${string}` }
586
+ | undefined
587
+ if (upstreamError?.errorName === 'InsufficientBalance') {
588
+ const synthetic = new Error(upstreamError.message ?? 'InsufficientBalance')
589
+ synthetic.name = 'UpstreamRevertError'
590
+ ;(synthetic as { data?: `0x${string}` | undefined }).data = upstreamError.data
591
+ throw synthetic
592
+ }
593
+ // Reconstruct a `swap` shape from upstream's `capabilities.autoSwap` so the
594
+ // wallet relay's outer code can re-resolve autoSwap metadata locally —
595
+ // otherwise upstream-driven swaps are silently dropped from the response.
596
+ const swap = extractSwapFromCapabilities(upstreamCapabilities?.autoSwap)
597
+ // The chain's `eth_fillTransaction` doesn't echo back `calls`, so merge
598
+ // them in from the original request before normalizing — otherwise the
599
+ // typed envelope built for sponsorship signing throws CallsEmptyError.
600
+ const mergedTx = mergeCallsFromRequest(result.tx as Record<string, unknown>, request)
601
+ return {
602
+ transaction: Utils.normalizeTempoTransaction(mergedTx),
603
+ sponsor,
604
+ ...(swap ? { swap } : {}),
605
+ }
579
606
  } catch (error) {
580
607
  if (!(error instanceof Error)) throw error
581
608
  if (!autoSwap) throw error
@@ -612,8 +639,12 @@ async function fill(
612
639
  const sponsor = (result as Record<string, any>).capabilities?.sponsor as
613
640
  | { address: Address; name?: string; url?: string }
614
641
  | undefined
642
+ const mergedTx = mergeCallsFromRequest(result.tx as Record<string, unknown>, {
643
+ ...request,
644
+ calls: [...swapCalls, ...originalCalls],
645
+ })
615
646
  return {
616
- transaction: Utils.normalizeTempoTransaction(result.tx),
647
+ transaction: Utils.normalizeTempoTransaction(mergedTx),
617
648
  sponsor,
618
649
  swap: {
619
650
  calls: swapCalls,
@@ -1014,3 +1045,75 @@ function buildSwapCalls(
1014
1045
  { to: buy.to, data: buy.data, value: 0n },
1015
1046
  ] as const
1016
1047
  }
1048
+
1049
+ /**
1050
+ * Merges the original fill request into the result tx. The chain's
1051
+ * `eth_fillTransaction` returns only the "filled" gas/nonce/fee fields and
1052
+ * omits envelope inputs like `calls`, `chainId`, `validBefore`, `nonceKey`,
1053
+ * `keyData`, `keyType`, `feePayer`. Without these the typed Tempo envelope
1054
+ * built for sponsorship signing throws `CallsEmptyError` or
1055
+ * `Cannot convert undefined to a BigInt` when serializing.
1056
+ *
1057
+ * Result fields take precedence (they are the chain's authoritative filled
1058
+ * values); request fields fill in everything else. Calls are normalized
1059
+ * separately so legacy `to`/`data`/`value` requests are also supported.
1060
+ */
1061
+ function mergeCallsFromRequest(
1062
+ resultTx: Record<string, unknown>,
1063
+ request: Record<string, unknown>,
1064
+ ): Record<string, unknown> {
1065
+ const merged: Record<string, unknown> = { ...request, ...resultTx }
1066
+ const resultCalls = resultTx.calls
1067
+ if (Array.isArray(resultCalls) && resultCalls.length > 0) return merged
1068
+
1069
+ const reqCalls = request.calls
1070
+ if (Array.isArray(reqCalls) && reqCalls.length > 0) {
1071
+ merged.calls = reqCalls
1072
+ return merged
1073
+ }
1074
+
1075
+ const { to, data, value } = request
1076
+ if (typeof to === 'undefined' && typeof data === 'undefined' && typeof value === 'undefined')
1077
+ return merged
1078
+
1079
+ merged.calls = [
1080
+ {
1081
+ ...(typeof to !== 'undefined' ? { to } : {}),
1082
+ ...(typeof data !== 'undefined' ? { data } : {}),
1083
+ ...(typeof value !== 'undefined' ? { value } : {}),
1084
+ },
1085
+ ]
1086
+ return merged
1087
+ }
1088
+
1089
+ /**
1090
+ * Reconstructs a `swap` shape (matching the inner autoSwap branch's return
1091
+ * value) from an upstream relay's `capabilities.autoSwap`. Used so a wallet
1092
+ * relay forwarding to an external feePayer URL can re-emit autoSwap metadata
1093
+ * locally without losing track of the upstream's swap.
1094
+ */
1095
+ function extractSwapFromCapabilities(autoSwap: unknown):
1096
+ | {
1097
+ calls: readonly { to: Address; data: `0x${string}` }[]
1098
+ tokenIn: Address
1099
+ tokenOut: Address
1100
+ amountOut: bigint
1101
+ maxAmountIn: bigint
1102
+ }
1103
+ | undefined {
1104
+ if (!autoSwap || typeof autoSwap !== 'object') return undefined
1105
+ const a = autoSwap as {
1106
+ calls?: readonly { to: Address; data: `0x${string}` }[]
1107
+ maxIn?: { token?: Address; value?: `0x${string}` }
1108
+ minOut?: { token?: Address; value?: `0x${string}` }
1109
+ }
1110
+ if (!a.calls || !a.maxIn?.token || !a.maxIn.value || !a.minOut?.token || !a.minOut.value)
1111
+ return undefined
1112
+ return {
1113
+ calls: a.calls,
1114
+ tokenIn: a.maxIn.token,
1115
+ tokenOut: a.minOut.token,
1116
+ amountOut: BigInt(a.minOut.value),
1117
+ maxAmountIn: BigInt(a.maxIn.value),
1118
+ }
1119
+ }