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.
- package/CHANGELOG.md +22 -0
- package/dist/cli/Provider.js +1 -1
- package/dist/cli/Provider.js.map +1 -1
- package/dist/core/Adapter.d.ts +6 -0
- package/dist/core/Adapter.d.ts.map +1 -1
- package/dist/core/Adapter.js.map +1 -1
- package/dist/core/Client.d.ts.map +1 -1
- package/dist/core/Client.js +16 -2
- package/dist/core/Client.js.map +1 -1
- package/dist/core/Provider.d.ts.map +1 -1
- package/dist/core/Provider.js +15 -0
- package/dist/core/Provider.js.map +1 -1
- package/dist/core/Remote.d.ts.map +1 -1
- package/dist/core/Remote.js +0 -6
- package/dist/core/Remote.js.map +1 -1
- package/dist/core/Schema.d.ts +85 -0
- package/dist/core/Schema.d.ts.map +1 -1
- package/dist/core/Schema.js +1 -0
- package/dist/core/Schema.js.map +1 -1
- package/dist/core/adapters/dialog.d.ts.map +1 -1
- package/dist/core/adapters/dialog.js +8 -1
- package/dist/core/adapters/dialog.js.map +1 -1
- package/dist/core/adapters/local.d.ts.map +1 -1
- package/dist/core/adapters/local.js +19 -5
- package/dist/core/adapters/local.js.map +1 -1
- package/dist/core/zod/rpc.d.ts +71 -0
- package/dist/core/zod/rpc.d.ts.map +1 -1
- package/dist/core/zod/rpc.js +25 -0
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/server/internal/handlers/relay.js +87 -4
- package/dist/server/internal/handlers/relay.js.map +1 -1
- package/package.json +4 -4
- package/src/cli/Provider.ts +1 -1
- package/src/core/Adapter.ts +12 -0
- package/src/core/Client.ts +14 -2
- package/src/core/Provider.test.ts +103 -4
- package/src/core/Provider.ts +19 -0
- package/src/core/Remote.ts +0 -7
- package/src/core/Schema.test-d.ts +18 -1
- package/src/core/Schema.ts +1 -0
- package/src/core/adapters/dialog.ts +9 -1
- package/src/core/adapters/local.ts +22 -5
- package/src/core/zod/rpc.ts +28 -0
- package/src/react-native/Provider.test.ts +1 -1
- package/src/server/internal/handlers/relay.test.ts +121 -0
- 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.
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|