mppx 0.17.3 → 0.17.4
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 +37 -0
- package/dist/Errors.d.ts +94 -0
- package/dist/Errors.js +83 -0
- package/dist/Store.d.ts +199 -11
- package/dist/Store.js +527 -48
- package/dist/cli/cli.js +141 -137
- package/dist/cli/internal.js +28 -6
- package/dist/cli/plugins/index.js +16 -12
- package/dist/cli/validate/challenge.d.ts +4 -0
- package/dist/cli/validate/challenge.js +9 -6
- package/dist/cloudflare/ChannelStore.d.ts +97 -0
- package/dist/cloudflare/ChannelStore.js +304 -0
- package/dist/cloudflare/Channels.d.ts +92 -0
- package/dist/cloudflare/Channels.js +325 -0
- package/dist/cloudflare/index.d.ts +5 -0
- package/dist/cloudflare/index.js +5 -0
- package/dist/internal/sse.js +32 -15
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/stripe/server/Methods.d.ts +2 -0
- package/dist/stripe/server/Methods.js +2 -1
- package/dist/stripe/server/internal/record-payment.d.ts +49 -0
- package/dist/stripe/server/internal/record-payment.js +75 -18
- package/dist/tempo/Methods.d.ts +8 -0
- package/dist/tempo/Methods.js +18 -2
- package/dist/tempo/client/Charge.d.ts +32 -1
- package/dist/tempo/client/Charge.js +201 -3
- package/dist/tempo/client/Methods.d.ts +16 -0
- package/dist/tempo/internal/fee-payer.d.ts +0 -25
- package/dist/tempo/internal/fee-payer.js +50 -3
- package/dist/tempo/internal/zone.d.ts +79 -0
- package/dist/tempo/internal/zone.js +133 -0
- package/dist/tempo/server/Charge.d.ts +7 -3
- package/dist/tempo/server/Charge.js +98 -4
- package/dist/tempo/server/Methods.d.ts +40 -0
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/session/server/ChannelStore.d.ts +5 -0
- package/dist/tempo/session/server/ChannelStore.js +2 -2
- package/dist/tempo/session/server/CredentialVerification.js +1 -1
- package/dist/validation/core.js +2 -0
- package/package.json +5 -1
package/dist/tempo/Methods.js
CHANGED
|
@@ -2,6 +2,7 @@ import { parseUnits } from 'viem';
|
|
|
2
2
|
import * as Constants from '../Constants.js';
|
|
3
3
|
import * as Method from '../Method.js';
|
|
4
4
|
import * as z from '../zod.js';
|
|
5
|
+
import * as Zone from './internal/zone.js';
|
|
5
6
|
export const chargeModes = ['push', 'pull'];
|
|
6
7
|
const split = z.object({
|
|
7
8
|
amount: z.amount(),
|
|
@@ -79,6 +80,12 @@ export const charge = Method.from({
|
|
|
79
80
|
z.object({ hash: z.hash(), type: z.literal('hash') }),
|
|
80
81
|
z.object({ signature: z.signature(), type: z.literal('transaction') }),
|
|
81
82
|
z.object({ signature: z.signature(), type: z.literal('proof') }),
|
|
83
|
+
z.object({
|
|
84
|
+
hash: z.hash(),
|
|
85
|
+
senderTag: z.hash(),
|
|
86
|
+
type: z.literal('zone'),
|
|
87
|
+
zoneId: z.number().check(z.refine(Zone.isId, 'Invalid Zone ID')),
|
|
88
|
+
}),
|
|
82
89
|
]),
|
|
83
90
|
},
|
|
84
91
|
request: z.pipe(z
|
|
@@ -95,6 +102,13 @@ export const charge = Method.from({
|
|
|
95
102
|
recipient: z.optional(z.string()),
|
|
96
103
|
splits: z.optional(z.array(split).check(z.minLength(1), z.maxLength(10))),
|
|
97
104
|
supportedModes: z.optional(z.array(z.enum(chargeModes)).check(z.minLength(1))),
|
|
105
|
+
// Zone IDs the payee accepts withdrawals from, or true for any Zone on the challenge's chain.
|
|
106
|
+
zones: z.optional(z.union([
|
|
107
|
+
z.literal(true),
|
|
108
|
+
z
|
|
109
|
+
.array(z.number().check(z.refine(Zone.isId, 'Invalid Zone ID')))
|
|
110
|
+
.check(z.minLength(1)),
|
|
111
|
+
])),
|
|
98
112
|
})
|
|
99
113
|
.check(z.refine(({ amount, decimals, splits }) => {
|
|
100
114
|
if (!splits)
|
|
@@ -103,14 +117,15 @@ export const charge = Method.from({
|
|
|
103
117
|
const splitTotal = splits.reduce((sum, split) => sum + parseUnits(split.amount, decimals), 0n);
|
|
104
118
|
return (splits.every((split) => parseUnits(split.amount, decimals) > 0n) &&
|
|
105
119
|
splitTotal < totalAmount);
|
|
106
|
-
}, 'Invalid splits')), z.transform(({ machineTokenEnabled, amount, chainId, decimals, feePayer, memo: _memo, splits, supportedModes, ...rest }) => ({
|
|
120
|
+
}, 'Invalid splits')), z.transform(({ machineTokenEnabled, amount, chainId, decimals, feePayer, memo: _memo, splits, supportedModes, zones, ...rest }) => ({
|
|
107
121
|
...rest,
|
|
108
122
|
amount: parseUnits(amount, decimals).toString(),
|
|
109
123
|
...(machineTokenEnabled !== undefined ||
|
|
110
124
|
chainId !== undefined ||
|
|
111
125
|
feePayer !== undefined ||
|
|
112
126
|
splits !== undefined ||
|
|
113
|
-
supportedModes !== undefined
|
|
127
|
+
supportedModes !== undefined ||
|
|
128
|
+
zones !== undefined
|
|
114
129
|
? {
|
|
115
130
|
methodDetails: {
|
|
116
131
|
...(machineTokenEnabled !== undefined && { machineTokenEnabled }),
|
|
@@ -123,6 +138,7 @@ export const charge = Method.from({
|
|
|
123
138
|
})),
|
|
124
139
|
}),
|
|
125
140
|
...(supportedModes !== undefined && { supportedModes }),
|
|
141
|
+
...(zones !== undefined && { zones }),
|
|
126
142
|
},
|
|
127
143
|
}
|
|
128
144
|
: {}),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Address } from 'viem';
|
|
1
|
+
import { type Address, type Client as viem_Client } from 'viem';
|
|
2
2
|
import * as Method from '../../Method.js';
|
|
3
3
|
import * as Account from '../../viem/Account.js';
|
|
4
4
|
import * as Client from '../../viem/Client.js';
|
|
@@ -61,6 +61,11 @@ export declare function charge(parameters?: charge.Parameters): {
|
|
|
61
61
|
}, z.core.$strip>, z.ZodMiniObject<{
|
|
62
62
|
signature: z.ZodMiniString<string>;
|
|
63
63
|
type: z.ZodMiniLiteral<"proof">;
|
|
64
|
+
}, z.core.$strip>, z.ZodMiniObject<{
|
|
65
|
+
hash: z.ZodMiniString<string>;
|
|
66
|
+
senderTag: z.ZodMiniString<string>;
|
|
67
|
+
type: z.ZodMiniLiteral<"zone">;
|
|
68
|
+
zoneId: z.ZodMiniNumber<number>;
|
|
64
69
|
}, z.core.$strip>], "type">;
|
|
65
70
|
};
|
|
66
71
|
request: z.ZodMiniPipe<z.ZodMiniObject<{
|
|
@@ -83,6 +88,7 @@ export declare function charge(parameters?: charge.Parameters): {
|
|
|
83
88
|
pull: "pull";
|
|
84
89
|
push: "push";
|
|
85
90
|
}>>>;
|
|
91
|
+
zones: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniLiteral<true>, z.ZodMiniArray<z.ZodMiniNumber<number>>]>>;
|
|
86
92
|
}, z.core.$strip>, z.ZodMiniTransform<{
|
|
87
93
|
currency: string;
|
|
88
94
|
description?: string | undefined;
|
|
@@ -99,6 +105,7 @@ export declare function charge(parameters?: charge.Parameters): {
|
|
|
99
105
|
amount: string;
|
|
100
106
|
}[] | undefined;
|
|
101
107
|
supportedModes?: ("pull" | "push")[] | undefined;
|
|
108
|
+
zones?: true | number[] | undefined;
|
|
102
109
|
} | undefined;
|
|
103
110
|
}, {
|
|
104
111
|
machineTokenEnabled?: boolean | undefined;
|
|
@@ -117,6 +124,7 @@ export declare function charge(parameters?: charge.Parameters): {
|
|
|
117
124
|
recipient: `0x${string}`;
|
|
118
125
|
}[] | undefined;
|
|
119
126
|
supportedModes?: ("pull" | "push")[] | undefined;
|
|
127
|
+
zones?: true | number[] | undefined;
|
|
120
128
|
}>>;
|
|
121
129
|
};
|
|
122
130
|
readonly createCredential: Method.CreateCredentialFn<{
|
|
@@ -133,6 +141,11 @@ export declare function charge(parameters?: charge.Parameters): {
|
|
|
133
141
|
}, z.core.$strip>, z.ZodMiniObject<{
|
|
134
142
|
signature: z.ZodMiniString<string>;
|
|
135
143
|
type: z.ZodMiniLiteral<"proof">;
|
|
144
|
+
}, z.core.$strip>, z.ZodMiniObject<{
|
|
145
|
+
hash: z.ZodMiniString<string>;
|
|
146
|
+
senderTag: z.ZodMiniString<string>;
|
|
147
|
+
type: z.ZodMiniLiteral<"zone">;
|
|
148
|
+
zoneId: z.ZodMiniNumber<number>;
|
|
136
149
|
}, z.core.$strip>], "type">;
|
|
137
150
|
};
|
|
138
151
|
request: z.ZodMiniPipe<z.ZodMiniObject<{
|
|
@@ -155,6 +168,7 @@ export declare function charge(parameters?: charge.Parameters): {
|
|
|
155
168
|
pull: "pull";
|
|
156
169
|
push: "push";
|
|
157
170
|
}>>>;
|
|
171
|
+
zones: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniLiteral<true>, z.ZodMiniArray<z.ZodMiniNumber<number>>]>>;
|
|
158
172
|
}, z.core.$strip>, z.ZodMiniTransform<{
|
|
159
173
|
currency: string;
|
|
160
174
|
description?: string | undefined;
|
|
@@ -171,6 +185,7 @@ export declare function charge(parameters?: charge.Parameters): {
|
|
|
171
185
|
amount: string;
|
|
172
186
|
}[] | undefined;
|
|
173
187
|
supportedModes?: ("pull" | "push")[] | undefined;
|
|
188
|
+
zones?: true | number[] | undefined;
|
|
174
189
|
} | undefined;
|
|
175
190
|
}, {
|
|
176
191
|
machineTokenEnabled?: boolean | undefined;
|
|
@@ -189,6 +204,7 @@ export declare function charge(parameters?: charge.Parameters): {
|
|
|
189
204
|
recipient: `0x${string}`;
|
|
190
205
|
}[] | undefined;
|
|
191
206
|
supportedModes?: ("pull" | "push")[] | undefined;
|
|
207
|
+
zones?: true | number[] | undefined;
|
|
192
208
|
}>>;
|
|
193
209
|
};
|
|
194
210
|
}, {
|
|
@@ -203,6 +219,15 @@ export declare namespace charge {
|
|
|
203
219
|
type Context = ChargeContext;
|
|
204
220
|
type ResolveAccount = AccountResolution.ResolveAccount;
|
|
205
221
|
type ResolveAccountInfo = AccountResolution.ResolveAccountInfo;
|
|
222
|
+
/** Pays from a Zone balance when a challenge accepts that Zone. */
|
|
223
|
+
type Zone = {
|
|
224
|
+
/** Client for the Zone chain, with the paying account and Zone RPC authorization. */
|
|
225
|
+
client: viem_Client;
|
|
226
|
+
/** Zone ID. Derived from `client.chain.id` when omitted. */
|
|
227
|
+
zoneId?: number | undefined;
|
|
228
|
+
/** Milliseconds to wait for the withdrawal to settle on Tempo. @default 60_000 */
|
|
229
|
+
settlementTimeoutMs?: number | undefined;
|
|
230
|
+
};
|
|
206
231
|
type Parameters = {
|
|
207
232
|
/**
|
|
208
233
|
* Automatically swap from a fallback currency (pathUsd, USDC.e) via the
|
|
@@ -277,6 +302,12 @@ export declare namespace charge {
|
|
|
277
302
|
mode?: Methods.ChargeMode | undefined;
|
|
278
303
|
/** Selects the account that signs this charge after the challenge and chain are known. */
|
|
279
304
|
resolveAccount?: ResolveAccount | undefined;
|
|
305
|
+
/**
|
|
306
|
+
* Pays from a Zone balance instead of the account's Tempo balance when the
|
|
307
|
+
* challenge accepts this Zone (`methodDetails.zones`). Other challenges,
|
|
308
|
+
* and split payments, are paid on Tempo as usual.
|
|
309
|
+
*/
|
|
310
|
+
zone?: Zone | undefined;
|
|
280
311
|
} & Account.getResolver.Parameters & Client.getResolver.Parameters;
|
|
281
312
|
}
|
|
282
313
|
//# sourceMappingURL=Charge.d.ts.map
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { TxEnvelopeTempo } from 'ox/tempo';
|
|
2
|
-
import { keccak256, parseUnits } from 'viem';
|
|
3
|
-
import { prepareTransactionRequest, sendCallsSync, sendTransactionSync, signTypedData, signTransaction, } from 'viem/actions';
|
|
4
|
-
import { Actions } from 'viem/tempo';
|
|
2
|
+
import { keccak256, parseEventLogs, parseUnits, } from 'viem';
|
|
3
|
+
import { prepareTransactionRequest, sendCallsSync, sendRawTransactionSync, sendTransactionSync, signTypedData, signTransaction, waitForTransactionReceipt, } from 'viem/actions';
|
|
4
|
+
import { Abis, Actions, Addresses } from 'viem/tempo';
|
|
5
5
|
import { tempo as tempo_chain } from 'viem/tempo/chains';
|
|
6
6
|
import * as MethodResponse from '../../client/internal/MethodResponse.js';
|
|
7
7
|
import * as Credential from '../../Credential.js';
|
|
8
|
+
import * as Errors from '../../Errors.js';
|
|
8
9
|
import * as Method from '../../Method.js';
|
|
9
10
|
import * as Account from '../../viem/Account.js';
|
|
10
11
|
import * as Client from '../../viem/Client.js';
|
|
@@ -16,6 +17,7 @@ import * as defaults from '../internal/defaults.js';
|
|
|
16
17
|
import { defaultFeeTokens, resolveFeeToken } from '../internal/fee-token.js';
|
|
17
18
|
import * as MachineTokenCharge from '../internal/machine-token-charge.js';
|
|
18
19
|
import * as Proof from '../internal/proof.js';
|
|
20
|
+
import * as Zone from '../internal/zone.js';
|
|
19
21
|
import * as Methods from '../Methods.js';
|
|
20
22
|
/**
|
|
21
23
|
* Seconds from now the client sets as the expiring-nonce `validBefore`,
|
|
@@ -63,6 +65,9 @@ export function charge(parameters = {}) {
|
|
|
63
65
|
rpcUrl: defaults.rpcUrl,
|
|
64
66
|
});
|
|
65
67
|
const getAccount = Account.getResolver({ account: parameters.account });
|
|
68
|
+
assertZone(parameters.zone);
|
|
69
|
+
// Zone withdrawals still settling, by challenge ID, so a retry resumes waiting instead of paying again.
|
|
70
|
+
const pendingZoneWithdrawals = new Map();
|
|
66
71
|
const method = Method.toClient(Methods.charge, {
|
|
67
72
|
// Skip offers that violate client policy so an acceptable offer can be chosen.
|
|
68
73
|
canHandleChallenge({ challenge }) {
|
|
@@ -91,6 +96,36 @@ export function charge(parameters = {}) {
|
|
|
91
96
|
const { request } = challenge;
|
|
92
97
|
const { amount, methodDetails } = request;
|
|
93
98
|
const supportedModes = methodDetails?.supportedModes ?? ['pull', 'push'];
|
|
99
|
+
const zoneChain = parameters.zone?.client.chain;
|
|
100
|
+
const zoneId = parameters.zone
|
|
101
|
+
? (parameters.zone.zoneId ?? Zone.idFromChainId(zoneChain?.id ?? 0))
|
|
102
|
+
: undefined;
|
|
103
|
+
// A Zone ID is only unique per parent chain, so the Zone must settle on the challenge's chain.
|
|
104
|
+
const zoneParentChainId = zoneChain?.sourceId ??
|
|
105
|
+
(zoneChain ? Zone.parentChainIdFromChainId(zoneChain.id) : undefined);
|
|
106
|
+
const explicitMode = context?.mode ?? parameters.mode;
|
|
107
|
+
// Pay from a Zone balance when the challenge accepts the configured Zone.
|
|
108
|
+
// Zone payments are push payments, so they follow the challenge's and client's modes.
|
|
109
|
+
if (BigInt(amount) > 0n &&
|
|
110
|
+
parameters.zone &&
|
|
111
|
+
zoneId !== undefined &&
|
|
112
|
+
zoneParentChainId === chainId &&
|
|
113
|
+
supportedModes.includes('push') &&
|
|
114
|
+
(explicitMode === undefined || explicitMode === 'push') &&
|
|
115
|
+
Zone.isAccepted(methodDetails?.zones, zoneId) &&
|
|
116
|
+
!methodDetails?.splits &&
|
|
117
|
+
// A withdrawal can't be undone, so start one only when it can settle before the
|
|
118
|
+
// challenge expires; an expired challenge would invite another payment.
|
|
119
|
+
(pendingZoneWithdrawals.has(zoneWithdrawalKey(challenge)) ||
|
|
120
|
+
settlesBeforeExpiry(challenge.expires, parameters.zone.settlementTimeoutMs)))
|
|
121
|
+
return createZoneCredential({
|
|
122
|
+
challenge,
|
|
123
|
+
clientId,
|
|
124
|
+
client,
|
|
125
|
+
pending: pendingZoneWithdrawals,
|
|
126
|
+
zone: parameters.zone,
|
|
127
|
+
zoneId,
|
|
128
|
+
});
|
|
94
129
|
const defaultAccount = getAccount(client, context);
|
|
95
130
|
const resolveAccount = async (operation) => (await parameters.resolveAccount?.({
|
|
96
131
|
account: defaultAccount,
|
|
@@ -326,6 +361,8 @@ function transactionHash(credential) {
|
|
|
326
361
|
const payload = parsed.data;
|
|
327
362
|
if (payload.type === 'hash')
|
|
328
363
|
return payload.hash;
|
|
364
|
+
if (payload.type === 'zone')
|
|
365
|
+
return payload.hash;
|
|
329
366
|
const serialized = payload.signature;
|
|
330
367
|
if (payload.type !== 'transaction' || !serialized.startsWith(TxEnvelopeTempo.serializedType))
|
|
331
368
|
return undefined;
|
|
@@ -337,6 +374,167 @@ function transactionHash(credential) {
|
|
|
337
374
|
return undefined;
|
|
338
375
|
return keccak256(serialized);
|
|
339
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Pays a challenge from a Zone balance: requests a withdrawal to the recipient
|
|
379
|
+
* with the challenge-bound memo, and returns a `zone` credential that names it
|
|
380
|
+
* by sender tag and the Tempo transaction hash that settled it.
|
|
381
|
+
*/
|
|
382
|
+
async function createZoneCredential(options) {
|
|
383
|
+
const { challenge, client, pending, zone, zoneId } = options;
|
|
384
|
+
const request = challenge.request;
|
|
385
|
+
const recipient = request.recipient;
|
|
386
|
+
// The whole challenge, so a challenge from another realm or request with the same ID never matches.
|
|
387
|
+
const key = zoneWithdrawalKey(challenge);
|
|
388
|
+
// A withdrawal can't be undone, so a retry after a failed wait resumes it instead of paying again.
|
|
389
|
+
// Concurrent calls for one challenge share the request, which is stored before it is sent.
|
|
390
|
+
let withdrawalRequest = pending.get(key);
|
|
391
|
+
if (!withdrawalRequest) {
|
|
392
|
+
const state = { sent: false };
|
|
393
|
+
withdrawalRequest = requestZoneWithdrawal(options, state);
|
|
394
|
+
pending.set(key, withdrawalRequest);
|
|
395
|
+
// Only a failure before the transaction was sent is safe to retry. After that, the
|
|
396
|
+
// withdrawal may have gone through, so retries of this challenge keep failing.
|
|
397
|
+
withdrawalRequest.catch(() => {
|
|
398
|
+
if (!state.sent)
|
|
399
|
+
pending.delete(key);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
const senderTag = await withdrawalRequest;
|
|
403
|
+
let hash;
|
|
404
|
+
try {
|
|
405
|
+
hash = await Zone.waitForSettlement(client, {
|
|
406
|
+
portal: Zone.portalAddress(zoneId),
|
|
407
|
+
senderTag,
|
|
408
|
+
timeoutMs: settlementTimeoutMs(challenge.expires, zone.settlementTimeoutMs),
|
|
409
|
+
to: recipient,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
catch (cause) {
|
|
413
|
+
throw new Errors.ZoneSettlementPendingError({
|
|
414
|
+
cause,
|
|
415
|
+
challengeId: challenge.id,
|
|
416
|
+
senderTag,
|
|
417
|
+
zoneId,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
pending.delete(key);
|
|
421
|
+
return Credential.serialize({
|
|
422
|
+
challenge,
|
|
423
|
+
payload: { hash, senderTag, type: 'zone', zoneId },
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Throws on Zone options that would fail only after an irreversible withdrawal:
|
|
428
|
+
* a `settlementTimeoutMs` that is not a finite, non-negative number, or a
|
|
429
|
+
* `zoneId` that names a different Zone from the one `zone.client` serves.
|
|
430
|
+
*/
|
|
431
|
+
function assertZone(zone) {
|
|
432
|
+
const timeoutMs = zone?.settlementTimeoutMs;
|
|
433
|
+
if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs < 0))
|
|
434
|
+
throw new Error('`zone.settlementTimeoutMs` must be a finite, non-negative number.');
|
|
435
|
+
const chainId = zone?.client.chain?.id;
|
|
436
|
+
if (zone?.zoneId === undefined || chainId === undefined)
|
|
437
|
+
return;
|
|
438
|
+
// Only chain IDs that follow a known Zone scheme name their Zone.
|
|
439
|
+
if (Zone.parentChainIdFromChainId(chainId) === undefined)
|
|
440
|
+
return;
|
|
441
|
+
const derived = Zone.idFromChainId(chainId);
|
|
442
|
+
if (derived !== zone.zoneId)
|
|
443
|
+
throw new Error(`zone.zoneId ${zone.zoneId} does not match Zone ${derived} of the zone.client chain ${chainId}.`);
|
|
444
|
+
}
|
|
445
|
+
/** Time the client allows for sending a Zone withdrawal before it waits for settlement. */
|
|
446
|
+
const zoneSubmissionMarginMs = 15_000;
|
|
447
|
+
/**
|
|
448
|
+
* Returns whether a Zone withdrawal can be sent and waited on for up to
|
|
449
|
+
* `settlementTimeoutMs` before `expires`.
|
|
450
|
+
*/
|
|
451
|
+
function settlesBeforeExpiry(expires, settlementTimeoutMs) {
|
|
452
|
+
if (!expires)
|
|
453
|
+
return true;
|
|
454
|
+
const timeoutMs = settlementTimeoutMs ?? Zone.defaultSettlementTimeoutMs;
|
|
455
|
+
return new Date(expires).getTime() - Date.now() > timeoutMs + zoneSubmissionMarginMs;
|
|
456
|
+
}
|
|
457
|
+
/** Returns how long to wait for settlement: `settlementTimeoutMs`, cut short at `expires`. */
|
|
458
|
+
function settlementTimeoutMs(expires, settlementTimeoutMs) {
|
|
459
|
+
const timeoutMs = settlementTimeoutMs ?? Zone.defaultSettlementTimeoutMs;
|
|
460
|
+
if (!expires)
|
|
461
|
+
return timeoutMs;
|
|
462
|
+
return Math.max(0, Math.min(timeoutMs, new Date(expires).getTime() - Date.now()));
|
|
463
|
+
}
|
|
464
|
+
/** Keys a Zone withdrawal by its whole Challenge. Unlike `Challenge.serialize`, it accepts any field value. */
|
|
465
|
+
function zoneWithdrawalKey(challenge) {
|
|
466
|
+
return JSON.stringify(challenge);
|
|
467
|
+
}
|
|
468
|
+
/** How long to look for a withdrawal's receipt on the Zone after its send response fails. */
|
|
469
|
+
const zoneReceiptRecoveryTimeoutMs = 15_000;
|
|
470
|
+
/** Requests a Zone withdrawal of the challenge amount to its recipient, bound to the challenge by its memo. */
|
|
471
|
+
async function requestZoneWithdrawal(options, state) {
|
|
472
|
+
const { challenge, clientId, zone } = options;
|
|
473
|
+
const request = challenge.request;
|
|
474
|
+
const account = zone.client.account;
|
|
475
|
+
if (!account)
|
|
476
|
+
throw new Error('`zone.client` requires an account.');
|
|
477
|
+
const amount = BigInt(request.amount);
|
|
478
|
+
const currency = request.currency;
|
|
479
|
+
const recipient = request.recipient;
|
|
480
|
+
const memo = Attribution.encode({
|
|
481
|
+
challengeId: challenge.id,
|
|
482
|
+
clientId,
|
|
483
|
+
serverId: challenge.realm,
|
|
484
|
+
});
|
|
485
|
+
// The outbox pulls the amount plus its withdrawal fee.
|
|
486
|
+
const fee = await Actions.zone.getWithdrawalFee(zone.client);
|
|
487
|
+
const [, withdrawal] = Actions.zone.requestWithdrawal.calls({
|
|
488
|
+
amount,
|
|
489
|
+
// A bounced payout refunds the payer, not the payee's Zone address.
|
|
490
|
+
fallbackRecipient: account.address,
|
|
491
|
+
memo,
|
|
492
|
+
to: recipient,
|
|
493
|
+
token: currency,
|
|
494
|
+
});
|
|
495
|
+
// Prepare and sign first: a failure there sent nothing, so the challenge may be retried.
|
|
496
|
+
const prepared = await prepareTransactionRequest(zone.client, {
|
|
497
|
+
account,
|
|
498
|
+
calls: [
|
|
499
|
+
Actions.token.approve.call({
|
|
500
|
+
amount: amount + fee,
|
|
501
|
+
spender: Addresses.zoneOutbox,
|
|
502
|
+
token: currency,
|
|
503
|
+
}),
|
|
504
|
+
withdrawal,
|
|
505
|
+
],
|
|
506
|
+
});
|
|
507
|
+
const serializedTransaction = await signTransaction(zone.client, prepared);
|
|
508
|
+
state.sent = true;
|
|
509
|
+
// The signed transaction fixes its hash, so a lost send response can still be recovered from the Zone.
|
|
510
|
+
const hash = keccak256(serializedTransaction);
|
|
511
|
+
const receipt = await sendRawTransactionSync(zone.client, { serializedTransaction }).catch((cause) => waitForTransactionReceipt(zone.client, {
|
|
512
|
+
hash,
|
|
513
|
+
timeout: zoneReceiptRecoveryTimeoutMs,
|
|
514
|
+
}).catch(() => {
|
|
515
|
+
const error = new Error(`Zone withdrawal outcome is unknown: transaction ${hash} may have been sent. Retrying this challenge does not withdraw again.`);
|
|
516
|
+
// Defined manually (non-enumerable, like native `cause`) so consumers on pre-ES2022 libs still typecheck.
|
|
517
|
+
Object.defineProperty(error, 'cause', { configurable: true, value: cause, writable: true });
|
|
518
|
+
throw error;
|
|
519
|
+
}));
|
|
520
|
+
// A reverted withdrawal moved nothing, so the challenge may be retried.
|
|
521
|
+
if (receipt.status === 'reverted') {
|
|
522
|
+
state.sent = false;
|
|
523
|
+
throw new Error(`Zone withdrawal transaction ${receipt.transactionHash} reverted.`);
|
|
524
|
+
}
|
|
525
|
+
const [requested] = parseEventLogs({
|
|
526
|
+
abi: Abis.zoneOutbox,
|
|
527
|
+
eventName: 'WithdrawalRequested',
|
|
528
|
+
logs: receipt.logs,
|
|
529
|
+
});
|
|
530
|
+
if (!requested)
|
|
531
|
+
throw new Error('Zone withdrawal request did not emit `WithdrawalRequested`.');
|
|
532
|
+
return Zone.senderTag({
|
|
533
|
+
fallbackNonce: requested.args.fallbackNonce,
|
|
534
|
+
sender: requested.args.sender,
|
|
535
|
+
transactionHash: receipt.transactionHash,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
340
538
|
/** Parses `maxAmount` to base units, rejecting values that cannot be represented exactly. */
|
|
341
539
|
function parseMaxAmount(maxAmount, decimals) {
|
|
342
540
|
if (maxAmount === undefined)
|
|
@@ -43,6 +43,11 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
|
|
|
43
43
|
}, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniObject<{
|
|
44
44
|
signature: import("zod/mini").ZodMiniString<string>;
|
|
45
45
|
type: import("zod/mini").ZodMiniLiteral<"proof">;
|
|
46
|
+
}, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniObject<{
|
|
47
|
+
hash: import("zod/mini").ZodMiniString<string>;
|
|
48
|
+
senderTag: import("zod/mini").ZodMiniString<string>;
|
|
49
|
+
type: import("zod/mini").ZodMiniLiteral<"zone">;
|
|
50
|
+
zoneId: import("zod/mini").ZodMiniNumber<number>;
|
|
46
51
|
}, import("zod/v4/core").$strip>], "type">;
|
|
47
52
|
};
|
|
48
53
|
request: import("zod/mini").ZodMiniPipe<import("zod/mini").ZodMiniObject<{
|
|
@@ -65,6 +70,7 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
|
|
|
65
70
|
pull: "pull";
|
|
66
71
|
push: "push";
|
|
67
72
|
}>>>;
|
|
73
|
+
zones: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniUnion<readonly [import("zod/mini").ZodMiniLiteral<true>, import("zod/mini").ZodMiniArray<import("zod/mini").ZodMiniNumber<number>>]>>;
|
|
68
74
|
}, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniTransform<{
|
|
69
75
|
currency: string;
|
|
70
76
|
description?: string | undefined;
|
|
@@ -81,6 +87,7 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
|
|
|
81
87
|
amount: string;
|
|
82
88
|
}[] | undefined;
|
|
83
89
|
supportedModes?: ("pull" | "push")[] | undefined;
|
|
90
|
+
zones?: true | number[] | undefined;
|
|
84
91
|
} | undefined;
|
|
85
92
|
}, {
|
|
86
93
|
machineTokenEnabled?: boolean | undefined;
|
|
@@ -99,6 +106,7 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
|
|
|
99
106
|
recipient: `0x${string}`;
|
|
100
107
|
}[] | undefined;
|
|
101
108
|
supportedModes?: ("pull" | "push")[] | undefined;
|
|
109
|
+
zones?: true | number[] | undefined;
|
|
102
110
|
}>>;
|
|
103
111
|
};
|
|
104
112
|
readonly createCredential: import("../../Method.js").CreateCredentialFn<{
|
|
@@ -115,6 +123,11 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
|
|
|
115
123
|
}, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniObject<{
|
|
116
124
|
signature: import("zod/mini").ZodMiniString<string>;
|
|
117
125
|
type: import("zod/mini").ZodMiniLiteral<"proof">;
|
|
126
|
+
}, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniObject<{
|
|
127
|
+
hash: import("zod/mini").ZodMiniString<string>;
|
|
128
|
+
senderTag: import("zod/mini").ZodMiniString<string>;
|
|
129
|
+
type: import("zod/mini").ZodMiniLiteral<"zone">;
|
|
130
|
+
zoneId: import("zod/mini").ZodMiniNumber<number>;
|
|
118
131
|
}, import("zod/v4/core").$strip>], "type">;
|
|
119
132
|
};
|
|
120
133
|
request: import("zod/mini").ZodMiniPipe<import("zod/mini").ZodMiniObject<{
|
|
@@ -137,6 +150,7 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
|
|
|
137
150
|
pull: "pull";
|
|
138
151
|
push: "push";
|
|
139
152
|
}>>>;
|
|
153
|
+
zones: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniUnion<readonly [import("zod/mini").ZodMiniLiteral<true>, import("zod/mini").ZodMiniArray<import("zod/mini").ZodMiniNumber<number>>]>>;
|
|
140
154
|
}, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniTransform<{
|
|
141
155
|
currency: string;
|
|
142
156
|
description?: string | undefined;
|
|
@@ -153,6 +167,7 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
|
|
|
153
167
|
amount: string;
|
|
154
168
|
}[] | undefined;
|
|
155
169
|
supportedModes?: ("pull" | "push")[] | undefined;
|
|
170
|
+
zones?: true | number[] | undefined;
|
|
156
171
|
} | undefined;
|
|
157
172
|
}, {
|
|
158
173
|
machineTokenEnabled?: boolean | undefined;
|
|
@@ -171,6 +186,7 @@ export declare function tempo(parameters?: tempo.Parameters): readonly [{
|
|
|
171
186
|
recipient: `0x${string}`;
|
|
172
187
|
}[] | undefined;
|
|
173
188
|
supportedModes?: ("pull" | "push")[] | undefined;
|
|
189
|
+
zones?: true | number[] | undefined;
|
|
174
190
|
}>>;
|
|
175
191
|
};
|
|
176
192
|
}, {
|
|
@@ -124,31 +124,6 @@ export type PreflightSponsorship = {
|
|
|
124
124
|
* Returns `undefined` when the envelope's fee payer cannot sign locally.
|
|
125
125
|
*/
|
|
126
126
|
export declare function signFeePayerSimulation(transaction: SponsoredTransaction): Promise<Signature.Signature | undefined>;
|
|
127
|
-
/**
|
|
128
|
-
* Runs the sender and final-envelope simulations around sponsorship.
|
|
129
|
-
*
|
|
130
|
-
* First, it simulates the sender's calls at the transaction's exact gas limit
|
|
131
|
-
* with zero fee fields (see {@link simulationTransaction}). This is the
|
|
132
|
-
* gas-bounded check at the broadcast limit, and needs no fee-token balance.
|
|
133
|
-
* Next, `complete` resolves the sponsor and produces the co-signed transaction.
|
|
134
|
-
* Finally, it simulates that transaction with its concrete fee payer, fee token
|
|
135
|
-
* and fee payer signature, so the node bills the sponsor rather than the sender
|
|
136
|
-
* and checks the sponsor's fee balance.
|
|
137
|
-
*
|
|
138
|
-
* A sponsored `eth_call` charges more gas than the mined transaction uses. A
|
|
139
|
-
* local fee payer therefore signs a simulation-only copy whose gas is raised by
|
|
140
|
-
* up to {@link sponsoredSimulationGasBuffer}, clamped so the copy stays within
|
|
141
|
-
* the same fee payer `policy` (`maxGas`, `maxTotalFee`) as the real
|
|
142
|
-
* transaction. The copy is never returned or broadcast. When the final
|
|
143
|
-
* simulation runs without the full buffer (hosted signatures cover the client's
|
|
144
|
-
* gas and cannot be padded, or the padding was clamped), a failure that is only
|
|
145
|
-
* out of gas is tolerated, since the sender simulation already bounded gas at
|
|
146
|
-
* the broadcast limit. Every other failure, such as an insufficient fee balance
|
|
147
|
-
* or a revert, is rethrown.
|
|
148
|
-
*
|
|
149
|
-
* `complete` runs only after sender-context execution succeeds, so a reverting
|
|
150
|
-
* transaction never reaches a local signer or hosted fee-payer.
|
|
151
|
-
*/
|
|
152
127
|
export declare function preflightSponsorship<sponsorship extends PreflightSponsorship>(parameters: {
|
|
153
128
|
complete: () => Promise<sponsorship>;
|
|
154
129
|
/** The fee payer policy the completed transaction was checked against. */
|
|
@@ -2,6 +2,7 @@ import { Address, Secp256k1, Signature } from 'ox';
|
|
|
2
2
|
import { TempoAddress, TxEnvelopeTempo } from 'ox/tempo';
|
|
3
3
|
import { decodeFunctionData, encodeFunctionData, maxUint256, toFunctionSelector, toHex } from 'viem';
|
|
4
4
|
import { Abis, Addresses, Transaction } from 'viem/tempo';
|
|
5
|
+
import { PaymentSimulationError } from '../../Errors.js';
|
|
5
6
|
import { escrowAbi as legacyEscrowAbi } from '../legacy/session/escrow.abi.js';
|
|
6
7
|
import { escrowAbi } from '../session/precompile/escrow.abi.js';
|
|
7
8
|
import * as TempoAddress_internal from './address.js';
|
|
@@ -243,6 +244,10 @@ export function sponsoredSimulationTransaction(transaction, options) {
|
|
|
243
244
|
feePayer,
|
|
244
245
|
feePayerSignature: feePayerSignature ? Signature.toRpc(feePayerSignature) : undefined,
|
|
245
246
|
...(feeToken !== undefined ? { feeToken } : {}),
|
|
247
|
+
// A deserialized envelope omits a zero priority fee, which the signed envelope encodes as 0.
|
|
248
|
+
// The node rebuilds the fee payer payload from the request, so an absent fee fails signature
|
|
249
|
+
// recovery (or takes the node's own value) for a hosted sponsor's signature.
|
|
250
|
+
maxPriorityFeePerGas: transaction.maxPriorityFeePerGas ?? 0n,
|
|
246
251
|
signature: undefined,
|
|
247
252
|
};
|
|
248
253
|
}
|
|
@@ -287,14 +292,48 @@ export async function signFeePayerSimulation(transaction) {
|
|
|
287
292
|
* gas and cannot be padded, or the padding was clamped), a failure that is only
|
|
288
293
|
* out of gas is tolerated, since the sender simulation already bounded gas at
|
|
289
294
|
* the broadcast limit. Every other failure, such as an insufficient fee balance
|
|
290
|
-
* or a revert, is
|
|
295
|
+
* or a revert, is thrown as a `PaymentSimulationError` that names the failing
|
|
296
|
+
* phase, sender, fee payer and payment token, with the node's error as `cause`.
|
|
291
297
|
*
|
|
292
298
|
* `complete` runs only after sender-context execution succeeds, so a reverting
|
|
293
299
|
* transaction never reaches a local signer or hosted fee-payer.
|
|
294
300
|
*/
|
|
301
|
+
/** viem's errors for an RPC request that never got a node's answer, matched by name so wrapped copies match. */
|
|
302
|
+
const transportErrorNames = new Set([
|
|
303
|
+
'HttpRequestError',
|
|
304
|
+
'SocketClosedError',
|
|
305
|
+
'TimeoutError',
|
|
306
|
+
'WebSocketRequestError',
|
|
307
|
+
]);
|
|
308
|
+
/** Whether an error or its cause chain is a transport failure, rather than the node rejecting the call. */
|
|
309
|
+
function isTransportError(error) {
|
|
310
|
+
for (let current = error, depth = 0; current && depth < 10; depth++) {
|
|
311
|
+
if (current instanceof Error && transportErrorNames.has(current.name))
|
|
312
|
+
return true;
|
|
313
|
+
current = current.cause;
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
295
317
|
export async function preflightSponsorship(parameters) {
|
|
296
318
|
const { complete, policy, simulate, transaction } = parameters;
|
|
297
|
-
|
|
319
|
+
const paymentCall = transaction.calls.length === 1 ? transaction.calls[0] : undefined;
|
|
320
|
+
const paymentToken = paymentCall?.data?.startsWith(Selectors.transfer) ||
|
|
321
|
+
paymentCall?.data?.startsWith(Selectors.transferWithMemo)
|
|
322
|
+
? paymentCall.to
|
|
323
|
+
: undefined;
|
|
324
|
+
try {
|
|
325
|
+
await simulate(simulationTransaction(transaction, { feePayer: true }));
|
|
326
|
+
}
|
|
327
|
+
catch (cause) {
|
|
328
|
+
if (isTransportError(cause))
|
|
329
|
+
throw cause;
|
|
330
|
+
throw new PaymentSimulationError({
|
|
331
|
+
cause,
|
|
332
|
+
paymentToken,
|
|
333
|
+
phase: 'sender',
|
|
334
|
+
sender: transaction.from,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
298
337
|
const completed = await complete();
|
|
299
338
|
const { gas } = completed.transaction;
|
|
300
339
|
const simulated = await (async () => {
|
|
@@ -325,8 +364,16 @@ export async function preflightSponsorship(parameters) {
|
|
|
325
364
|
const unpadded = gas !== undefined &&
|
|
326
365
|
simulatedGas !== undefined &&
|
|
327
366
|
simulatedGas < gas + sponsoredSimulationGasBuffer;
|
|
328
|
-
if (
|
|
367
|
+
if (isTransportError(error))
|
|
329
368
|
throw error;
|
|
369
|
+
if (!unpadded || !isOutOfGasError(error))
|
|
370
|
+
throw new PaymentSimulationError({
|
|
371
|
+
cause: error,
|
|
372
|
+
feePayer: TempoAddress.resolve(completed.feePayer),
|
|
373
|
+
paymentToken,
|
|
374
|
+
phase: 'sponsored',
|
|
375
|
+
sender: transaction.from,
|
|
376
|
+
});
|
|
330
377
|
}
|
|
331
378
|
return completed;
|
|
332
379
|
}
|