@solana/mpp 0.1.1 → 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/dist/Methods.d.ts +1 -213
- package/dist/Methods.d.ts.map +1 -1
- package/dist/Methods.js +1 -158
- package/dist/Methods.js.map +1 -1
- package/dist/client/Methods.d.ts +0 -2
- package/dist/client/Methods.d.ts.map +1 -1
- package/dist/client/Methods.js +0 -2
- package/dist/client/Methods.js.map +1 -1
- package/dist/client/index.d.ts +0 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +0 -1
- package/dist/client/index.js.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/server/Charge.d.ts +2 -2
- package/dist/server/Charge.d.ts.map +1 -1
- package/dist/server/Charge.js +4 -0
- package/dist/server/Charge.js.map +1 -1
- package/dist/server/Methods.d.ts +0 -2
- package/dist/server/Methods.d.ts.map +1 -1
- package/dist/server/Methods.js +0 -2
- package/dist/server/Methods.js.map +1 -1
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +0 -1
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -9
- package/src/Methods.ts +1 -171
- package/src/client/Methods.ts +0 -3
- package/src/client/index.ts +0 -1
- package/src/index.ts +0 -29
- package/src/server/Charge.ts +7 -2
- package/src/server/Methods.ts +0 -3
- package/src/server/index.ts +0 -1
- package/dist/client/Session.d.ts +0 -195
- package/dist/client/Session.d.ts.map +0 -1
- package/dist/client/Session.js +0 -411
- package/dist/client/Session.js.map +0 -1
- package/dist/server/Session.d.ts +0 -171
- package/dist/server/Session.d.ts.map +0 -1
- package/dist/server/Session.js +0 -430
- package/dist/server/Session.js.map +0 -1
- package/dist/session/ChannelStore.d.ts +0 -12
- package/dist/session/ChannelStore.d.ts.map +0 -1
- package/dist/session/ChannelStore.js +0 -88
- package/dist/session/ChannelStore.js.map +0 -1
- package/dist/session/Types.d.ts +0 -179
- package/dist/session/Types.d.ts.map +0 -1
- package/dist/session/Types.js +0 -2
- package/dist/session/Types.js.map +0 -1
- package/dist/session/Voucher.d.ts +0 -7
- package/dist/session/Voucher.d.ts.map +0 -1
- package/dist/session/Voucher.js +0 -118
- package/dist/session/Voucher.js.map +0 -1
- package/dist/session/authorizers/BudgetAuthorizer.d.ts +0 -90
- package/dist/session/authorizers/BudgetAuthorizer.d.ts.map +0 -1
- package/dist/session/authorizers/BudgetAuthorizer.js +0 -398
- package/dist/session/authorizers/BudgetAuthorizer.js.map +0 -1
- package/dist/session/authorizers/SwigSessionAuthorizer.d.ts +0 -104
- package/dist/session/authorizers/SwigSessionAuthorizer.d.ts.map +0 -1
- package/dist/session/authorizers/SwigSessionAuthorizer.js +0 -522
- package/dist/session/authorizers/SwigSessionAuthorizer.js.map +0 -1
- package/dist/session/authorizers/UnboundedAuthorizer.d.ts +0 -36
- package/dist/session/authorizers/UnboundedAuthorizer.d.ts.map +0 -1
- package/dist/session/authorizers/UnboundedAuthorizer.js +0 -204
- package/dist/session/authorizers/UnboundedAuthorizer.js.map +0 -1
- package/dist/session/authorizers/index.d.ts +0 -5
- package/dist/session/authorizers/index.d.ts.map +0 -1
- package/dist/session/authorizers/index.js +0 -5
- package/dist/session/authorizers/index.js.map +0 -1
- package/dist/session/authorizers/makeSessionAuthorizer.d.ts +0 -19
- package/dist/session/authorizers/makeSessionAuthorizer.d.ts.map +0 -1
- package/dist/session/authorizers/makeSessionAuthorizer.js +0 -72
- package/dist/session/authorizers/makeSessionAuthorizer.js.map +0 -1
- package/dist/session/index.d.ts +0 -5
- package/dist/session/index.d.ts.map +0 -1
- package/dist/session/index.js +0 -5
- package/dist/session/index.js.map +0 -1
- package/src/client/Session.ts +0 -630
- package/src/server/Session.ts +0 -687
- package/src/session/ChannelStore.ts +0 -128
- package/src/session/Types.ts +0 -189
- package/src/session/Voucher.ts +0 -158
- package/src/session/authorizers/BudgetAuthorizer.ts +0 -574
- package/src/session/authorizers/SwigSessionAuthorizer.ts +0 -767
- package/src/session/authorizers/UnboundedAuthorizer.ts +0 -284
- package/src/session/authorizers/index.ts +0 -4
- package/src/session/authorizers/makeSessionAuthorizer.ts +0 -104
- package/src/session/index.ts +0 -4
|
@@ -1,574 +0,0 @@
|
|
|
1
|
-
import { createSolanaRpc, type MessagePartialSigner } from '@solana/kit';
|
|
2
|
-
|
|
3
|
-
import { DEFAULT_RPC_URLS } from '../../constants.js';
|
|
4
|
-
import {
|
|
5
|
-
type AuthorizeCloseInput,
|
|
6
|
-
type AuthorizedClose,
|
|
7
|
-
type AuthorizedOpen,
|
|
8
|
-
type AuthorizedTopup,
|
|
9
|
-
type AuthorizedUpdate,
|
|
10
|
-
type AuthorizeOpenInput,
|
|
11
|
-
type AuthorizerCapabilities,
|
|
12
|
-
type AuthorizeTopupInput,
|
|
13
|
-
type AuthorizeUpdateInput,
|
|
14
|
-
type SessionAuthorizer,
|
|
15
|
-
} from '../Types.js';
|
|
16
|
-
import { signVoucher } from '../Voucher.js';
|
|
17
|
-
|
|
18
|
-
type SwigRoleActions = {
|
|
19
|
-
canUseProgram?: (programId: string) => boolean;
|
|
20
|
-
solSpendLimit?: () => bigint | null;
|
|
21
|
-
tokenSpendLimit?: (mint: string) => bigint | null;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
type SwigAddressLike = {
|
|
25
|
-
toBase58?: () => string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type SwigRoleAuthority = {
|
|
29
|
-
ed25519PublicKey?: SwigAddressLike;
|
|
30
|
-
publicKey?: SwigAddressLike;
|
|
31
|
-
sessionKey?: SwigAddressLike;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type SwigRole = {
|
|
35
|
-
actions?: SwigRoleActions;
|
|
36
|
-
authority?: SwigRoleAuthority;
|
|
37
|
-
id: number;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
type SwigAccount = {
|
|
41
|
-
findRoleById?: (id: number) => SwigRole | null;
|
|
42
|
-
findRoleBySessionKey?: (sessionKey: string) => SwigRole | null;
|
|
43
|
-
findRolesByEd25519SignerPk?: (signerPk: string) => SwigRole[];
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export type BudgetSwigModule = {
|
|
47
|
-
fetchSwig: (rpc: unknown, swigAddress: string) => Promise<SwigAccount>;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
type SwigOnChainRoleConfig = {
|
|
51
|
-
rpcUrl?: string;
|
|
52
|
-
swigAddress: string;
|
|
53
|
-
swigRoleId: number;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
type ChannelProgress = {
|
|
57
|
-
deposited: bigint;
|
|
58
|
-
lastCumulative: bigint;
|
|
59
|
-
lastSequence: number;
|
|
60
|
-
maxCumulativeAmount: bigint;
|
|
61
|
-
maxDepositAmount?: bigint;
|
|
62
|
-
swigRoleId?: number;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export interface BudgetAuthorizerParameters {
|
|
66
|
-
allowedPrograms?: string[];
|
|
67
|
-
buildCloseTx?: (input: AuthorizeCloseInput) => Promise<string> | string;
|
|
68
|
-
buildOpenTx?: (input: AuthorizeOpenInput) => Promise<string> | string;
|
|
69
|
-
buildTopupTx?: (input: AuthorizeTopupInput) => Promise<string> | string;
|
|
70
|
-
maxCumulativeAmount: string;
|
|
71
|
-
maxDepositAmount?: string;
|
|
72
|
-
requireApprovalOnTopup?: boolean;
|
|
73
|
-
signer: MessagePartialSigner;
|
|
74
|
-
swig: SwigOnChainRoleConfig;
|
|
75
|
-
swigModule?: BudgetSwigModule;
|
|
76
|
-
validUntil?: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Session authorizer for `regular_budget` mode.
|
|
81
|
-
*
|
|
82
|
-
* Budget limits are fail-closed against a concrete on-chain Swig role:
|
|
83
|
-
* - `authorizeOpen` reads role constraints from chain and clamps local limits.
|
|
84
|
-
* - `authorizeUpdate`/`authorizeTopup`/`authorizeClose` require open-time state.
|
|
85
|
-
* - Program access and spend caps are validated from Swig role actions.
|
|
86
|
-
*/
|
|
87
|
-
export class BudgetAuthorizer implements SessionAuthorizer {
|
|
88
|
-
private readonly signer: MessagePartialSigner;
|
|
89
|
-
private readonly maxCumulativeAmount: bigint;
|
|
90
|
-
private readonly maxDepositAmount?: bigint;
|
|
91
|
-
private readonly validUntil?: string;
|
|
92
|
-
private readonly validUntilUnixMs?: number;
|
|
93
|
-
private readonly allowedPrograms?: Set<string>;
|
|
94
|
-
private readonly swig: SwigOnChainRoleConfig;
|
|
95
|
-
private readonly buildOpenTx?: (input: AuthorizeOpenInput) => Promise<string> | string;
|
|
96
|
-
private readonly buildTopupTx?: (input: AuthorizeTopupInput) => Promise<string> | string;
|
|
97
|
-
private readonly buildCloseTx?: (input: AuthorizeCloseInput) => Promise<string> | string;
|
|
98
|
-
private readonly channels = new Map<string, ChannelProgress>();
|
|
99
|
-
private readonly capabilities: AuthorizerCapabilities;
|
|
100
|
-
private swigLoaded = false;
|
|
101
|
-
private swigModule: BudgetSwigModule | null = null;
|
|
102
|
-
|
|
103
|
-
constructor(parameters: BudgetAuthorizerParameters) {
|
|
104
|
-
if (!parameters.swig) {
|
|
105
|
-
throw new Error('BudgetAuthorizer requires `swig` configuration with on-chain role details');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (!Number.isInteger(parameters.swig.swigRoleId) || parameters.swig.swigRoleId < 0) {
|
|
109
|
-
throw new Error('swig.swigRoleId must be a non-negative integer');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (parameters.swig.swigAddress.trim().length === 0) {
|
|
113
|
-
throw new Error('swig.swigAddress must be a non-empty string');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
this.signer = parameters.signer;
|
|
117
|
-
this.maxCumulativeAmount = parseNonNegativeAmount(parameters.maxCumulativeAmount, 'maxCumulativeAmount');
|
|
118
|
-
this.maxDepositAmount =
|
|
119
|
-
parameters.maxDepositAmount !== undefined
|
|
120
|
-
? parseNonNegativeAmount(parameters.maxDepositAmount, 'maxDepositAmount')
|
|
121
|
-
: undefined;
|
|
122
|
-
this.validUntil = parameters.validUntil;
|
|
123
|
-
this.validUntilUnixMs =
|
|
124
|
-
parameters.validUntil !== undefined ? parseIsoTimestamp(parameters.validUntil, 'validUntil') : undefined;
|
|
125
|
-
this.allowedPrograms = parameters.allowedPrograms ? new Set(parameters.allowedPrograms) : undefined;
|
|
126
|
-
this.swig = {
|
|
127
|
-
swigAddress: parameters.swig.swigAddress,
|
|
128
|
-
swigRoleId: parameters.swig.swigRoleId,
|
|
129
|
-
...(parameters.swig.rpcUrl ? { rpcUrl: parameters.swig.rpcUrl } : {}),
|
|
130
|
-
};
|
|
131
|
-
if (parameters.swigModule) {
|
|
132
|
-
this.swigModule = parameters.swigModule;
|
|
133
|
-
this.swigLoaded = true;
|
|
134
|
-
}
|
|
135
|
-
this.buildOpenTx = parameters.buildOpenTx;
|
|
136
|
-
this.buildTopupTx = parameters.buildTopupTx;
|
|
137
|
-
this.buildCloseTx = parameters.buildCloseTx;
|
|
138
|
-
|
|
139
|
-
this.capabilities = {
|
|
140
|
-
mode: 'regular_budget',
|
|
141
|
-
...(this.validUntil ? { expiresAt: this.validUntil } : {}),
|
|
142
|
-
maxCumulativeAmount: this.maxCumulativeAmount.toString(),
|
|
143
|
-
...(parameters.maxDepositAmount ? { maxDepositAmount: parameters.maxDepositAmount } : {}),
|
|
144
|
-
...(parameters.allowedPrograms ? { allowedPrograms: [...parameters.allowedPrograms] } : {}),
|
|
145
|
-
allowedActions: ['open', 'update', 'topup', 'close'],
|
|
146
|
-
requiresInteractiveApproval: {
|
|
147
|
-
close: false,
|
|
148
|
-
open: false,
|
|
149
|
-
topup: parameters.requireApprovalOnTopup ?? false,
|
|
150
|
-
update: false,
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
getMode() {
|
|
156
|
-
return 'regular_budget' as const;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
getCapabilities(): AuthorizerCapabilities {
|
|
160
|
-
return this.capabilities;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async authorizeOpen(input: AuthorizeOpenInput): Promise<AuthorizedOpen> {
|
|
164
|
-
this.assertNotExpired();
|
|
165
|
-
this.assertProgramAllowed(input.channelProgram);
|
|
166
|
-
|
|
167
|
-
// Pin this channel's effective limits from on-chain role metadata.
|
|
168
|
-
const onChainConstraints = await this.resolveOnChainConstraints(input);
|
|
169
|
-
|
|
170
|
-
const deposit = parseNonNegativeAmount(input.depositAmount, 'depositAmount');
|
|
171
|
-
const maxDepositAmount = onChainConstraints.maxDepositAmount ?? this.maxDepositAmount;
|
|
172
|
-
if (maxDepositAmount !== undefined && deposit > maxDepositAmount) {
|
|
173
|
-
throw new Error(`Open deposit exceeds maxDepositAmount (${maxDepositAmount.toString()})`);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const openTx = await this.resolveOpenTx(input);
|
|
177
|
-
|
|
178
|
-
const voucher = await signVoucher(this.signer, {
|
|
179
|
-
channelId: input.channelId,
|
|
180
|
-
cumulativeAmount: '0',
|
|
181
|
-
meter: input.pricing?.meter ?? 'session',
|
|
182
|
-
payer: this.signer.address,
|
|
183
|
-
recipient: input.recipient,
|
|
184
|
-
sequence: 0,
|
|
185
|
-
units: '0',
|
|
186
|
-
...(this.validUntil ? { expiresAt: this.validUntil } : {}),
|
|
187
|
-
chainId: normalizeChainId(input.network),
|
|
188
|
-
channelProgram: input.channelProgram,
|
|
189
|
-
serverNonce: input.serverNonce,
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
this.channels.set(input.channelId, {
|
|
193
|
-
deposited: deposit,
|
|
194
|
-
lastCumulative: 0n,
|
|
195
|
-
lastSequence: 0,
|
|
196
|
-
maxCumulativeAmount: onChainConstraints.maxCumulativeAmount ?? this.maxCumulativeAmount,
|
|
197
|
-
...(maxDepositAmount !== undefined ? { maxDepositAmount } : {}),
|
|
198
|
-
swigRoleId: onChainConstraints.swigRoleId,
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
capabilities: this.getCapabilities(),
|
|
203
|
-
openTx,
|
|
204
|
-
voucher,
|
|
205
|
-
...(this.validUntil ? { expiresAt: this.validUntil } : {}),
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async authorizeUpdate(input: AuthorizeUpdateInput): Promise<AuthorizedUpdate> {
|
|
210
|
-
this.assertNotExpired();
|
|
211
|
-
this.assertProgramAllowed(input.channelProgram);
|
|
212
|
-
|
|
213
|
-
const cumulativeAmount = parseNonNegativeAmount(input.cumulativeAmount, 'cumulativeAmount');
|
|
214
|
-
|
|
215
|
-
const progress = this.channels.get(input.channelId);
|
|
216
|
-
if (!progress) {
|
|
217
|
-
throw new Error(`Unknown channel ${input.channelId}. Call authorizeOpen before authorizeUpdate.`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const maxCumulativeAmount = progress.maxCumulativeAmount;
|
|
221
|
-
|
|
222
|
-
if (cumulativeAmount > maxCumulativeAmount) {
|
|
223
|
-
throw new Error(`Cumulative amount exceeds maxCumulativeAmount (${maxCumulativeAmount.toString()})`);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
this.assertMonotonic(input.channelId, input.sequence, cumulativeAmount, progress);
|
|
227
|
-
|
|
228
|
-
const voucher = await signVoucher(this.signer, {
|
|
229
|
-
channelId: input.channelId,
|
|
230
|
-
cumulativeAmount: cumulativeAmount.toString(),
|
|
231
|
-
meter: input.meter,
|
|
232
|
-
payer: this.signer.address,
|
|
233
|
-
recipient: input.recipient,
|
|
234
|
-
sequence: input.sequence,
|
|
235
|
-
units: input.units,
|
|
236
|
-
...(this.validUntil ? { expiresAt: this.validUntil } : {}),
|
|
237
|
-
chainId: normalizeChainId(input.network),
|
|
238
|
-
channelProgram: input.channelProgram,
|
|
239
|
-
serverNonce: input.serverNonce,
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
this.channels.set(input.channelId, {
|
|
243
|
-
deposited: progress.deposited,
|
|
244
|
-
lastCumulative: cumulativeAmount,
|
|
245
|
-
lastSequence: input.sequence,
|
|
246
|
-
maxCumulativeAmount,
|
|
247
|
-
...(progress.maxDepositAmount !== undefined ? { maxDepositAmount: progress.maxDepositAmount } : {}),
|
|
248
|
-
...(progress.swigRoleId !== undefined ? { swigRoleId: progress.swigRoleId } : {}),
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
return { voucher };
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async authorizeTopup(input: AuthorizeTopupInput): Promise<AuthorizedTopup> {
|
|
255
|
-
this.assertNotExpired();
|
|
256
|
-
this.assertProgramAllowed(input.channelProgram);
|
|
257
|
-
|
|
258
|
-
const additionalAmount = parseNonNegativeAmount(input.additionalAmount, 'additionalAmount');
|
|
259
|
-
const progress = this.channels.get(input.channelId);
|
|
260
|
-
if (!progress) {
|
|
261
|
-
throw new Error(`Unknown channel ${input.channelId}. Call authorizeOpen before authorizeTopup.`);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const nextDeposited = progress.deposited + additionalAmount;
|
|
265
|
-
const maxDepositAmount = progress.maxDepositAmount ?? this.maxDepositAmount;
|
|
266
|
-
|
|
267
|
-
if (maxDepositAmount !== undefined && nextDeposited > maxDepositAmount) {
|
|
268
|
-
throw new Error(`Topup exceeds maxDepositAmount (${maxDepositAmount.toString()})`);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const topupTx = await this.resolveTopupTx(input);
|
|
272
|
-
|
|
273
|
-
this.channels.set(input.channelId, {
|
|
274
|
-
deposited: nextDeposited,
|
|
275
|
-
lastCumulative: progress.lastCumulative,
|
|
276
|
-
lastSequence: progress.lastSequence,
|
|
277
|
-
maxCumulativeAmount: progress.maxCumulativeAmount,
|
|
278
|
-
...(maxDepositAmount !== undefined ? { maxDepositAmount } : {}),
|
|
279
|
-
...(progress.swigRoleId !== undefined ? { swigRoleId: progress.swigRoleId } : {}),
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
return { topupTx };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async authorizeClose(input: AuthorizeCloseInput): Promise<AuthorizedClose> {
|
|
286
|
-
this.assertNotExpired();
|
|
287
|
-
this.assertProgramAllowed(input.channelProgram);
|
|
288
|
-
|
|
289
|
-
const finalCumulativeAmount = parseNonNegativeAmount(input.finalCumulativeAmount, 'finalCumulativeAmount');
|
|
290
|
-
|
|
291
|
-
const progress = this.channels.get(input.channelId);
|
|
292
|
-
if (!progress) {
|
|
293
|
-
throw new Error(`Unknown channel ${input.channelId}. Call authorizeOpen before authorizeClose.`);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const maxCumulativeAmount = progress.maxCumulativeAmount;
|
|
297
|
-
|
|
298
|
-
if (finalCumulativeAmount > maxCumulativeAmount) {
|
|
299
|
-
throw new Error(`Final cumulative amount exceeds maxCumulativeAmount (${maxCumulativeAmount.toString()})`);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
this.assertMonotonic(input.channelId, input.sequence, finalCumulativeAmount, progress);
|
|
303
|
-
|
|
304
|
-
const voucher = await signVoucher(this.signer, {
|
|
305
|
-
channelId: input.channelId,
|
|
306
|
-
cumulativeAmount: finalCumulativeAmount.toString(),
|
|
307
|
-
meter: 'close',
|
|
308
|
-
payer: this.signer.address,
|
|
309
|
-
recipient: input.recipient,
|
|
310
|
-
sequence: input.sequence,
|
|
311
|
-
units: '0',
|
|
312
|
-
...(this.validUntil ? { expiresAt: this.validUntil } : {}),
|
|
313
|
-
chainId: normalizeChainId(input.network),
|
|
314
|
-
channelProgram: input.channelProgram,
|
|
315
|
-
serverNonce: input.serverNonce,
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
const closeTx = await this.resolveCloseTx(input);
|
|
319
|
-
|
|
320
|
-
this.channels.set(input.channelId, {
|
|
321
|
-
deposited: progress.deposited,
|
|
322
|
-
lastCumulative: finalCumulativeAmount,
|
|
323
|
-
lastSequence: input.sequence,
|
|
324
|
-
maxCumulativeAmount,
|
|
325
|
-
...(progress.maxDepositAmount !== undefined ? { maxDepositAmount: progress.maxDepositAmount } : {}),
|
|
326
|
-
...(progress.swigRoleId !== undefined ? { swigRoleId: progress.swigRoleId } : {}),
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
return {
|
|
330
|
-
voucher,
|
|
331
|
-
...(closeTx ? { closeTx } : {}),
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
private assertNotExpired() {
|
|
336
|
-
if (this.validUntilUnixMs !== undefined && Date.now() > this.validUntilUnixMs) {
|
|
337
|
-
throw new Error('Budget authorizer policy has expired');
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
private assertProgramAllowed(channelProgram: string) {
|
|
342
|
-
if (!this.allowedPrograms) {
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (!this.allowedPrograms.has(channelProgram)) {
|
|
347
|
-
throw new Error(`Channel program is not allowed: ${channelProgram}`);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
private assertMonotonic(
|
|
352
|
-
channelId: string,
|
|
353
|
-
sequence: number,
|
|
354
|
-
cumulativeAmount: bigint,
|
|
355
|
-
progress: ChannelProgress | undefined,
|
|
356
|
-
) {
|
|
357
|
-
if (!Number.isInteger(sequence) || sequence < 0) {
|
|
358
|
-
throw new Error('Sequence must be a non-negative integer');
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (!progress) {
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (sequence <= progress.lastSequence) {
|
|
366
|
-
throw new Error(
|
|
367
|
-
`Sequence must increase for channel ${channelId}. Last=${progress.lastSequence}, received=${sequence}`,
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (cumulativeAmount < progress.lastCumulative) {
|
|
372
|
-
throw new Error(
|
|
373
|
-
`Cumulative amount must not decrease for channel ${channelId}. Last=${progress.lastCumulative.toString()}, received=${cumulativeAmount.toString()}`,
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
private async resolveOnChainConstraints(input: AuthorizeOpenInput): Promise<{
|
|
379
|
-
maxCumulativeAmount: bigint;
|
|
380
|
-
maxDepositAmount: bigint;
|
|
381
|
-
swigRoleId: number;
|
|
382
|
-
}> {
|
|
383
|
-
// Budget mode requires Swig action metadata at runtime.
|
|
384
|
-
await this.ensureSwigInstalled();
|
|
385
|
-
|
|
386
|
-
const swigModule = this.swigModule;
|
|
387
|
-
if (!swigModule) {
|
|
388
|
-
throw new Error('Swig SDK was not loaded before budget role validation');
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const rpc = createSolanaRpc(this.resolveRpcUrl(input.network));
|
|
392
|
-
const swig = await swigModule.fetchSwig(rpc, this.swig.swigAddress);
|
|
393
|
-
const role = this.resolveSwigRole(swig);
|
|
394
|
-
const actions = role.actions;
|
|
395
|
-
|
|
396
|
-
if (!actions) {
|
|
397
|
-
throw new Error(`Swig role ${role.id} does not expose action metadata for budget validation`);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (!actions.canUseProgram) {
|
|
401
|
-
throw new Error(`Swig role ${role.id} does not expose canUseProgram() for program authorization checks`);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (!actions.canUseProgram(input.channelProgram)) {
|
|
405
|
-
throw new Error(`Swig role ${role.id} does not allow channel program ${input.channelProgram}`);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const onChainLimit = this.resolveOnChainSpendLimit(actions, input);
|
|
409
|
-
if (onChainLimit === null) {
|
|
410
|
-
throw new Error(
|
|
411
|
-
`Swig role ${role.id} has uncapped ${input.asset.kind.toUpperCase()} spending; BudgetAuthorizer requires an on-chain spend cap`,
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
return {
|
|
416
|
-
maxCumulativeAmount: minBigInt(this.maxCumulativeAmount, onChainLimit),
|
|
417
|
-
maxDepositAmount:
|
|
418
|
-
this.maxDepositAmount !== undefined ? minBigInt(this.maxDepositAmount, onChainLimit) : onChainLimit,
|
|
419
|
-
swigRoleId: role.id,
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
private resolveSwigRole(swig: SwigAccount): SwigRole {
|
|
424
|
-
// Role ID is required by construction, so this path is deterministic.
|
|
425
|
-
if (!swig.findRoleById) {
|
|
426
|
-
throw new Error('Swig account object does not expose findRoleById() required for configured swigRoleId');
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const role = swig.findRoleById(this.swig.swigRoleId);
|
|
430
|
-
if (!role) {
|
|
431
|
-
throw new Error(`Unable to locate Swig role ${this.swig.swigRoleId} for signer ${this.signer.address}`);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
if (swig.findRolesByEd25519SignerPk) {
|
|
435
|
-
const signerRoles = swig.findRolesByEd25519SignerPk(this.signer.address);
|
|
436
|
-
const roleMatchesSigner = signerRoles.some(signerRole => signerRole.id === role.id);
|
|
437
|
-
|
|
438
|
-
if (roleMatchesSigner) {
|
|
439
|
-
return role;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
if (swig.findRoleBySessionKey) {
|
|
444
|
-
const sessionRole = swig.findRoleBySessionKey(this.signer.address);
|
|
445
|
-
if (sessionRole?.id === role.id) {
|
|
446
|
-
return role;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const authorityAddresses = collectAuthorityAddresses(role.authority);
|
|
451
|
-
if (authorityAddresses.includes(this.signer.address)) {
|
|
452
|
-
return role;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
throw new Error(`Configured Swig role ${role.id} does not match signer ${this.signer.address}`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
private resolveOnChainSpendLimit(actions: SwigRoleActions, input: AuthorizeOpenInput): bigint | null {
|
|
459
|
-
if (input.asset.kind === 'spl') {
|
|
460
|
-
if (!input.asset.mint) {
|
|
461
|
-
throw new Error('asset.mint is required for SPL budget validation');
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (!actions.tokenSpendLimit) {
|
|
465
|
-
throw new Error('Swig role does not expose tokenSpendLimit() for SPL budget validation');
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
return actions.tokenSpendLimit(input.asset.mint);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (!actions.solSpendLimit) {
|
|
472
|
-
throw new Error('Swig role does not expose solSpendLimit() for SOL budget validation');
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return actions.solSpendLimit();
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
private resolveRpcUrl(network: string): string {
|
|
479
|
-
return this.swig.rpcUrl ?? DEFAULT_RPC_URLS[network] ?? DEFAULT_RPC_URLS['mainnet-beta'];
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
private async ensureSwigInstalled() {
|
|
483
|
-
if (this.swigLoaded) {
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
try {
|
|
488
|
-
const swigPackageName = '@swig-wallet/kit';
|
|
489
|
-
const module = (await import(swigPackageName)) as Partial<BudgetSwigModule>;
|
|
490
|
-
if (typeof module.fetchSwig !== 'function') {
|
|
491
|
-
throw new Error('Installed `@swig-wallet/kit` does not export fetchSwig() at runtime');
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
this.swigModule = {
|
|
495
|
-
fetchSwig: module.fetchSwig,
|
|
496
|
-
};
|
|
497
|
-
this.swigLoaded = true;
|
|
498
|
-
} catch {
|
|
499
|
-
throw new Error(
|
|
500
|
-
'BudgetAuthorizer with `swig` config requires optional dependency `@swig-wallet/kit`. Install it with `npm install @swig-wallet/kit`.',
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
private async resolveOpenTx(input: AuthorizeOpenInput): Promise<string> {
|
|
506
|
-
if (!this.buildOpenTx) {
|
|
507
|
-
throw new Error('BudgetAuthorizer requires `buildOpenTx` to authorize open requests');
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
return await this.buildOpenTx(input);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
private async resolveTopupTx(input: AuthorizeTopupInput): Promise<string> {
|
|
514
|
-
if (!this.buildTopupTx) {
|
|
515
|
-
throw new Error('BudgetAuthorizer requires `buildTopupTx` to authorize topup requests');
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
return await this.buildTopupTx(input);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
private async resolveCloseTx(input: AuthorizeCloseInput): Promise<string | undefined> {
|
|
522
|
-
if (!this.buildCloseTx) {
|
|
523
|
-
return undefined;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
return await this.buildCloseTx(input);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function collectAuthorityAddresses(authority: SwigRoleAuthority | undefined): string[] {
|
|
531
|
-
if (!authority) {
|
|
532
|
-
return [];
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Some Swig authority variants expose only one of these fields.
|
|
536
|
-
const candidates = [authority.publicKey, authority.ed25519PublicKey, authority.sessionKey];
|
|
537
|
-
|
|
538
|
-
return candidates.map(candidate => candidate?.toBase58?.()).filter((candidate): candidate is string => !!candidate);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function parseNonNegativeAmount(value: string, field: string): bigint {
|
|
542
|
-
let amount: bigint;
|
|
543
|
-
try {
|
|
544
|
-
amount = BigInt(value);
|
|
545
|
-
} catch {
|
|
546
|
-
throw new Error(`${field} must be a valid integer string`);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
if (amount < 0n) {
|
|
550
|
-
throw new Error(`${field} must be non-negative`);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return amount;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
function parseIsoTimestamp(value: string, field: string): number {
|
|
557
|
-
const unixMs = Date.parse(value);
|
|
558
|
-
if (Number.isNaN(unixMs)) {
|
|
559
|
-
throw new Error(`${field} must be a valid ISO timestamp`);
|
|
560
|
-
}
|
|
561
|
-
return unixMs;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function minBigInt(a: bigint, b: bigint): bigint {
|
|
565
|
-
return a <= b ? a : b;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function normalizeChainId(network: string): string {
|
|
569
|
-
const normalized = network.trim();
|
|
570
|
-
if (normalized.length === 0) {
|
|
571
|
-
throw new Error('network must be a non-empty string');
|
|
572
|
-
}
|
|
573
|
-
return normalized.startsWith('solana:') ? normalized : `solana:${normalized}`;
|
|
574
|
-
}
|