@subly_fi/pay 0.6.2 → 0.7.1
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 +94 -152
- package/dist/budget.js +2267 -0
- package/dist/cli.js +37 -20
- package/dist/deposit.js +243 -58
- package/dist/doctor.js +188 -0
- package/dist/mcp-server.js +1537 -1227
- package/dist/pay.js +437 -225
- package/dist/setup-link.js +238 -55
- package/dist/vaults.js +129 -0
- package/dist/withdraw.js +238 -55
- package/package.json +7 -5
package/dist/mcp-server.js
CHANGED
|
@@ -1,24 +1,367 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
// ../../src/config/vault-catalog.ts
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
|
|
5
|
-
// ../../src/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
// ../../src/lib/solana-address.ts
|
|
6
|
+
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
7
|
+
var BASE58_LOOKUP = new Map(
|
|
8
|
+
[...BASE58_ALPHABET].map((character, index) => [character, BigInt(index)])
|
|
9
|
+
);
|
|
10
|
+
function assertSolanaAddress(value, fieldName) {
|
|
11
|
+
if (value.length < 32 || value.length > 44) {
|
|
12
|
+
throw new Error(`${fieldName} must be a valid Solana public key`);
|
|
13
|
+
}
|
|
14
|
+
if (decodeBase58(value).length !== 32) {
|
|
15
|
+
throw new Error(`${fieldName} must be a valid Solana public key`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function decodeBase58(value) {
|
|
20
|
+
if (value.length === 0) {
|
|
21
|
+
return new Uint8Array();
|
|
22
|
+
}
|
|
23
|
+
let decoded = 0n;
|
|
24
|
+
for (const character of value) {
|
|
25
|
+
const digit = BASE58_LOOKUP.get(character);
|
|
26
|
+
if (digit === void 0) {
|
|
27
|
+
return new Uint8Array();
|
|
28
|
+
}
|
|
29
|
+
decoded = decoded * 58n + digit;
|
|
30
|
+
}
|
|
31
|
+
const bytes = [];
|
|
32
|
+
while (decoded > 0n) {
|
|
33
|
+
bytes.push(Number(decoded & 0xffn));
|
|
34
|
+
decoded >>= 8n;
|
|
35
|
+
}
|
|
36
|
+
for (const character of value) {
|
|
37
|
+
if (character !== "1") {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
bytes.push(0);
|
|
41
|
+
}
|
|
42
|
+
return Uint8Array.from(bytes.reverse());
|
|
43
|
+
}
|
|
12
44
|
|
|
13
|
-
// ../../src/
|
|
45
|
+
// ../../src/config/vault.ts
|
|
46
|
+
var MAINNET_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
47
|
+
var KAMINO_VAULT_PROGRAM_ID = "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd";
|
|
48
|
+
var NO_VAULT_FARM = "11111111111111111111111111111111";
|
|
49
|
+
var DEFAULT_VAULT_CONFIG = Object.freeze({
|
|
50
|
+
address: "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr",
|
|
51
|
+
programId: KAMINO_VAULT_PROGRAM_ID,
|
|
52
|
+
usdcMint: MAINNET_USDC_MINT,
|
|
53
|
+
shareMint: "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a",
|
|
54
|
+
farm: "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
|
|
55
|
+
});
|
|
56
|
+
function vaultConfigFromEnv(env = process.env) {
|
|
57
|
+
const value = (name) => env[name]?.trim() || void 0;
|
|
58
|
+
const vaultAddress = value("SUBLY_VAULT_ADDRESS") ?? DEFAULT_VAULT_CONFIG.address;
|
|
59
|
+
const customVault = vaultAddress !== DEFAULT_VAULT_CONFIG.address;
|
|
60
|
+
const anchor = (name, fallback) => {
|
|
61
|
+
const configured = value(name);
|
|
62
|
+
if (customVault && configured === void 0) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`${name} is required for a custom vault. Generate its settings with npm run configure:vault -- <vault-address>; use ${NO_VAULT_FARM} for a vault without a farm.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return assertSolanaAddress(configured ?? fallback, name);
|
|
68
|
+
};
|
|
69
|
+
const usdcMint = value("SUBLY_VAULT_USDC_MINT") ?? MAINNET_USDC_MINT;
|
|
70
|
+
if (usdcMint !== MAINNET_USDC_MINT) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"SUBLY_VAULT_USDC_MINT must be mainnet USDC; other deposit assets are not supported"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
address: assertSolanaAddress(vaultAddress, "SUBLY_VAULT_ADDRESS"),
|
|
77
|
+
programId: KAMINO_VAULT_PROGRAM_ID,
|
|
78
|
+
usdcMint,
|
|
79
|
+
shareMint: anchor("SUBLY_VAULT_SHARE_MINT", DEFAULT_VAULT_CONFIG.shareMint),
|
|
80
|
+
farm: anchor("SUBLY_VAULT_FARM", DEFAULT_VAULT_CONFIG.farm)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ../../src/config/vault-catalog.ts
|
|
85
|
+
var publicKey = z.string().refine((value) => {
|
|
86
|
+
try {
|
|
87
|
+
assertSolanaAddress(value, "vault catalog address");
|
|
88
|
+
return true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}, "Invalid Solana public key");
|
|
93
|
+
var catalogSchema = z.object({
|
|
94
|
+
version: z.literal(1),
|
|
95
|
+
defaultVault: publicKey,
|
|
96
|
+
vaults: z.array(z.object({
|
|
97
|
+
address: publicKey,
|
|
98
|
+
programId: z.literal(KAMINO_VAULT_PROGRAM_ID),
|
|
99
|
+
usdcMint: z.literal(MAINNET_USDC_MINT),
|
|
100
|
+
shareMint: publicKey,
|
|
101
|
+
farm: publicKey,
|
|
102
|
+
name: z.string().min(1).max(128).optional(),
|
|
103
|
+
depositsEnabled: z.boolean().optional(),
|
|
104
|
+
extraLookupTables: z.array(publicKey).max(16).optional()
|
|
105
|
+
}).strict()).min(1).max(100)
|
|
106
|
+
}).strict();
|
|
107
|
+
function parseVaultCatalog(value) {
|
|
108
|
+
const catalog2 = catalogSchema.parse(value);
|
|
109
|
+
const addresses = new Set(catalog2.vaults.map((vault) => vault.address));
|
|
110
|
+
if (addresses.size !== catalog2.vaults.length) throw new Error("Duplicate vault in catalog");
|
|
111
|
+
if (!addresses.has(catalog2.defaultVault)) throw new Error("defaultVault must be in the vault catalog");
|
|
112
|
+
return { ...catalog2, vaults: catalog2.vaults.map((vault) => Object.freeze(vault)) };
|
|
113
|
+
}
|
|
114
|
+
function vaultCatalogFromEnv(env = process.env) {
|
|
115
|
+
const path = env.SUBLY_VAULTS_FILE?.trim();
|
|
116
|
+
if (!path) {
|
|
117
|
+
const vault = vaultConfigFromEnv(env);
|
|
118
|
+
return { version: 1, defaultVault: vault.address, vaults: [vault] };
|
|
119
|
+
}
|
|
120
|
+
const catalog2 = parseVaultCatalog(JSON.parse(readFileSync(path, "utf8")));
|
|
121
|
+
const selected2 = env.SUBLY_VAULT_ADDRESS?.trim() || catalog2.defaultVault;
|
|
122
|
+
if (!catalog2.vaults.some((vault) => vault.address === selected2)) {
|
|
123
|
+
throw new Error("SUBLY_VAULT_ADDRESS must be in SUBLY_VAULTS_FILE");
|
|
124
|
+
}
|
|
125
|
+
return { ...catalog2, defaultVault: selected2 };
|
|
126
|
+
}
|
|
127
|
+
function defaultCatalogVault(catalog2) {
|
|
128
|
+
return catalog2.vaults.find((vault) => vault.address === catalog2.defaultVault);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ../../src/client/vault-selection.ts
|
|
132
|
+
var McpVaultSelection = class {
|
|
133
|
+
selectedAddress;
|
|
134
|
+
sessions;
|
|
135
|
+
constructor(sessions2, defaultVault) {
|
|
136
|
+
this.sessions = new Map(sessions2.map((session) => [session.vault.address, session]));
|
|
137
|
+
if (this.sessions.size !== sessions2.length || !this.sessions.has(defaultVault)) {
|
|
138
|
+
throw new Error("MCP vault sessions must be unique and include the default vault");
|
|
139
|
+
}
|
|
140
|
+
this.selectedAddress = defaultVault;
|
|
141
|
+
}
|
|
142
|
+
current() {
|
|
143
|
+
return this.sessions.get(this.selectedAddress);
|
|
144
|
+
}
|
|
145
|
+
list() {
|
|
146
|
+
return {
|
|
147
|
+
selectedVault: this.selectedAddress,
|
|
148
|
+
vaults: [...this.sessions.values()].map(({ vault }) => vault)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** The relayer only confirms support; addresses/mints are always pinned locally. */
|
|
152
|
+
async select(address3, relayerBaseUrl2, fetchImpl = fetch) {
|
|
153
|
+
const session = this.sessions.get(address3);
|
|
154
|
+
if (!session) throw new Error("Vault is not in this MCP client's local catalog");
|
|
155
|
+
const response = await fetchImpl(`${relayerBaseUrl2.replace(/\/$/, "")}/v1/vaults`, {
|
|
156
|
+
signal: AbortSignal.timeout(1e4)
|
|
157
|
+
});
|
|
158
|
+
if (!response.ok) throw new Error(`Relayer vault list returned ${response.status}`);
|
|
159
|
+
const body = await response.json();
|
|
160
|
+
const remote = Array.isArray(body.vaults) ? body.vaults.find((vault) => vault?.address === address3) : void 0;
|
|
161
|
+
if (!remote) throw new Error("Vault is not configured on this relayer");
|
|
162
|
+
for (const field of ["programId", "usdcMint", "shareMint", "farm"]) {
|
|
163
|
+
if (remote[field] !== session.vault[field]) {
|
|
164
|
+
throw new Error(`Relayer ${field} differs from the local vault catalog`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
this.selectedAddress = address3;
|
|
168
|
+
return {
|
|
169
|
+
selectedVault: address3,
|
|
170
|
+
name: session.vault.name ?? null,
|
|
171
|
+
depositsEnabled: session.vault.depositsEnabled !== false && remote.depositsEnabled !== false,
|
|
172
|
+
instructions: "Subsequent tools use this vault. Existing funds stay in their original vault. Each vault needs its own owner setup and spending limits. Selection lasts until changed or this MCP process restarts."
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// ../../src/config/constants.ts
|
|
178
|
+
var PAYMENT_SCHEME = "subly-yield-exact";
|
|
179
|
+
var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
180
|
+
var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
181
|
+
var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
182
|
+
var SUBLY_VAULT = defaultCatalogVault(vaultCatalogFromEnv());
|
|
183
|
+
var USDC_DECIMALS = 6;
|
|
184
|
+
|
|
185
|
+
// ../../src/lib/associated-token-account.ts
|
|
14
186
|
import { createHash } from "node:crypto";
|
|
15
187
|
import bs58 from "bs58";
|
|
188
|
+
var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
|
|
189
|
+
var ED25519_P = (1n << 255n) - 19n;
|
|
190
|
+
var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
|
|
191
|
+
function deriveAssociatedTokenAddress(params) {
|
|
192
|
+
const owner = decodePublicKey(params.owner, "owner");
|
|
193
|
+
const mint = decodePublicKey(params.mint ?? SUBLY_VAULT.usdcMint, "mint");
|
|
194
|
+
const tokenProgramId = decodePublicKey(
|
|
195
|
+
params.tokenProgramId ?? SPL_TOKEN_PROGRAM_ID,
|
|
196
|
+
"tokenProgramId"
|
|
197
|
+
);
|
|
198
|
+
const associatedTokenProgramId = decodePublicKey(
|
|
199
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
200
|
+
"associatedTokenProgramId"
|
|
201
|
+
);
|
|
202
|
+
for (let bump = 255; bump >= 0; bump -= 1) {
|
|
203
|
+
const address3 = createProgramAddress(
|
|
204
|
+
[owner, tokenProgramId, mint, Uint8Array.of(bump)],
|
|
205
|
+
associatedTokenProgramId
|
|
206
|
+
);
|
|
207
|
+
if (address3 !== null) {
|
|
208
|
+
return bs58.encode(address3);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
throw new Error("Unable to derive associated token account address");
|
|
212
|
+
}
|
|
213
|
+
function createProgramAddress(seeds, programId) {
|
|
214
|
+
const hash = createHash("sha256");
|
|
215
|
+
for (const seed of seeds) {
|
|
216
|
+
hash.update(seed);
|
|
217
|
+
}
|
|
218
|
+
hash.update(programId);
|
|
219
|
+
hash.update(PDA_MARKER);
|
|
220
|
+
const digest = hash.digest();
|
|
221
|
+
return isEd25519Point(digest) ? null : new Uint8Array(digest);
|
|
222
|
+
}
|
|
223
|
+
function decodePublicKey(value, fieldName) {
|
|
224
|
+
const decoded = bs58.decode(value);
|
|
225
|
+
if (decoded.length !== 32) {
|
|
226
|
+
throw new Error(`${fieldName} must be a 32-byte public key`);
|
|
227
|
+
}
|
|
228
|
+
return decoded;
|
|
229
|
+
}
|
|
230
|
+
function isEd25519Point(bytes) {
|
|
231
|
+
if (bytes.length !== 32) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
const yBytes = Uint8Array.from(bytes);
|
|
235
|
+
yBytes[31] = yBytes[31] & 127;
|
|
236
|
+
const y = littleEndianToBigInt(yBytes);
|
|
237
|
+
if (y >= ED25519_P) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
const ySquared = mod(y * y, ED25519_P);
|
|
241
|
+
const numerator = mod(ySquared - 1n, ED25519_P);
|
|
242
|
+
const denominator = mod(ED25519_D * ySquared + 1n, ED25519_P);
|
|
243
|
+
if (denominator === 0n) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
const xSquared = mod(
|
|
247
|
+
numerator * modPow(denominator, ED25519_P - 2n, ED25519_P),
|
|
248
|
+
ED25519_P
|
|
249
|
+
);
|
|
250
|
+
return xSquared === 0n || modPow(xSquared, (ED25519_P - 1n) / 2n, ED25519_P) === 1n;
|
|
251
|
+
}
|
|
252
|
+
function littleEndianToBigInt(bytes) {
|
|
253
|
+
let value = 0n;
|
|
254
|
+
for (let index = bytes.length - 1; index >= 0; index -= 1) {
|
|
255
|
+
value = (value << 8n) + BigInt(bytes[index]);
|
|
256
|
+
}
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
function mod(value, modulus) {
|
|
260
|
+
const result = value % modulus;
|
|
261
|
+
return result >= 0n ? result : result + modulus;
|
|
262
|
+
}
|
|
263
|
+
function modPow(base, exponent, modulus) {
|
|
264
|
+
let result = 1n;
|
|
265
|
+
let nextBase = mod(base, modulus);
|
|
266
|
+
let nextExponent = exponent;
|
|
267
|
+
while (nextExponent > 0n) {
|
|
268
|
+
if ((nextExponent & 1n) === 1n) {
|
|
269
|
+
result = mod(result * nextBase, modulus);
|
|
270
|
+
}
|
|
271
|
+
nextBase = mod(nextBase * nextBase, modulus);
|
|
272
|
+
nextExponent >>= 1n;
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ../../src/client/withdrawal-preview.ts
|
|
278
|
+
var ROUNDING_RAW_USDC = 10n;
|
|
279
|
+
async function assertWithdrawalPreview(input) {
|
|
280
|
+
const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
|
|
281
|
+
const simulation = await input.rpc.simulateTransaction(
|
|
282
|
+
input.serializedTransaction,
|
|
283
|
+
{
|
|
284
|
+
encoding: "base64",
|
|
285
|
+
commitment: "confirmed",
|
|
286
|
+
sigVerify: false,
|
|
287
|
+
replaceRecentBlockhash: false,
|
|
288
|
+
innerInstructions: true
|
|
289
|
+
}
|
|
290
|
+
).send({ abortSignal: AbortSignal.timeout(15e3) });
|
|
291
|
+
if (simulation.value.err !== null) {
|
|
292
|
+
throw new Error("Withdrawal preview failed on the client RPC; no transaction was signed. Check liquidity, RPC and blockhash, then prepare again.");
|
|
293
|
+
}
|
|
294
|
+
let received = 0n;
|
|
295
|
+
for (const group of simulation.value.innerInstructions ?? []) {
|
|
296
|
+
for (const instruction of group.instructions) {
|
|
297
|
+
if (!("parsed" in instruction) || instruction.programId !== SPL_TOKEN_PROGRAM_ID) continue;
|
|
298
|
+
const parsed = instruction.parsed;
|
|
299
|
+
if (parsed.type !== "transfer" && parsed.type !== "transferChecked") continue;
|
|
300
|
+
const info = parsed.info;
|
|
301
|
+
if (!info || info.destination !== destination && info.source !== destination) continue;
|
|
302
|
+
const raw = parsed.type === "transferChecked" ? info.tokenAmount?.amount : info.amount;
|
|
303
|
+
if (typeof raw !== "string" || !/^\d+$/.test(raw)) {
|
|
304
|
+
throw new Error("Withdrawal preview returned an invalid token amount");
|
|
305
|
+
}
|
|
306
|
+
const amount = BigInt(raw);
|
|
307
|
+
if (info.destination === destination) received += amount;
|
|
308
|
+
if (info.source === destination) received -= amount;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (received <= 0n || received > input.amountRawUsdc + ROUNDING_RAW_USDC || received < input.amountRawUsdc - ROUNDING_RAW_USDC) {
|
|
312
|
+
throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ../../src/client/lookup-tables.ts
|
|
317
|
+
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
|
|
318
|
+
import { address, getCompiledTransactionMessageDecoder } from "@solana/kit";
|
|
319
|
+
function lookupTableAddressesForTransaction(serializedTransaction) {
|
|
320
|
+
const wire = Buffer.from(serializedTransaction, "base64");
|
|
321
|
+
let offset = 0;
|
|
322
|
+
let signatureCount = 0;
|
|
323
|
+
let shift = 0;
|
|
324
|
+
while (offset < wire.length) {
|
|
325
|
+
const byte = wire[offset];
|
|
326
|
+
signatureCount |= (byte & 127) << shift;
|
|
327
|
+
offset += 1;
|
|
328
|
+
if ((byte & 128) === 0) {
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
shift += 7;
|
|
332
|
+
}
|
|
333
|
+
const messageBytes = wire.subarray(offset + signatureCount * 64);
|
|
334
|
+
const compiled = getCompiledTransactionMessageDecoder().decode(messageBytes);
|
|
335
|
+
const lookups = compiled.addressTableLookups ?? [];
|
|
336
|
+
return lookups.map((lookup) => String(lookup.lookupTableAddress));
|
|
337
|
+
}
|
|
338
|
+
async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
|
|
339
|
+
const addresses = lookupTableAddressesForTransaction(serializedTransaction);
|
|
340
|
+
if (addresses.length === 0) {
|
|
341
|
+
return {};
|
|
342
|
+
}
|
|
343
|
+
const tables = await fetchAllMaybeAddressLookupTable(
|
|
344
|
+
rpc2,
|
|
345
|
+
addresses.map((value) => address(value))
|
|
346
|
+
);
|
|
347
|
+
const result = {};
|
|
348
|
+
for (const table of tables) {
|
|
349
|
+
if (table.exists) {
|
|
350
|
+
result[table.address] = table.data.addresses.map(String);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ../../src/api/wallet-auth.ts
|
|
357
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
358
|
+
import bs582 from "bs58";
|
|
16
359
|
import nacl from "tweetnacl";
|
|
17
360
|
var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
|
|
18
361
|
var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
|
|
19
362
|
var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
|
|
20
363
|
function sha256Hex(data) {
|
|
21
|
-
return
|
|
364
|
+
return createHash2("sha256").update(data, "utf8").digest("hex");
|
|
22
365
|
}
|
|
23
366
|
function walletAuthMessage(params) {
|
|
24
367
|
return new TextEncoder().encode(
|
|
@@ -47,1054 +390,1165 @@ async function walletAuthHeaders(params) {
|
|
|
47
390
|
};
|
|
48
391
|
}
|
|
49
392
|
|
|
50
|
-
// ../../src/client/
|
|
51
|
-
var
|
|
52
|
-
|
|
53
|
-
constructor(step, message, detail = null) {
|
|
393
|
+
// ../../src/client/vault-flows.ts
|
|
394
|
+
var VaultFlowClientError = class extends Error {
|
|
395
|
+
constructor(step, message, detail = null, code = null, errorDetails = null) {
|
|
54
396
|
super(message);
|
|
55
397
|
this.step = step;
|
|
56
398
|
this.detail = detail;
|
|
57
|
-
this.
|
|
399
|
+
this.code = code;
|
|
400
|
+
this.errorDetails = errorDetails;
|
|
401
|
+
this.name = "VaultFlowClientError";
|
|
58
402
|
}
|
|
59
403
|
step;
|
|
60
404
|
detail;
|
|
405
|
+
code;
|
|
406
|
+
errorDetails;
|
|
61
407
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}),
|
|
77
|
-
"content-type": "application/json"
|
|
78
|
-
},
|
|
79
|
-
body: serialized
|
|
80
|
-
});
|
|
81
|
-
if (response.status !== 200) {
|
|
82
|
-
let detail = null;
|
|
83
|
-
try {
|
|
84
|
-
detail = await response.json();
|
|
85
|
-
} catch {
|
|
86
|
-
detail = null;
|
|
87
|
-
}
|
|
88
|
-
throw new OnboardingError(
|
|
89
|
-
step,
|
|
90
|
-
`wallet onboarding ${step} failed with ${response.status}`,
|
|
91
|
-
detail
|
|
92
|
-
);
|
|
408
|
+
var VaultFlowClient = class {
|
|
409
|
+
vault;
|
|
410
|
+
rpc;
|
|
411
|
+
baseUrl;
|
|
412
|
+
signer;
|
|
413
|
+
fetchImpl;
|
|
414
|
+
lookupTablesFor;
|
|
415
|
+
pollTimeoutMs;
|
|
416
|
+
pollIntervalMs;
|
|
417
|
+
constructor(config) {
|
|
418
|
+
this.rpc = config.rpc;
|
|
419
|
+
this.vault = config.vault ?? config.signer.vault ?? SUBLY_VAULT;
|
|
420
|
+
if (config.signer.vault && config.signer.vault.address !== this.vault.address) {
|
|
421
|
+
throw new Error("Vault flow client and signer must select the same vault");
|
|
93
422
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
signerValidationMode: params.signer.validationMode,
|
|
101
|
-
signerProvider: params.signer.provider ?? "local-keypair",
|
|
102
|
-
activateForPayments: true
|
|
103
|
-
});
|
|
104
|
-
await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ../../src/x402/headers.ts
|
|
108
|
-
import { z } from "zod";
|
|
109
|
-
|
|
110
|
-
// ../../src/config/constants.ts
|
|
111
|
-
var PAYMENT_SCHEME = "subly-yield-exact";
|
|
112
|
-
var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
113
|
-
var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
114
|
-
var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
115
|
-
var envOr = (name, fallback) => {
|
|
116
|
-
const value = process.env[name]?.trim();
|
|
117
|
-
return value ? value : fallback;
|
|
118
|
-
};
|
|
119
|
-
var SUBLY_VAULT = {
|
|
120
|
-
name: "Subly USDC Payment Vault Alpha",
|
|
121
|
-
address: envOr(
|
|
122
|
-
"SUBLY_VAULT_ADDRESS",
|
|
123
|
-
"5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr"
|
|
124
|
-
),
|
|
125
|
-
programId: "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd",
|
|
126
|
-
usdcMint: envOr(
|
|
127
|
-
"SUBLY_VAULT_USDC_MINT",
|
|
128
|
-
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
129
|
-
),
|
|
130
|
-
shareMint: envOr(
|
|
131
|
-
"SUBLY_VAULT_SHARE_MINT",
|
|
132
|
-
"7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a"
|
|
133
|
-
),
|
|
134
|
-
lookupTable: "7UbXhDnpK7WVnwsfivzQRENoqKqAULQ5s19gS1xJrQEo",
|
|
135
|
-
farm: envOr(
|
|
136
|
-
"SUBLY_VAULT_FARM",
|
|
137
|
-
"E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
|
|
138
|
-
)
|
|
139
|
-
};
|
|
140
|
-
var USDC_DECIMALS = 6;
|
|
141
|
-
|
|
142
|
-
// ../../src/lib/hash.ts
|
|
143
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
144
|
-
var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
145
|
-
function sha256TaggedHex(data) {
|
|
146
|
-
return `sha256-${createHash2("sha256").update(data).digest("hex")}`;
|
|
147
|
-
}
|
|
148
|
-
function stableStringify(value) {
|
|
149
|
-
if (value === null) {
|
|
150
|
-
return "null";
|
|
151
|
-
}
|
|
152
|
-
if (typeof value === "bigint") {
|
|
153
|
-
return JSON.stringify(value.toString());
|
|
154
|
-
}
|
|
155
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
156
|
-
return JSON.stringify(value);
|
|
157
|
-
}
|
|
158
|
-
if (Array.isArray(value)) {
|
|
159
|
-
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
423
|
+
this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
|
|
424
|
+
this.signer = config.signer;
|
|
425
|
+
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
426
|
+
this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
|
|
427
|
+
this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
|
|
428
|
+
this.pollIntervalMs = config.pollIntervalMs ?? 2500;
|
|
160
429
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
430
|
+
/**
|
|
431
|
+
* Moves USDC from the agent wallet into the vault (fee sponsored). Under
|
|
432
|
+
* depositPolicy "owner_approval_required" the relayer refuses to prepare
|
|
433
|
+
* without an owner approval; when the caller passes none, an already
|
|
434
|
+
* APPROVED deposit approval for this exact amount (e.g. the mandate's
|
|
435
|
+
* initialDeposit — "one Face ID covers mandate + first deposit") is looked
|
|
436
|
+
* up and used automatically before surfacing deposit_approval_required.
|
|
437
|
+
*/
|
|
438
|
+
async deposit(input) {
|
|
439
|
+
let approvalId = input.approvalId;
|
|
440
|
+
let prepared;
|
|
441
|
+
try {
|
|
442
|
+
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
443
|
+
wallet: this.signer.walletAddress,
|
|
444
|
+
vault: this.vault.address,
|
|
445
|
+
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
446
|
+
...approvalId === void 0 ? {} : { approvalId }
|
|
447
|
+
});
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId !== void 0) {
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
approvalId = await this.findApprovedDepositApproval(input.amountRawUsdc);
|
|
453
|
+
if (approvalId === void 0) {
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
457
|
+
wallet: this.signer.walletAddress,
|
|
458
|
+
vault: this.vault.address,
|
|
459
|
+
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
460
|
+
approvalId
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.signingIntent.amountRawUsdc !== input.amountRawUsdc.toString()) {
|
|
464
|
+
throw new VaultFlowClientError("prepare", "Prepared deposit differs from the requested wallet, vault or amount");
|
|
465
|
+
}
|
|
466
|
+
const signed = await this.signer.signDeposit({
|
|
467
|
+
intent: prepared.signingIntent,
|
|
468
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
469
|
+
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
470
|
+
});
|
|
471
|
+
let outcome = await this.postJson("submit", "/v1/deposits/submit", {
|
|
472
|
+
depositId: prepared.depositId,
|
|
473
|
+
serializedTransaction: signed.serializedTransaction,
|
|
474
|
+
agentSignature: signed.agentSignature
|
|
475
|
+
});
|
|
476
|
+
if (outcome.status === "submitted") {
|
|
477
|
+
outcome = await this.pollUntilTerminal(
|
|
478
|
+
`/v1/deposits/${prepared.depositId}`,
|
|
479
|
+
outcome
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
depositId: prepared.depositId,
|
|
484
|
+
status: outcome.status,
|
|
485
|
+
txSignature: outcome.txSignature ?? null,
|
|
486
|
+
actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
|
|
487
|
+
sharesMintedRaw: outcome.sharesMintedRaw ?? null,
|
|
488
|
+
errorCode: outcome.errorCode ?? null
|
|
489
|
+
};
|
|
177
490
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
})
|
|
197
|
-
}).loose();
|
|
198
|
-
var paymentRequiredSchema = z.object({
|
|
199
|
-
x402Version: z.number().int(),
|
|
200
|
-
accepts: z.array(z.unknown()),
|
|
201
|
-
error: z.string().optional()
|
|
202
|
-
}).loose();
|
|
203
|
-
var sublyPaymentPayloadSchema = z.object({
|
|
204
|
-
x402Version: z.number().int(),
|
|
205
|
-
scheme: z.literal(PAYMENT_SCHEME),
|
|
206
|
-
network: z.string().min(1),
|
|
207
|
-
payload: z.object({
|
|
208
|
-
paymentId: z.string().min(1),
|
|
209
|
-
requestBindingHash: z.string().min(1),
|
|
210
|
-
preparedMessageHash: z.string().min(1),
|
|
211
|
-
serializedTransaction: z.string().min(1).max(4096),
|
|
212
|
-
agentSignature: z.string().min(1).max(128),
|
|
213
|
-
temporarySettlementSignature: z.string().min(1).max(128)
|
|
214
|
-
})
|
|
215
|
-
}).loose();
|
|
216
|
-
function decodeX402Header(headerValue) {
|
|
217
|
-
if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
|
|
218
|
-
throw new X402HeaderError(
|
|
219
|
-
"header_too_large",
|
|
220
|
-
`x402 header exceeds ${MAX_HEADER_JSON_BYTES} encoded bytes`
|
|
491
|
+
/**
|
|
492
|
+
* Moves USDC from the vault back to the agent wallet's USDC ATA (fee
|
|
493
|
+
* sponsored). A plain withdrawal is the exit path and MAY spend principal;
|
|
494
|
+
* with purpose "yield_realize" the relayer refuses anything beyond the
|
|
495
|
+
* spendable yield (the payment path, via RelayerYieldRealizer).
|
|
496
|
+
*/
|
|
497
|
+
async withdraw(input) {
|
|
498
|
+
const prepared = await this.postJson(
|
|
499
|
+
"prepare",
|
|
500
|
+
"/v1/withdrawals/prepare",
|
|
501
|
+
{
|
|
502
|
+
wallet: this.signer.walletAddress,
|
|
503
|
+
vault: this.vault.address,
|
|
504
|
+
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
505
|
+
...input.purpose === void 0 ? {} : { purpose: input.purpose },
|
|
506
|
+
...input.payment === void 0 ? {} : { payment: input.payment },
|
|
507
|
+
...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
|
|
508
|
+
}
|
|
221
509
|
);
|
|
510
|
+
if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.requestedWithdrawRawUsdc !== input.amountRawUsdc.toString() || prepared.purpose !== (input.purpose ?? "normal") || input.purpose === "yield_realize" && prepared.signingIntent.allowFullExit) {
|
|
511
|
+
throw new VaultFlowClientError("prepare", "Prepared withdrawal differs from the requested operation");
|
|
512
|
+
}
|
|
513
|
+
await assertWithdrawalPreview({
|
|
514
|
+
rpc: this.rpc,
|
|
515
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
516
|
+
wallet: this.signer.walletAddress,
|
|
517
|
+
vault: this.vault,
|
|
518
|
+
amountRawUsdc: input.amountRawUsdc
|
|
519
|
+
});
|
|
520
|
+
const signed = await this.signer.signWithdrawal({
|
|
521
|
+
intent: prepared.signingIntent,
|
|
522
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
523
|
+
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
524
|
+
});
|
|
525
|
+
let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
|
|
526
|
+
withdrawalId: prepared.withdrawalId,
|
|
527
|
+
serializedTransaction: signed.serializedTransaction,
|
|
528
|
+
agentSignature: signed.agentSignature
|
|
529
|
+
});
|
|
530
|
+
if (outcome.status === "submitted") {
|
|
531
|
+
outcome = await this.pollUntilTerminal(
|
|
532
|
+
`/v1/withdrawals/${prepared.withdrawalId}`,
|
|
533
|
+
outcome
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
withdrawalId: prepared.withdrawalId,
|
|
538
|
+
status: outcome.status,
|
|
539
|
+
txSignature: outcome.txSignature ?? null,
|
|
540
|
+
destinationUsdcAta: prepared.destinationUsdcAta,
|
|
541
|
+
actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
|
|
542
|
+
actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
|
|
543
|
+
errorCode: outcome.errorCode ?? null
|
|
544
|
+
};
|
|
222
545
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
546
|
+
/**
|
|
547
|
+
* Reads the yield budget. Syncs the relayer's ledger from chain first (so
|
|
548
|
+
* yield accrued since the last sync shows up); the sync is best-effort and
|
|
549
|
+
* on failure the last-synced view is returned.
|
|
550
|
+
*/
|
|
551
|
+
async getBudget(options = {}) {
|
|
552
|
+
if (options.refreshFromChain !== false) {
|
|
553
|
+
try {
|
|
554
|
+
await this.postJson(
|
|
555
|
+
"sync",
|
|
556
|
+
`/v1/wallets/${this.signer.walletAddress}/sync`,
|
|
557
|
+
{ source: "chain", vault: this.vault.address }
|
|
558
|
+
);
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget?vault=${this.vault.address}`;
|
|
563
|
+
const response = await this.fetchImpl(url, {
|
|
564
|
+
headers: await walletAuthHeaders({
|
|
565
|
+
signer: this.signer,
|
|
566
|
+
method: "GET",
|
|
567
|
+
url
|
|
568
|
+
})
|
|
569
|
+
});
|
|
570
|
+
const text = await response.text();
|
|
571
|
+
if (response.status !== 200) {
|
|
572
|
+
throw new VaultFlowClientError(
|
|
573
|
+
"budget",
|
|
574
|
+
`budget endpoint returned ${response.status}: ${text}`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
let parsed;
|
|
578
|
+
try {
|
|
579
|
+
parsed = JSON.parse(text);
|
|
580
|
+
} catch {
|
|
581
|
+
throw new VaultFlowClientError(
|
|
582
|
+
"budget",
|
|
583
|
+
"budget endpoint returned 200 with a non-JSON body",
|
|
584
|
+
text
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
const body = parsed;
|
|
588
|
+
if (body.position?.vault !== void 0 && body.position.vault !== this.vault.address) {
|
|
589
|
+
throw new VaultFlowClientError("budget", "Relayer returned the budget for a different vault");
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
wallet: this.signer.walletAddress,
|
|
593
|
+
vault: this.vault.address,
|
|
594
|
+
principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
|
|
595
|
+
positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
|
|
596
|
+
grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
|
|
597
|
+
spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
|
|
598
|
+
};
|
|
231
599
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
600
|
+
/** Best-effort audit link: reports the x402 payment tx a realize funded. */
|
|
601
|
+
async reportPayment(input) {
|
|
602
|
+
await this.postJson("submit", "/v1/payments/report", {
|
|
603
|
+
wallet: this.signer.walletAddress,
|
|
604
|
+
withdrawalId: input.withdrawalId,
|
|
605
|
+
paymentTxSignature: input.paymentTxSignature
|
|
606
|
+
});
|
|
236
607
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
function formatRawUsdcAmount(raw) {
|
|
244
|
-
const value = BigInt(raw);
|
|
245
|
-
const negative = value < 0n;
|
|
246
|
-
const abs = negative ? -value : value;
|
|
247
|
-
const whole = abs / 1000000n;
|
|
248
|
-
const frac = (abs % 1000000n).toString().padStart(6, "0");
|
|
249
|
-
return `${negative ? "-" : ""}${whole}.${frac}`;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// ../../src/lib/canonical-json.ts
|
|
253
|
-
import { createHash as createHash3 } from "node:crypto";
|
|
254
|
-
function sha256HexOf(data) {
|
|
255
|
-
return createHash3("sha256").update(data, "utf8").digest("hex");
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ../../src/x402/standard-requirements.ts
|
|
259
|
-
import { z as z2 } from "zod";
|
|
260
|
-
var STANDARD_EXACT_SCHEME = "exact";
|
|
261
|
-
var standardExactRequirementSchema = z2.object({
|
|
262
|
-
scheme: z2.literal(STANDARD_EXACT_SCHEME),
|
|
263
|
-
/** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
|
|
264
|
-
network: z2.string().min(1),
|
|
265
|
-
/** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
|
|
266
|
-
asset: z2.string().min(1),
|
|
267
|
-
/** Exact price in the asset's atomic units, as a decimal string. */
|
|
268
|
-
amount: z2.string().regex(/^[1-9]\d*$/),
|
|
269
|
-
/** Recipient wallet; the transfer destination ATA is derived from it. */
|
|
270
|
-
payTo: z2.string().min(1),
|
|
271
|
-
maxTimeoutSeconds: z2.number().int().positive().optional(),
|
|
272
|
-
extra: z2.object({
|
|
273
|
-
/** Facilitator address that pays the tx fee (gas sponsorship). */
|
|
274
|
-
feePayer: z2.string().min(1).optional()
|
|
275
|
-
}).loose().optional()
|
|
276
|
-
}).loose();
|
|
277
|
-
var standardPaymentRequiredSchema = z2.object({
|
|
278
|
-
x402Version: z2.number().int(),
|
|
279
|
-
accepts: z2.array(z2.unknown()),
|
|
280
|
-
error: z2.string().optional(),
|
|
281
|
-
resource: z2.object({ url: z2.string().optional() }).loose().optional()
|
|
282
|
-
}).loose();
|
|
283
|
-
var StandardX402ChallengeError = class extends Error {
|
|
284
|
-
reason;
|
|
285
|
-
constructor(reason, message) {
|
|
286
|
-
super(message);
|
|
287
|
-
this.name = "StandardX402ChallengeError";
|
|
288
|
-
this.reason = reason;
|
|
608
|
+
/** Wallet's approvals as the relayer sees them (optionally by status). */
|
|
609
|
+
async listApprovals(status) {
|
|
610
|
+
const body = await this.getJson(
|
|
611
|
+
`/v1/wallets/${this.signer.walletAddress}/approvals${`?vault=${this.vault.address}${status === void 0 ? "" : `&status=${encodeURIComponent(status)}`}`}`
|
|
612
|
+
);
|
|
613
|
+
return body.approvals ?? [];
|
|
289
614
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
"
|
|
615
|
+
/**
|
|
616
|
+
* Creates the owner-onboarding setup link (wallet-auth pins the agreed
|
|
617
|
+
* policy + initial deposit). Paste `setupUrl` into the chat verbatim.
|
|
618
|
+
*/
|
|
619
|
+
async createSetupSession(input) {
|
|
620
|
+
const session = await this.postJson(
|
|
621
|
+
"prepare",
|
|
622
|
+
`/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
|
|
623
|
+
{
|
|
624
|
+
vault: this.vault.address,
|
|
625
|
+
...input.policy === void 0 ? {} : { policy: input.policy },
|
|
626
|
+
...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
|
|
627
|
+
...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
|
|
628
|
+
...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
|
|
629
|
+
}
|
|
297
630
|
);
|
|
631
|
+
if (session.vault !== this.vault.address || session.wallet !== this.signer.walletAddress) {
|
|
632
|
+
throw new VaultFlowClientError("prepare", "Relayer returned a setup session for a different wallet or vault");
|
|
633
|
+
}
|
|
634
|
+
return session;
|
|
298
635
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
636
|
+
/** Polls a setup session (public capability URL — no auth needed). */
|
|
637
|
+
async getSetupSession(sessionId) {
|
|
638
|
+
const url = `${this.baseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`;
|
|
639
|
+
const response = await this.fetchImpl(url);
|
|
640
|
+
const text = await response.text();
|
|
641
|
+
if (response.status !== 200) {
|
|
642
|
+
const parsed = parseRelayerError(text);
|
|
643
|
+
throw new VaultFlowClientError(
|
|
644
|
+
"read",
|
|
645
|
+
parsed.message ?? `setup session read failed with ${response.status}`,
|
|
646
|
+
text,
|
|
647
|
+
parsed.code,
|
|
648
|
+
parsed.details
|
|
649
|
+
);
|
|
303
650
|
}
|
|
304
|
-
return
|
|
305
|
-
});
|
|
306
|
-
return { paymentRequired: parsed.data, solanaExactRequirements };
|
|
307
|
-
}
|
|
308
|
-
function decodeStandardPaymentRequiredHeader(headerValue) {
|
|
309
|
-
let decoded;
|
|
310
|
-
try {
|
|
311
|
-
decoded = decodeX402Header(headerValue);
|
|
312
|
-
} catch (error) {
|
|
313
|
-
throw new StandardX402ChallengeError(
|
|
314
|
-
error instanceof X402HeaderError ? error.reason : "invalid_header",
|
|
315
|
-
"Cannot decode the payment-required header"
|
|
316
|
-
);
|
|
651
|
+
return JSON.parse(text);
|
|
317
652
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
653
|
+
/**
|
|
654
|
+
* Finds an APPROVED, unconsumed deposit approval bound to exactly this
|
|
655
|
+
* amount — the shape the mandate's initialDeposit approval has.
|
|
656
|
+
*/
|
|
657
|
+
async findApprovedDepositApproval(amountRawUsdc) {
|
|
658
|
+
try {
|
|
659
|
+
const approvals = await this.listApprovals("approved");
|
|
660
|
+
const match = approvals.find((approval) => {
|
|
661
|
+
const binding = approval.binding;
|
|
662
|
+
return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc.toString();
|
|
663
|
+
});
|
|
664
|
+
return match?.approvalId;
|
|
665
|
+
} catch {
|
|
666
|
+
return void 0;
|
|
667
|
+
}
|
|
331
668
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
)
|
|
669
|
+
/**
|
|
670
|
+
* Polls the reconciling GET endpoint until the intent leaves "submitted"
|
|
671
|
+
* (each read looks the tx up on-chain) or the timeout elapses.
|
|
672
|
+
*/
|
|
673
|
+
async pollUntilTerminal(path, last) {
|
|
674
|
+
const deadline = Date.now() + this.pollTimeoutMs;
|
|
675
|
+
let latest = last;
|
|
676
|
+
while (Date.now() < deadline) {
|
|
677
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
678
|
+
const url = `${this.baseUrl}${path}`;
|
|
679
|
+
const response = await this.fetchImpl(url, {
|
|
680
|
+
headers: await walletAuthHeaders({
|
|
681
|
+
signer: this.signer,
|
|
682
|
+
method: "GET",
|
|
683
|
+
url
|
|
684
|
+
})
|
|
685
|
+
});
|
|
686
|
+
if (response.status !== 200) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
latest = await response.json();
|
|
691
|
+
} catch {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (latest.status !== "submitted") {
|
|
695
|
+
return latest;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return latest;
|
|
340
699
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
"
|
|
346
|
-
|
|
700
|
+
async postJson(step, path, body) {
|
|
701
|
+
const url = `${this.baseUrl}${path}`;
|
|
702
|
+
const serialized = JSON.stringify(body);
|
|
703
|
+
const response = await this.fetchImpl(url, {
|
|
704
|
+
method: "POST",
|
|
705
|
+
headers: {
|
|
706
|
+
...await walletAuthHeaders({
|
|
707
|
+
signer: this.signer,
|
|
708
|
+
method: "POST",
|
|
709
|
+
url,
|
|
710
|
+
body: serialized
|
|
711
|
+
}),
|
|
712
|
+
"content-type": "application/json"
|
|
713
|
+
},
|
|
714
|
+
body: serialized
|
|
715
|
+
});
|
|
716
|
+
const text = await response.text();
|
|
717
|
+
if (response.status !== 200) {
|
|
718
|
+
const parsed = parseRelayerError(text);
|
|
719
|
+
throw new VaultFlowClientError(
|
|
720
|
+
step,
|
|
721
|
+
parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
|
|
722
|
+
text,
|
|
723
|
+
parsed.code,
|
|
724
|
+
parsed.details
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
return JSON.parse(text);
|
|
729
|
+
} catch {
|
|
730
|
+
throw new VaultFlowClientError(
|
|
731
|
+
step,
|
|
732
|
+
`${path} returned 200 with a non-JSON body`,
|
|
733
|
+
text
|
|
734
|
+
);
|
|
735
|
+
}
|
|
347
736
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
737
|
+
async getJson(path) {
|
|
738
|
+
const url = `${this.baseUrl}${path}`;
|
|
739
|
+
const response = await this.fetchImpl(url, {
|
|
740
|
+
headers: await walletAuthHeaders({
|
|
741
|
+
signer: this.signer,
|
|
742
|
+
method: "GET",
|
|
743
|
+
url
|
|
744
|
+
})
|
|
745
|
+
});
|
|
746
|
+
const text = await response.text();
|
|
747
|
+
if (response.status !== 200) {
|
|
748
|
+
const parsed = parseRelayerError(text);
|
|
749
|
+
throw new VaultFlowClientError(
|
|
750
|
+
"read",
|
|
751
|
+
parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
|
|
752
|
+
text,
|
|
753
|
+
parsed.code,
|
|
754
|
+
parsed.details
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
try {
|
|
758
|
+
return JSON.parse(text);
|
|
759
|
+
} catch {
|
|
760
|
+
throw new VaultFlowClientError(
|
|
761
|
+
"read",
|
|
762
|
+
`${path} returned 200 with a non-JSON body`,
|
|
763
|
+
text
|
|
764
|
+
);
|
|
765
|
+
}
|
|
365
766
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
);
|
|
767
|
+
};
|
|
768
|
+
function parseRelayerError(text) {
|
|
769
|
+
try {
|
|
770
|
+
const parsed = JSON.parse(text);
|
|
771
|
+
return {
|
|
772
|
+
code: typeof parsed.error?.code === "string" ? parsed.error.code : null,
|
|
773
|
+
message: typeof parsed.error?.message === "string" ? parsed.error.message : null,
|
|
774
|
+
details: parsed.error?.details ?? null
|
|
775
|
+
};
|
|
776
|
+
} catch {
|
|
777
|
+
return { code: null, message: null, details: null };
|
|
370
778
|
}
|
|
371
|
-
return value;
|
|
372
779
|
}
|
|
373
780
|
|
|
374
|
-
// ../../src/client/
|
|
375
|
-
var
|
|
376
|
-
|
|
781
|
+
// ../../src/client/relayer-yield-realizer.ts
|
|
782
|
+
var REALIZE_OVERHEAD_RAW_USDC = 2500n;
|
|
783
|
+
var RelayerRealizeError = class extends Error {
|
|
784
|
+
constructor(code, message, detail = null) {
|
|
377
785
|
super(message);
|
|
378
|
-
this.
|
|
786
|
+
this.code = code;
|
|
379
787
|
this.detail = detail;
|
|
380
|
-
this.name = "
|
|
788
|
+
this.name = "RelayerRealizeError";
|
|
381
789
|
}
|
|
382
|
-
|
|
790
|
+
code;
|
|
383
791
|
detail;
|
|
384
792
|
};
|
|
385
|
-
var
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
probeFetch;
|
|
389
|
-
defaultMaxAmountRawUsdc;
|
|
390
|
-
network;
|
|
391
|
-
usdcMint;
|
|
392
|
-
stateStore;
|
|
393
|
-
pending = /* @__PURE__ */ new Map();
|
|
394
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
395
|
-
nowMs;
|
|
396
|
-
constructor(config) {
|
|
397
|
-
this.realizer = config.realizer;
|
|
398
|
-
this.x402Fetch = config.x402Fetch;
|
|
399
|
-
this.probeFetch = config.probeFetch ?? fetch;
|
|
400
|
-
this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
|
|
401
|
-
this.network = config.network ?? SOLANA_MAINNET_NETWORK;
|
|
402
|
-
this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
|
|
403
|
-
this.stateStore = config.stateStore ?? null;
|
|
404
|
-
this.nowMs = config.nowMs ?? (() => Date.now());
|
|
405
|
-
if (this.stateStore !== null) {
|
|
406
|
-
for (const record of this.stateStore.load()) {
|
|
407
|
-
this.pending.set(record.key, record);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
793
|
+
var RelayerYieldRealizer = class {
|
|
794
|
+
get vault() {
|
|
795
|
+
return this.vaultFlows.vault.address;
|
|
410
796
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const existingFlow = this.inFlight.get(pendingKey);
|
|
420
|
-
if (existingFlow !== void 0) {
|
|
421
|
-
return existingFlow;
|
|
422
|
-
}
|
|
423
|
-
const flow = this.run(input, {
|
|
424
|
-
method,
|
|
425
|
-
requestBodyHash,
|
|
426
|
-
pendingKey
|
|
427
|
-
}).finally(() => {
|
|
428
|
-
this.inFlight.delete(pendingKey);
|
|
797
|
+
vaultFlows;
|
|
798
|
+
constructor(config) {
|
|
799
|
+
this.vaultFlows = new VaultFlowClient({
|
|
800
|
+
relayerBaseUrl: config.relayerBaseUrl,
|
|
801
|
+
signer: config.signer,
|
|
802
|
+
rpc: config.rpc,
|
|
803
|
+
...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
|
|
804
|
+
...config.lookupTablesFor === void 0 ? {} : { lookupTablesFor: config.lookupTablesFor }
|
|
429
805
|
});
|
|
430
|
-
this.inFlight.set(pendingKey, flow);
|
|
431
|
-
return flow;
|
|
432
806
|
}
|
|
433
|
-
async
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
throw new StandardX402PayError(
|
|
438
|
-
"payment_outcome_unknown",
|
|
439
|
-
"a previous external x402 payment for this request has an unknown outcome. Verify whether it settled before purchasing again; to pay again anyway, call with forceNewPayment=true.",
|
|
440
|
-
existingPending
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
if (existingPending !== void 0 && input.forceNewPayment === true) {
|
|
444
|
-
try {
|
|
445
|
-
this.untrack(pendingKey);
|
|
446
|
-
} catch (error) {
|
|
447
|
-
throw new StandardX402PayError(
|
|
448
|
-
"state_persist_failed",
|
|
449
|
-
"could not clear the previous pending x402 marker before forcing a new payment",
|
|
450
|
-
error
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
const init = {
|
|
455
|
-
method,
|
|
456
|
-
...input.body === void 0 ? {} : { body: input.body },
|
|
457
|
-
...input.headers === void 0 ? {} : { headers: input.headers }
|
|
458
|
-
};
|
|
459
|
-
const probe = await this.probeFetch(input.url, init);
|
|
460
|
-
if (probe.status !== 402) {
|
|
461
|
-
return { paid: false, status: probe.status, body: await probe.text() };
|
|
462
|
-
}
|
|
463
|
-
const selected = await this.selectRequirement(probe);
|
|
464
|
-
const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
|
|
465
|
-
if (selected.amountRawUsdc > cap) {
|
|
466
|
-
throw new StandardX402PayError(
|
|
467
|
-
"amount_exceeds_client_cap",
|
|
468
|
-
`the challenge demands ${selected.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
|
|
469
|
-
{ amountRawUsdc: selected.amountRawUsdc.toString(), payTo: selected.payTo }
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
let realized;
|
|
807
|
+
async ensureUsdcAvailable(input) {
|
|
808
|
+
const shortfallRawUsdc = input.amountRawUsdc;
|
|
809
|
+
await this.assertSpendableYield(shortfallRawUsdc);
|
|
810
|
+
let outcome;
|
|
473
811
|
try {
|
|
474
|
-
|
|
475
|
-
amountRawUsdc:
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
},
|
|
812
|
+
outcome = await this.vaultFlows.withdraw({
|
|
813
|
+
amountRawUsdc: shortfallRawUsdc,
|
|
814
|
+
// The relayer refuses to prepare this withdrawal beyond the spendable
|
|
815
|
+
// yield — the principal-protection guard the client cannot bypass.
|
|
816
|
+
purpose: "yield_realize",
|
|
817
|
+
// Declares what is being paid so the relayer's spending-mandate layer
|
|
818
|
+
// can enforce caps/payee and keep the mandate → payment audit chain.
|
|
819
|
+
...input.payment === void 0 ? {} : { payment: input.payment },
|
|
482
820
|
...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
|
|
483
821
|
});
|
|
484
822
|
} catch (error) {
|
|
485
|
-
|
|
486
|
-
throw new StandardX402PayError(
|
|
487
|
-
"approval_required",
|
|
488
|
-
"this payment exceeds the owner-approval threshold; NOTHING was paid. Ask the owner to open the approveUrl, then retry the same call with the approvalId",
|
|
489
|
-
error.detail ?? null
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
throw new StandardX402PayError(
|
|
493
|
-
"realize_failed",
|
|
494
|
-
`could not realize yield to cover ${selected.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
|
|
495
|
-
error
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
const pendingRecord = {
|
|
499
|
-
key: pendingKey,
|
|
500
|
-
url: input.url,
|
|
501
|
-
method,
|
|
502
|
-
requestBodyHash,
|
|
503
|
-
amountRawUsdc: selected.amountRawUsdc.toString(),
|
|
504
|
-
payTo: selected.payTo,
|
|
505
|
-
feePayer: selected.feePayer,
|
|
506
|
-
realizedRawUsdc: realized.realizedRawUsdc.toString(),
|
|
507
|
-
realizeTxSignature: realized.txSignature,
|
|
508
|
-
status: "realized",
|
|
509
|
-
createdAtMs: this.nowMs(),
|
|
510
|
-
updatedAtMs: this.nowMs()
|
|
511
|
-
};
|
|
512
|
-
try {
|
|
513
|
-
this.track(pendingRecord);
|
|
514
|
-
} catch (error) {
|
|
515
|
-
throw new StandardX402PayError(
|
|
516
|
-
"state_persist_failed",
|
|
517
|
-
"could not persist the pending x402 marker; refusing to attempt the external payment because a restart would not be double-payment safe",
|
|
518
|
-
{ error, pendingPayment: pendingRecord }
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
let response;
|
|
522
|
-
try {
|
|
523
|
-
response = await this.x402Fetch(input.url, init, selected);
|
|
524
|
-
} catch (error) {
|
|
525
|
-
const persistError = this.tryMarkUnknown(pendingKey, {
|
|
526
|
-
message: error instanceof Error ? error.message : String(error)
|
|
527
|
-
});
|
|
528
|
-
throw new StandardX402PayError(
|
|
529
|
-
"payment_outcome_unknown",
|
|
530
|
-
`the x402 payment attempt failed after yield was realized; verify whether it settled before paying again: ${error instanceof Error ? error.message : String(error)}`,
|
|
531
|
-
{ error, persistError }
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
const bodyText = await response.text();
|
|
535
|
-
if (response.status !== 200) {
|
|
536
|
-
const persistError = this.tryMarkUnknown(pendingKey, {
|
|
537
|
-
status: response.status,
|
|
538
|
-
body: bodyText
|
|
539
|
-
});
|
|
540
|
-
throw new StandardX402PayError(
|
|
541
|
-
"payment_outcome_unknown",
|
|
542
|
-
`the x402 payment attempt returned ${response.status} after yield was realized; verify whether it settled before paying again`,
|
|
543
|
-
{ status: response.status, body: bodyText, persistError }
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
|
-
this.clearDelivered(pendingKey);
|
|
547
|
-
const paymentTxSignature = extractSettledPaymentTxSignature(response);
|
|
548
|
-
if (paymentTxSignature !== null && typeof realized.withdrawalId === "string" && this.realizer.reportPayment !== void 0) {
|
|
549
|
-
try {
|
|
550
|
-
await this.realizer.reportPayment({
|
|
551
|
-
withdrawalId: realized.withdrawalId,
|
|
552
|
-
paymentTxSignature
|
|
553
|
-
});
|
|
554
|
-
} catch (error) {
|
|
555
|
-
console.error(
|
|
556
|
-
`[subly-x402] payment report-back failed (audit only, payment ok): ${error instanceof Error ? error.message : String(error)}`
|
|
557
|
-
);
|
|
558
|
-
}
|
|
823
|
+
throw this.mapWithdrawError(error);
|
|
559
824
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
825
|
+
if (outcome.status !== "confirmed" || outcome.txSignature === null) {
|
|
826
|
+
throw new RelayerRealizeError(
|
|
827
|
+
"realize_not_confirmed",
|
|
828
|
+
`yield realize withdrawal did not confirm (status=${outcome.status})`,
|
|
829
|
+
outcome
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
return {
|
|
833
|
+
realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
|
|
834
|
+
txSignature: outcome.txSignature,
|
|
835
|
+
withdrawalId: outcome.withdrawalId
|
|
572
836
|
};
|
|
573
837
|
}
|
|
574
|
-
/**
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
838
|
+
/**
|
|
839
|
+
* Best-effort report-back of the x402 payment tx this realize funded —
|
|
840
|
+
* closes the relayer's mandate → realize → payment audit chain. Callers
|
|
841
|
+
* must never let a failure here affect the payment result.
|
|
842
|
+
*/
|
|
843
|
+
async reportPayment(input) {
|
|
844
|
+
await this.vaultFlows.reportPayment(input);
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Refuses to realize more than the ledger's spendable yield (principal).
|
|
848
|
+
* getBudget syncs the relayer's ledger from chain first (best-effort), so a
|
|
849
|
+
* long-running client sees yield as it accrues instead of a frozen view.
|
|
850
|
+
*/
|
|
851
|
+
async assertSpendableYield(shortfallRawUsdc) {
|
|
852
|
+
let spendable;
|
|
578
853
|
try {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
} else {
|
|
582
|
-
requirements = parseStandardChallenge(
|
|
583
|
-
await probe.json()
|
|
584
|
-
).solanaExactRequirements;
|
|
585
|
-
}
|
|
854
|
+
const budget = await this.vaultFlows.getBudget();
|
|
855
|
+
spendable = BigInt(budget.spendableYieldRawUsdc);
|
|
586
856
|
} catch (error) {
|
|
587
|
-
throw new
|
|
588
|
-
|
|
589
|
-
"could not
|
|
857
|
+
throw new RelayerRealizeError(
|
|
858
|
+
"budget_unavailable",
|
|
859
|
+
"could not read the spendable-yield budget",
|
|
590
860
|
error
|
|
591
861
|
);
|
|
592
862
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
863
|
+
const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
|
|
864
|
+
if (spendable < requiredRawUsdc) {
|
|
865
|
+
throw new RelayerRealizeError(
|
|
866
|
+
"insufficient_yield",
|
|
867
|
+
`spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC plus the ${REALIZE_OVERHEAD_RAW_USDC} raw fee headroom; the principal is never spent \u2014 wait for more yield`,
|
|
868
|
+
{ spendableYieldRawUsdc: spendable.toString() }
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
mapWithdrawError(error) {
|
|
873
|
+
if (!(error instanceof VaultFlowClientError)) {
|
|
874
|
+
return new RelayerRealizeError(
|
|
875
|
+
"prepare_failed",
|
|
876
|
+
`yield realize failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
602
877
|
error
|
|
603
878
|
);
|
|
604
879
|
}
|
|
880
|
+
const serverCode = error.code ?? errorCodeFrom(error.detail);
|
|
881
|
+
if (serverCode === "approval_required") {
|
|
882
|
+
return new RelayerRealizeError(
|
|
883
|
+
"approval_required",
|
|
884
|
+
"this payment exceeds the owner-approval threshold; nothing was realized or paid. Ask the owner to approve, then retry with the approvalId",
|
|
885
|
+
error.errorDetails ?? error.detail
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
|
|
889
|
+
return new RelayerRealizeError(
|
|
890
|
+
"insufficient_yield",
|
|
891
|
+
"the relayer refused to realize beyond the spendable yield; the principal is never spent \u2014 wait for more yield",
|
|
892
|
+
error.detail
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
return new RelayerRealizeError(
|
|
896
|
+
error.step === "submit" ? "submit_failed" : "prepare_failed",
|
|
897
|
+
error.message,
|
|
898
|
+
error.detail
|
|
899
|
+
);
|
|
605
900
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
901
|
+
};
|
|
902
|
+
function errorCodeFrom(detail) {
|
|
903
|
+
if (typeof detail !== "string") {
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
try {
|
|
907
|
+
const parsed = JSON.parse(detail);
|
|
908
|
+
return typeof parsed.error?.code === "string" ? parsed.error.code : null;
|
|
909
|
+
} catch {
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// src/mcp-server.ts
|
|
915
|
+
import { homedir } from "node:os";
|
|
916
|
+
import { join as join2 } from "node:path";
|
|
917
|
+
|
|
918
|
+
// ../../src/client/mcp-payment-server.ts
|
|
919
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
920
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
921
|
+
import {
|
|
922
|
+
CallToolRequestSchema,
|
|
923
|
+
ListToolsRequestSchema
|
|
924
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
925
|
+
|
|
926
|
+
// ../../src/client/onboarding.ts
|
|
927
|
+
var SELF_SERVE_POLICY_ID = "self-serve";
|
|
928
|
+
var OnboardingError = class extends Error {
|
|
929
|
+
constructor(step, message, detail = null) {
|
|
930
|
+
super(message);
|
|
931
|
+
this.step = step;
|
|
932
|
+
this.detail = detail;
|
|
933
|
+
this.name = "OnboardingError";
|
|
934
|
+
}
|
|
935
|
+
step;
|
|
936
|
+
detail;
|
|
937
|
+
};
|
|
938
|
+
async function ensureWalletOnboarded(params) {
|
|
939
|
+
const fetchImpl = params.fetchImpl ?? fetch;
|
|
940
|
+
const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
|
|
941
|
+
const post = async (step, path, body) => {
|
|
942
|
+
const url = `${baseUrl}${path}`;
|
|
943
|
+
const serialized = JSON.stringify(body);
|
|
944
|
+
const response = await fetchImpl(url, {
|
|
945
|
+
method: "POST",
|
|
946
|
+
headers: {
|
|
947
|
+
...await walletAuthHeaders({
|
|
948
|
+
signer: params.signer,
|
|
949
|
+
method: "POST",
|
|
950
|
+
url,
|
|
951
|
+
body: serialized
|
|
952
|
+
}),
|
|
953
|
+
"content-type": "application/json"
|
|
954
|
+
},
|
|
955
|
+
body: serialized
|
|
956
|
+
});
|
|
957
|
+
if (response.status !== 200) {
|
|
958
|
+
let detail = null;
|
|
959
|
+
try {
|
|
960
|
+
detail = await response.json();
|
|
961
|
+
} catch {
|
|
962
|
+
detail = null;
|
|
616
963
|
}
|
|
617
|
-
throw
|
|
964
|
+
throw new OnboardingError(
|
|
965
|
+
step,
|
|
966
|
+
`wallet onboarding ${step} failed with ${response.status}`,
|
|
967
|
+
detail
|
|
968
|
+
);
|
|
618
969
|
}
|
|
970
|
+
};
|
|
971
|
+
const wallet = params.signer.walletAddress;
|
|
972
|
+
const vault = params.vault ?? params.signer.vault?.address ?? SUBLY_VAULT.address;
|
|
973
|
+
await post("register", "/v1/wallets/agent", {
|
|
974
|
+
wallet,
|
|
975
|
+
vault,
|
|
976
|
+
signingPolicyId: SELF_SERVE_POLICY_ID,
|
|
977
|
+
signingMode: "non_interactive",
|
|
978
|
+
signerValidationMode: params.signer.validationMode,
|
|
979
|
+
signerProvider: params.signer.provider ?? "local-keypair",
|
|
980
|
+
activateForPayments: true
|
|
981
|
+
});
|
|
982
|
+
await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain", vault });
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// ../../src/x402/headers.ts
|
|
986
|
+
import { z as z2 } from "zod";
|
|
987
|
+
|
|
988
|
+
// ../../src/lib/hash.ts
|
|
989
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
990
|
+
var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
991
|
+
function sha256TaggedHex(data) {
|
|
992
|
+
return `sha256-${createHash3("sha256").update(data).digest("hex")}`;
|
|
993
|
+
}
|
|
994
|
+
function stableStringify(value) {
|
|
995
|
+
if (value === null) {
|
|
996
|
+
return "null";
|
|
619
997
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
998
|
+
if (typeof value === "bigint") {
|
|
999
|
+
return JSON.stringify(value.toString());
|
|
1000
|
+
}
|
|
1001
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1002
|
+
return JSON.stringify(value);
|
|
1003
|
+
}
|
|
1004
|
+
if (Array.isArray(value)) {
|
|
1005
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
1006
|
+
}
|
|
1007
|
+
const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
|
|
1008
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
1009
|
+
}
|
|
1010
|
+
function hashStableJson(value) {
|
|
1011
|
+
return sha256TaggedHex(stableStringify(value));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// ../../src/x402/headers.ts
|
|
1015
|
+
var PAYMENT_REQUIRED_HEADER = "payment-required";
|
|
1016
|
+
var MAX_HEADER_JSON_BYTES = 16384;
|
|
1017
|
+
var X402HeaderError = class extends Error {
|
|
1018
|
+
reason;
|
|
1019
|
+
constructor(reason, message) {
|
|
1020
|
+
super(message);
|
|
1021
|
+
this.name = "X402HeaderError";
|
|
1022
|
+
this.reason = reason;
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
var sublyPaymentRequirementsSchema = z2.object({
|
|
1026
|
+
scheme: z2.literal(PAYMENT_SCHEME),
|
|
1027
|
+
network: z2.string().min(1),
|
|
1028
|
+
asset: z2.string().min(32),
|
|
1029
|
+
/** Exact seller amount in raw USDC; the scheme settles exactly this. */
|
|
1030
|
+
amountRawUsdc: z2.string().regex(/^[1-9]\d*$/),
|
|
1031
|
+
resource: z2.string().url(),
|
|
1032
|
+
description: z2.string().optional(),
|
|
1033
|
+
mimeType: z2.string().optional(),
|
|
1034
|
+
payTo: z2.string().min(32),
|
|
1035
|
+
maxTimeoutSeconds: z2.number().int().positive(),
|
|
1036
|
+
extra: z2.object({
|
|
1037
|
+
sellerRequestId: z2.string().min(1),
|
|
1038
|
+
seller: z2.string().min(32),
|
|
1039
|
+
sellerUsdcAta: z2.string().min(32),
|
|
1040
|
+
vault: z2.string().min(32),
|
|
1041
|
+
shareMint: z2.string().min(32)
|
|
1042
|
+
})
|
|
1043
|
+
}).loose();
|
|
1044
|
+
var paymentRequiredSchema = z2.object({
|
|
1045
|
+
x402Version: z2.number().int(),
|
|
1046
|
+
accepts: z2.array(z2.unknown()),
|
|
1047
|
+
error: z2.string().optional()
|
|
1048
|
+
}).loose();
|
|
1049
|
+
var sublyPaymentPayloadSchema = z2.object({
|
|
1050
|
+
x402Version: z2.number().int(),
|
|
1051
|
+
scheme: z2.literal(PAYMENT_SCHEME),
|
|
1052
|
+
network: z2.string().min(1),
|
|
1053
|
+
payload: z2.object({
|
|
1054
|
+
paymentId: z2.string().min(1),
|
|
1055
|
+
requestBindingHash: z2.string().min(1),
|
|
1056
|
+
preparedMessageHash: z2.string().min(1),
|
|
1057
|
+
serializedTransaction: z2.string().min(1).max(4096),
|
|
1058
|
+
agentSignature: z2.string().min(1).max(128),
|
|
1059
|
+
temporarySettlementSignature: z2.string().min(1).max(128)
|
|
1060
|
+
})
|
|
1061
|
+
}).loose();
|
|
1062
|
+
function decodeX402Header(headerValue) {
|
|
1063
|
+
if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
|
|
1064
|
+
throw new X402HeaderError(
|
|
1065
|
+
"header_too_large",
|
|
1066
|
+
`x402 header exceeds ${MAX_HEADER_JSON_BYTES} encoded bytes`
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
const json = Buffer.from(headerValue, "base64").toString("utf8");
|
|
1070
|
+
try {
|
|
1071
|
+
return JSON.parse(json);
|
|
1072
|
+
} catch {
|
|
1073
|
+
throw new X402HeaderError(
|
|
1074
|
+
"invalid_header_encoding",
|
|
1075
|
+
"x402 header is not base64-encoded JSON"
|
|
1076
|
+
);
|
|
638
1077
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
} catch (error) {
|
|
644
|
-
return error;
|
|
645
|
-
}
|
|
1078
|
+
}
|
|
1079
|
+
function requestBodyHashFor(body) {
|
|
1080
|
+
if (body === null || body === void 0 || body.length === 0) {
|
|
1081
|
+
return EMPTY_BODY_HASH;
|
|
646
1082
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1083
|
+
return sha256TaggedHex(
|
|
1084
|
+
typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body)
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// ../../src/client/paid-fetch.ts
|
|
1089
|
+
function formatRawUsdcAmount(raw) {
|
|
1090
|
+
const value = BigInt(raw);
|
|
1091
|
+
const negative = value < 0n;
|
|
1092
|
+
const abs = negative ? -value : value;
|
|
1093
|
+
const whole = abs / 1000000n;
|
|
1094
|
+
const frac = (abs % 1000000n).toString().padStart(6, "0");
|
|
1095
|
+
return `${negative ? "-" : ""}${whole}.${frac}`;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// ../../src/lib/canonical-json.ts
|
|
1099
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1100
|
+
function sha256HexOf(data) {
|
|
1101
|
+
return createHash4("sha256").update(data, "utf8").digest("hex");
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// ../../src/x402/standard-requirements.ts
|
|
1105
|
+
import { z as z3 } from "zod";
|
|
1106
|
+
var STANDARD_EXACT_SCHEME = "exact";
|
|
1107
|
+
var standardExactRequirementSchema = z3.object({
|
|
1108
|
+
scheme: z3.literal(STANDARD_EXACT_SCHEME),
|
|
1109
|
+
/** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
|
|
1110
|
+
network: z3.string().min(1),
|
|
1111
|
+
/** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
|
|
1112
|
+
asset: z3.string().min(1),
|
|
1113
|
+
/** Exact price in the asset's atomic units, as a decimal string. */
|
|
1114
|
+
amount: z3.string().regex(/^[1-9]\d*$/),
|
|
1115
|
+
/** Recipient wallet; the transfer destination ATA is derived from it. */
|
|
1116
|
+
payTo: z3.string().min(1),
|
|
1117
|
+
maxTimeoutSeconds: z3.number().int().positive().optional(),
|
|
1118
|
+
extra: z3.object({
|
|
1119
|
+
/** Facilitator address that pays the tx fee (gas sponsorship). */
|
|
1120
|
+
feePayer: z3.string().min(1).optional()
|
|
1121
|
+
}).loose().optional()
|
|
1122
|
+
}).loose();
|
|
1123
|
+
var standardPaymentRequiredSchema = z3.object({
|
|
1124
|
+
x402Version: z3.number().int(),
|
|
1125
|
+
accepts: z3.array(z3.unknown()),
|
|
1126
|
+
error: z3.string().optional(),
|
|
1127
|
+
resource: z3.object({ url: z3.string().optional() }).loose().optional()
|
|
1128
|
+
}).loose();
|
|
1129
|
+
var StandardX402ChallengeError = class extends Error {
|
|
1130
|
+
reason;
|
|
1131
|
+
constructor(reason, message) {
|
|
1132
|
+
super(message);
|
|
1133
|
+
this.name = "StandardX402ChallengeError";
|
|
1134
|
+
this.reason = reason;
|
|
659
1135
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
}
|
|
669
|
-
console.error(
|
|
670
|
-
`[subly-x402] payment delivered but pending marker could not be cleared: ${error instanceof Error ? error.message : String(error)}`
|
|
671
|
-
);
|
|
672
|
-
}
|
|
1136
|
+
};
|
|
1137
|
+
function parseStandardChallenge(challenge) {
|
|
1138
|
+
const parsed = standardPaymentRequiredSchema.safeParse(challenge);
|
|
1139
|
+
if (!parsed.success) {
|
|
1140
|
+
throw new StandardX402ChallengeError(
|
|
1141
|
+
"invalid_payment_required",
|
|
1142
|
+
"Response is not a valid x402 PaymentRequired object"
|
|
1143
|
+
);
|
|
673
1144
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
1145
|
+
const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
|
|
1146
|
+
const requirement = standardExactRequirementSchema.safeParse(candidate);
|
|
1147
|
+
if (!requirement.success) {
|
|
1148
|
+
return [];
|
|
677
1149
|
}
|
|
678
|
-
|
|
679
|
-
}
|
|
680
|
-
};
|
|
681
|
-
function pendingPaymentKey(input) {
|
|
682
|
-
return `${input.method}:${input.url}:${input.requestBodyHash}`;
|
|
1150
|
+
return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
|
|
1151
|
+
});
|
|
1152
|
+
return { paymentRequired: parsed.data, solanaExactRequirements };
|
|
683
1153
|
}
|
|
684
|
-
function
|
|
685
|
-
|
|
686
|
-
if (header === null || header.length === 0) {
|
|
687
|
-
return null;
|
|
688
|
-
}
|
|
1154
|
+
function decodeStandardPaymentRequiredHeader(headerValue) {
|
|
1155
|
+
let decoded;
|
|
689
1156
|
try {
|
|
690
|
-
|
|
691
|
-
|
|
1157
|
+
decoded = decodeX402Header(headerValue);
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
throw new StandardX402ChallengeError(
|
|
1160
|
+
error instanceof X402HeaderError ? error.reason : "invalid_header",
|
|
1161
|
+
"Cannot decode the payment-required header"
|
|
692
1162
|
);
|
|
693
|
-
if (typeof decoded.transaction === "string" && decoded.transaction.length > 0) {
|
|
694
|
-
return decoded.transaction;
|
|
695
|
-
}
|
|
696
|
-
if (typeof decoded.txHash === "string" && decoded.txHash.length > 0) {
|
|
697
|
-
return decoded.txHash;
|
|
698
|
-
}
|
|
699
|
-
return null;
|
|
700
|
-
} catch {
|
|
701
|
-
return null;
|
|
702
1163
|
}
|
|
1164
|
+
return parseStandardChallenge(decoded);
|
|
703
1165
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
signatureCount |= (byte & 127) << shift;
|
|
716
|
-
offset += 1;
|
|
717
|
-
if ((byte & 128) === 0) {
|
|
718
|
-
break;
|
|
719
|
-
}
|
|
720
|
-
shift += 7;
|
|
1166
|
+
function selectPayableSolanaRequirement(requirements, options) {
|
|
1167
|
+
const network = options?.network ?? SOLANA_MAINNET_NETWORK;
|
|
1168
|
+
const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
|
|
1169
|
+
const matchingRequirements = requirements.filter(
|
|
1170
|
+
(candidate) => candidate.network === network && candidate.asset === usdcMint
|
|
1171
|
+
);
|
|
1172
|
+
if (matchingRequirements.length === 0) {
|
|
1173
|
+
throw new StandardX402ChallengeError(
|
|
1174
|
+
"no_payable_requirement",
|
|
1175
|
+
`The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
|
|
1176
|
+
);
|
|
721
1177
|
}
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1178
|
+
const requirement = matchingRequirements.find(
|
|
1179
|
+
(candidate) => candidate.extra?.feePayer !== void 0
|
|
1180
|
+
) ?? null;
|
|
1181
|
+
if (requirement === null) {
|
|
1182
|
+
throw new StandardX402ChallengeError(
|
|
1183
|
+
"missing_svm_fee_payer",
|
|
1184
|
+
"The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
const feePayer = requirement.extra?.feePayer;
|
|
1188
|
+
if (feePayer === void 0) {
|
|
1189
|
+
throw new StandardX402ChallengeError(
|
|
1190
|
+
"missing_svm_fee_payer",
|
|
1191
|
+
"The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
return {
|
|
1195
|
+
requirement,
|
|
1196
|
+
amountRawUsdc: BigInt(requirement.amount),
|
|
1197
|
+
payTo: requirement.payTo,
|
|
1198
|
+
feePayer
|
|
1199
|
+
};
|
|
726
1200
|
}
|
|
727
|
-
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
1201
|
+
function standardRequirementMatchesSelected(candidate, selected2) {
|
|
1202
|
+
const parsed = standardExactRequirementSchema.safeParse(candidate);
|
|
1203
|
+
return parsed.success && stableJson(parsed.data) === stableJson(selected2.requirement);
|
|
1204
|
+
}
|
|
1205
|
+
function stableJson(value) {
|
|
1206
|
+
return JSON.stringify(sortJson(value));
|
|
1207
|
+
}
|
|
1208
|
+
function sortJson(value) {
|
|
1209
|
+
if (Array.isArray(value)) {
|
|
1210
|
+
return value.map(sortJson);
|
|
731
1211
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const result = {};
|
|
737
|
-
for (const table of tables) {
|
|
738
|
-
if (table.exists) {
|
|
739
|
-
result[table.address] = table.data.addresses.map(String);
|
|
740
|
-
}
|
|
1212
|
+
if (value !== null && typeof value === "object") {
|
|
1213
|
+
return Object.fromEntries(
|
|
1214
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortJson(entry)])
|
|
1215
|
+
);
|
|
741
1216
|
}
|
|
742
|
-
return
|
|
1217
|
+
return value;
|
|
743
1218
|
}
|
|
744
1219
|
|
|
745
|
-
// ../../src/client/
|
|
746
|
-
var
|
|
747
|
-
constructor(
|
|
1220
|
+
// ../../src/client/standard-x402-payer.ts
|
|
1221
|
+
var StandardX402PayError = class extends Error {
|
|
1222
|
+
constructor(reason, message, detail = null) {
|
|
748
1223
|
super(message);
|
|
749
|
-
this.
|
|
750
|
-
this.detail = detail;
|
|
751
|
-
this.
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
constructor(config) {
|
|
768
|
-
this.
|
|
769
|
-
this.
|
|
770
|
-
this.
|
|
771
|
-
this.
|
|
772
|
-
this.
|
|
773
|
-
this.
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
* APPROVED deposit approval for this exact amount (e.g. the mandate's
|
|
780
|
-
* initialDeposit — "one Face ID covers mandate + first deposit") is looked
|
|
781
|
-
* up and used automatically before surfacing deposit_approval_required.
|
|
782
|
-
*/
|
|
783
|
-
async deposit(input) {
|
|
784
|
-
let approvalId = input.approvalId;
|
|
785
|
-
let prepared;
|
|
786
|
-
try {
|
|
787
|
-
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
788
|
-
wallet: this.signer.walletAddress,
|
|
789
|
-
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
790
|
-
...approvalId === void 0 ? {} : { approvalId }
|
|
791
|
-
});
|
|
792
|
-
} catch (error) {
|
|
793
|
-
if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId !== void 0) {
|
|
794
|
-
throw error;
|
|
795
|
-
}
|
|
796
|
-
approvalId = await this.findApprovedDepositApproval(input.amountRawUsdc);
|
|
797
|
-
if (approvalId === void 0) {
|
|
798
|
-
throw error;
|
|
1224
|
+
this.reason = reason;
|
|
1225
|
+
this.detail = detail;
|
|
1226
|
+
this.name = "StandardX402PayError";
|
|
1227
|
+
}
|
|
1228
|
+
reason;
|
|
1229
|
+
detail;
|
|
1230
|
+
};
|
|
1231
|
+
var StandardX402Payer = class {
|
|
1232
|
+
realizer;
|
|
1233
|
+
x402Fetch;
|
|
1234
|
+
probeFetch;
|
|
1235
|
+
defaultMaxAmountRawUsdc;
|
|
1236
|
+
network;
|
|
1237
|
+
usdcMint;
|
|
1238
|
+
stateStore;
|
|
1239
|
+
pending = /* @__PURE__ */ new Map();
|
|
1240
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
1241
|
+
nowMs;
|
|
1242
|
+
constructor(config) {
|
|
1243
|
+
this.realizer = config.realizer;
|
|
1244
|
+
this.x402Fetch = config.x402Fetch;
|
|
1245
|
+
this.probeFetch = config.probeFetch ?? fetch;
|
|
1246
|
+
this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
|
|
1247
|
+
this.network = config.network ?? SOLANA_MAINNET_NETWORK;
|
|
1248
|
+
this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
|
|
1249
|
+
this.stateStore = config.stateStore ?? null;
|
|
1250
|
+
this.nowMs = config.nowMs ?? (() => Date.now());
|
|
1251
|
+
if (this.stateStore !== null) {
|
|
1252
|
+
for (const record of this.stateStore.load()) {
|
|
1253
|
+
this.pending.set(record.key, record);
|
|
799
1254
|
}
|
|
800
|
-
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
801
|
-
wallet: this.signer.walletAddress,
|
|
802
|
-
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
803
|
-
approvalId
|
|
804
|
-
});
|
|
805
1255
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1256
|
+
}
|
|
1257
|
+
pay(input, realizer = this.realizer) {
|
|
1258
|
+
const method = (input.method ?? "GET").toUpperCase();
|
|
1259
|
+
const requestBodyHash = requestBodyHashFor(input.body ?? null);
|
|
1260
|
+
const pendingKey = pendingPaymentKey({
|
|
1261
|
+
url: input.url,
|
|
1262
|
+
method,
|
|
1263
|
+
requestBodyHash
|
|
810
1264
|
});
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1265
|
+
const existingFlow = this.inFlight.get(pendingKey);
|
|
1266
|
+
if (existingFlow !== void 0) {
|
|
1267
|
+
return existingFlow;
|
|
1268
|
+
}
|
|
1269
|
+
const run = async () => {
|
|
1270
|
+
if (this.stateStore?.withExclusiveLock) {
|
|
1271
|
+
this.pending.clear();
|
|
1272
|
+
for (const record of this.stateStore.load()) this.pending.set(record.key, record);
|
|
1273
|
+
}
|
|
1274
|
+
return this.run(input, { method, requestBodyHash, pendingKey }, realizer);
|
|
1275
|
+
};
|
|
1276
|
+
const flow = (this.stateStore?.withExclusiveLock ? this.stateStore.withExclusiveLock(run) : run()).finally(() => {
|
|
1277
|
+
this.inFlight.delete(pendingKey);
|
|
815
1278
|
});
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1279
|
+
this.inFlight.set(pendingKey, flow);
|
|
1280
|
+
return flow;
|
|
1281
|
+
}
|
|
1282
|
+
async run(input, computed, realizer) {
|
|
1283
|
+
const { method, requestBodyHash, pendingKey } = computed;
|
|
1284
|
+
const existingPending = this.pending.get(pendingKey);
|
|
1285
|
+
if (existingPending !== void 0 && input.forceNewPayment !== true) {
|
|
1286
|
+
throw new StandardX402PayError(
|
|
1287
|
+
"payment_outcome_unknown",
|
|
1288
|
+
"a previous external x402 payment for this request has an unknown outcome. Verify whether it settled before purchasing again; to pay again anyway, call with forceNewPayment=true.",
|
|
1289
|
+
existingPending
|
|
820
1290
|
);
|
|
821
1291
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1292
|
+
if (existingPending !== void 0 && input.forceNewPayment === true) {
|
|
1293
|
+
try {
|
|
1294
|
+
this.untrack(pendingKey);
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
throw new StandardX402PayError(
|
|
1297
|
+
"state_persist_failed",
|
|
1298
|
+
"could not clear the previous pending x402 marker before forcing a new payment",
|
|
1299
|
+
error
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
const init = {
|
|
1304
|
+
method,
|
|
1305
|
+
...input.body === void 0 ? {} : { body: input.body },
|
|
1306
|
+
...input.headers === void 0 ? {} : { headers: input.headers }
|
|
829
1307
|
};
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1308
|
+
const probe = await this.probeFetch(input.url, init);
|
|
1309
|
+
if (probe.status !== 402) {
|
|
1310
|
+
return { paid: false, status: probe.status, body: await probe.text() };
|
|
1311
|
+
}
|
|
1312
|
+
const selected2 = await this.selectRequirement(probe);
|
|
1313
|
+
const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
|
|
1314
|
+
if (selected2.amountRawUsdc > cap) {
|
|
1315
|
+
throw new StandardX402PayError(
|
|
1316
|
+
"amount_exceeds_client_cap",
|
|
1317
|
+
`the challenge demands ${selected2.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
|
|
1318
|
+
{ amountRawUsdc: selected2.amountRawUsdc.toString(), payTo: selected2.payTo }
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
let realized;
|
|
1322
|
+
try {
|
|
1323
|
+
realized = await realizer.ensureUsdcAvailable({
|
|
1324
|
+
amountRawUsdc: selected2.amountRawUsdc,
|
|
1325
|
+
payment: {
|
|
1326
|
+
payTo: selected2.payTo,
|
|
1327
|
+
amountRawUsdc: selected2.amountRawUsdc.toString(),
|
|
1328
|
+
resourceUrlHash: sha256HexOf(input.url),
|
|
1329
|
+
method
|
|
1330
|
+
},
|
|
846
1331
|
...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
|
|
1332
|
+
});
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
if (error.code === "approval_required") {
|
|
1335
|
+
throw new StandardX402PayError(
|
|
1336
|
+
"approval_required",
|
|
1337
|
+
"this payment exceeds the owner-approval threshold; NOTHING was paid. Ask the owner to open the approveUrl, then retry the same call with the approvalId",
|
|
1338
|
+
error.detail ?? null
|
|
1339
|
+
);
|
|
847
1340
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
853
|
-
});
|
|
854
|
-
let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
|
|
855
|
-
withdrawalId: prepared.withdrawalId,
|
|
856
|
-
serializedTransaction: signed.serializedTransaction,
|
|
857
|
-
agentSignature: signed.agentSignature
|
|
858
|
-
});
|
|
859
|
-
if (outcome.status === "submitted") {
|
|
860
|
-
outcome = await this.pollUntilTerminal(
|
|
861
|
-
`/v1/withdrawals/${prepared.withdrawalId}`,
|
|
862
|
-
outcome
|
|
1341
|
+
throw new StandardX402PayError(
|
|
1342
|
+
"realize_failed",
|
|
1343
|
+
`could not realize yield to cover ${selected2.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
|
|
1344
|
+
error
|
|
863
1345
|
);
|
|
864
1346
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1347
|
+
const pendingRecord = {
|
|
1348
|
+
key: pendingKey,
|
|
1349
|
+
url: input.url,
|
|
1350
|
+
method,
|
|
1351
|
+
requestBodyHash,
|
|
1352
|
+
amountRawUsdc: selected2.amountRawUsdc.toString(),
|
|
1353
|
+
payTo: selected2.payTo,
|
|
1354
|
+
feePayer: selected2.feePayer,
|
|
1355
|
+
realizedRawUsdc: realized.realizedRawUsdc.toString(),
|
|
1356
|
+
realizeTxSignature: realized.txSignature,
|
|
1357
|
+
status: "realized",
|
|
1358
|
+
createdAtMs: this.nowMs(),
|
|
1359
|
+
updatedAtMs: this.nowMs()
|
|
873
1360
|
};
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1361
|
+
try {
|
|
1362
|
+
this.track(pendingRecord);
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
throw new StandardX402PayError(
|
|
1365
|
+
"state_persist_failed",
|
|
1366
|
+
"could not persist the pending x402 marker; refusing to attempt the external payment because a restart would not be double-payment safe",
|
|
1367
|
+
{ error, pendingPayment: pendingRecord }
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
let response;
|
|
1371
|
+
try {
|
|
1372
|
+
response = await this.x402Fetch(input.url, init, selected2);
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
const persistError = this.tryMarkUnknown(pendingKey, {
|
|
1375
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1376
|
+
});
|
|
1377
|
+
throw new StandardX402PayError(
|
|
1378
|
+
"payment_outcome_unknown",
|
|
1379
|
+
`the x402 payment attempt failed after yield was realized; verify whether it settled before paying again: ${error instanceof Error ? error.message : String(error)}`,
|
|
1380
|
+
{ error, persistError }
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
const bodyText = await response.text();
|
|
1384
|
+
if (response.status !== 200) {
|
|
1385
|
+
const persistError = this.tryMarkUnknown(pendingKey, {
|
|
1386
|
+
status: response.status,
|
|
1387
|
+
body: bodyText
|
|
1388
|
+
});
|
|
1389
|
+
throw new StandardX402PayError(
|
|
1390
|
+
"payment_outcome_unknown",
|
|
1391
|
+
`the x402 payment attempt returned ${response.status} after yield was realized; verify whether it settled before paying again`,
|
|
1392
|
+
{ status: response.status, body: bodyText, persistError }
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
this.clearDelivered(pendingKey);
|
|
1396
|
+
const paymentTxSignature = extractSettledPaymentTxSignature(response);
|
|
1397
|
+
if (paymentTxSignature !== null && typeof realized.withdrawalId === "string" && realizer.reportPayment !== void 0) {
|
|
882
1398
|
try {
|
|
883
|
-
await
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1399
|
+
await realizer.reportPayment({
|
|
1400
|
+
withdrawalId: realized.withdrawalId,
|
|
1401
|
+
paymentTxSignature
|
|
1402
|
+
});
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
console.error(
|
|
1405
|
+
`[subly-x402] payment report-back failed (audit only, payment ok): ${error instanceof Error ? error.message : String(error)}`
|
|
887
1406
|
);
|
|
888
|
-
} catch {
|
|
889
1407
|
}
|
|
890
1408
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1409
|
+
return {
|
|
1410
|
+
paid: true,
|
|
1411
|
+
...realizer.vault === void 0 ? {} : { fundingVault: realizer.vault },
|
|
1412
|
+
status: response.status,
|
|
1413
|
+
body: bodyText,
|
|
1414
|
+
payment: {
|
|
1415
|
+
amountRawUsdc: selected2.amountRawUsdc.toString(),
|
|
1416
|
+
payTo: selected2.payTo,
|
|
1417
|
+
feePayer: selected2.feePayer,
|
|
1418
|
+
realizedRawUsdc: realized.realizedRawUsdc.toString(),
|
|
1419
|
+
realizeTxSignature: realized.txSignature,
|
|
1420
|
+
paymentTxSignature
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
/** Reads the challenge from the header (preferred) or the JSON body. */
|
|
1425
|
+
async selectRequirement(probe) {
|
|
1426
|
+
const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
1427
|
+
let requirements;
|
|
1428
|
+
try {
|
|
1429
|
+
if (header !== null) {
|
|
1430
|
+
requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
|
|
1431
|
+
} else {
|
|
1432
|
+
requirements = parseStandardChallenge(
|
|
1433
|
+
await probe.json()
|
|
1434
|
+
).solanaExactRequirements;
|
|
1435
|
+
}
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
throw new StandardX402PayError(
|
|
1438
|
+
error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
|
|
1439
|
+
"could not parse the x402 402 challenge",
|
|
1440
|
+
error
|
|
904
1441
|
);
|
|
905
1442
|
}
|
|
906
|
-
let parsed;
|
|
907
1443
|
try {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1444
|
+
return selectPayableSolanaRequirement(requirements, {
|
|
1445
|
+
network: this.network,
|
|
1446
|
+
usdcMint: this.usdcMint
|
|
1447
|
+
});
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
throw new StandardX402PayError(
|
|
1450
|
+
"no_payable_requirement",
|
|
1451
|
+
error instanceof Error ? error.message : String(error),
|
|
1452
|
+
error
|
|
914
1453
|
);
|
|
915
1454
|
}
|
|
916
|
-
const body = parsed;
|
|
917
|
-
return {
|
|
918
|
-
wallet: this.signer.walletAddress,
|
|
919
|
-
principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
|
|
920
|
-
positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
|
|
921
|
-
grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
|
|
922
|
-
spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
|
|
923
|
-
};
|
|
924
|
-
}
|
|
925
|
-
/** Best-effort audit link: reports the x402 payment tx a realize funded. */
|
|
926
|
-
async reportPayment(input) {
|
|
927
|
-
await this.postJson("submit", "/v1/payments/report", {
|
|
928
|
-
wallet: this.signer.walletAddress,
|
|
929
|
-
withdrawalId: input.withdrawalId,
|
|
930
|
-
paymentTxSignature: input.paymentTxSignature
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
/** Wallet's approvals as the relayer sees them (optionally by status). */
|
|
934
|
-
async listApprovals(status) {
|
|
935
|
-
const body = await this.getJson(
|
|
936
|
-
`/v1/wallets/${this.signer.walletAddress}/approvals${status === void 0 ? "" : `?status=${encodeURIComponent(status)}`}`
|
|
937
|
-
);
|
|
938
|
-
return body.approvals ?? [];
|
|
939
1455
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
{
|
|
949
|
-
|
|
950
|
-
...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
|
|
951
|
-
...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
|
|
952
|
-
...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
|
|
1456
|
+
track(record) {
|
|
1457
|
+
const previous = this.pending.get(record.key);
|
|
1458
|
+
this.pending.set(record.key, record);
|
|
1459
|
+
try {
|
|
1460
|
+
this.persist();
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
if (previous === void 0) {
|
|
1463
|
+
this.pending.delete(record.key);
|
|
1464
|
+
} else {
|
|
1465
|
+
this.pending.set(record.key, previous);
|
|
953
1466
|
}
|
|
954
|
-
|
|
1467
|
+
throw error;
|
|
1468
|
+
}
|
|
955
1469
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
);
|
|
1470
|
+
markUnknown(key, detail) {
|
|
1471
|
+
const current = this.pending.get(key);
|
|
1472
|
+
if (current === void 0) {
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
const next = {
|
|
1476
|
+
...current,
|
|
1477
|
+
status: "external_outcome_unknown",
|
|
1478
|
+
updatedAtMs: this.nowMs(),
|
|
1479
|
+
detail
|
|
1480
|
+
};
|
|
1481
|
+
this.pending.set(key, next);
|
|
1482
|
+
try {
|
|
1483
|
+
this.persist();
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
this.pending.set(key, current);
|
|
1486
|
+
throw error;
|
|
970
1487
|
}
|
|
971
|
-
return JSON.parse(text);
|
|
972
1488
|
}
|
|
973
|
-
|
|
974
|
-
* Finds an APPROVED, unconsumed deposit approval bound to exactly this
|
|
975
|
-
* amount — the shape the mandate's initialDeposit approval has.
|
|
976
|
-
*/
|
|
977
|
-
async findApprovedDepositApproval(amountRawUsdc) {
|
|
1489
|
+
tryMarkUnknown(key, detail) {
|
|
978
1490
|
try {
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
});
|
|
984
|
-
return match?.approvalId;
|
|
985
|
-
} catch {
|
|
986
|
-
return void 0;
|
|
1491
|
+
this.markUnknown(key, detail);
|
|
1492
|
+
return null;
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
return error;
|
|
987
1495
|
}
|
|
988
1496
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
const url = `${this.baseUrl}${path}`;
|
|
999
|
-
const response = await this.fetchImpl(url, {
|
|
1000
|
-
headers: await walletAuthHeaders({
|
|
1001
|
-
signer: this.signer,
|
|
1002
|
-
method: "GET",
|
|
1003
|
-
url
|
|
1004
|
-
})
|
|
1005
|
-
});
|
|
1006
|
-
if (response.status !== 200) {
|
|
1007
|
-
continue;
|
|
1008
|
-
}
|
|
1009
|
-
try {
|
|
1010
|
-
latest = await response.json();
|
|
1011
|
-
} catch {
|
|
1012
|
-
continue;
|
|
1013
|
-
}
|
|
1014
|
-
if (latest.status !== "submitted") {
|
|
1015
|
-
return latest;
|
|
1497
|
+
untrack(key) {
|
|
1498
|
+
const previous = this.pending.get(key);
|
|
1499
|
+
const existed = previous !== void 0;
|
|
1500
|
+
this.pending.delete(key);
|
|
1501
|
+
try {
|
|
1502
|
+
this.persist();
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
if (existed) {
|
|
1505
|
+
this.pending.set(key, previous);
|
|
1016
1506
|
}
|
|
1507
|
+
throw error;
|
|
1017
1508
|
}
|
|
1018
|
-
return latest;
|
|
1019
1509
|
}
|
|
1020
|
-
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
const response = await this.fetchImpl(url, {
|
|
1024
|
-
method: "POST",
|
|
1025
|
-
headers: {
|
|
1026
|
-
...await walletAuthHeaders({
|
|
1027
|
-
signer: this.signer,
|
|
1028
|
-
method: "POST",
|
|
1029
|
-
url,
|
|
1030
|
-
body: serialized
|
|
1031
|
-
}),
|
|
1032
|
-
"content-type": "application/json"
|
|
1033
|
-
},
|
|
1034
|
-
body: serialized
|
|
1035
|
-
});
|
|
1036
|
-
const text = await response.text();
|
|
1037
|
-
if (response.status !== 200) {
|
|
1038
|
-
const parsed = parseRelayerError(text);
|
|
1039
|
-
throw new VaultFlowClientError(
|
|
1040
|
-
step,
|
|
1041
|
-
parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
|
|
1042
|
-
text,
|
|
1043
|
-
parsed.code,
|
|
1044
|
-
parsed.details
|
|
1045
|
-
);
|
|
1046
|
-
}
|
|
1510
|
+
clearDelivered(key) {
|
|
1511
|
+
const previous = this.pending.get(key);
|
|
1512
|
+
this.pending.delete(key);
|
|
1047
1513
|
try {
|
|
1048
|
-
|
|
1049
|
-
} catch {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1514
|
+
this.persist();
|
|
1515
|
+
} catch (error) {
|
|
1516
|
+
if (previous !== void 0) {
|
|
1517
|
+
this.pending.set(key, previous);
|
|
1518
|
+
}
|
|
1519
|
+
console.error(
|
|
1520
|
+
`[subly-x402] payment delivered but pending marker could not be cleared: ${error instanceof Error ? error.message : String(error)}`
|
|
1054
1521
|
);
|
|
1055
1522
|
}
|
|
1056
1523
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
headers: await walletAuthHeaders({
|
|
1061
|
-
signer: this.signer,
|
|
1062
|
-
method: "GET",
|
|
1063
|
-
url
|
|
1064
|
-
})
|
|
1065
|
-
});
|
|
1066
|
-
const text = await response.text();
|
|
1067
|
-
if (response.status !== 200) {
|
|
1068
|
-
const parsed = parseRelayerError(text);
|
|
1069
|
-
throw new VaultFlowClientError(
|
|
1070
|
-
"read",
|
|
1071
|
-
parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
|
|
1072
|
-
text,
|
|
1073
|
-
parsed.code,
|
|
1074
|
-
parsed.details
|
|
1075
|
-
);
|
|
1076
|
-
}
|
|
1077
|
-
try {
|
|
1078
|
-
return JSON.parse(text);
|
|
1079
|
-
} catch {
|
|
1080
|
-
throw new VaultFlowClientError(
|
|
1081
|
-
"read",
|
|
1082
|
-
`${path} returned 200 with a non-JSON body`,
|
|
1083
|
-
text
|
|
1084
|
-
);
|
|
1524
|
+
persist() {
|
|
1525
|
+
if (this.stateStore === null) {
|
|
1526
|
+
return;
|
|
1085
1527
|
}
|
|
1528
|
+
this.stateStore.save([...this.pending.values()]);
|
|
1086
1529
|
}
|
|
1087
1530
|
};
|
|
1088
|
-
function
|
|
1531
|
+
function pendingPaymentKey(input) {
|
|
1532
|
+
return `${input.method}:${input.url}:${input.requestBodyHash}`;
|
|
1533
|
+
}
|
|
1534
|
+
function extractSettledPaymentTxSignature(response) {
|
|
1535
|
+
const header = response.headers.get("x-payment-response");
|
|
1536
|
+
if (header === null || header.length === 0) {
|
|
1537
|
+
return null;
|
|
1538
|
+
}
|
|
1089
1539
|
try {
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}
|
|
1540
|
+
const decoded = JSON.parse(
|
|
1541
|
+
Buffer.from(header, "base64").toString("utf8")
|
|
1542
|
+
);
|
|
1543
|
+
if (typeof decoded.transaction === "string" && decoded.transaction.length > 0) {
|
|
1544
|
+
return decoded.transaction;
|
|
1545
|
+
}
|
|
1546
|
+
if (typeof decoded.txHash === "string" && decoded.txHash.length > 0) {
|
|
1547
|
+
return decoded.txHash;
|
|
1548
|
+
}
|
|
1549
|
+
return null;
|
|
1096
1550
|
} catch {
|
|
1097
|
-
return
|
|
1551
|
+
return null;
|
|
1098
1552
|
}
|
|
1099
1553
|
}
|
|
1100
1554
|
|
|
@@ -1105,19 +1559,19 @@ var WITHDRAW_TOOL_NAME = "withdraw_from_subly_vault";
|
|
|
1105
1559
|
var BUDGET_TOOL_NAME = "get_subly_yield_budget";
|
|
1106
1560
|
var SETUP_TOOL_NAME = "create_subly_setup_link";
|
|
1107
1561
|
var SETUP_STATUS_TOOL_NAME = "check_subly_setup";
|
|
1108
|
-
var SERVER_INSTRUCTIONS = `Subly lets an agent pay standard x402 (HTTP 402) paid APIs that offer a Solana USDC exact rail with facilitator feePayer support from its wallet's Kamino vault YIELD \u2014 the
|
|
1562
|
+
var SERVER_INSTRUCTIONS = `Subly lets an agent pay standard x402 (HTTP 402) paid APIs that offer a Solana USDC exact rail with facilitator feePayer support from its wallet's Kamino vault YIELD \u2014 the relayer limits API spending to recorded yield, and the seller needs no Subly integration. With a configured vault catalog, call list_subly_vaults and select_subly_vault(vaultAddress) for the user's choice before owner setup. All subsequent tools use that vault until changed. Selection never moves existing funds; set up a separate mandate for each vault. Never select or switch vaults automatically based on APY.
|
|
1109
1563
|
|
|
1110
|
-
One-time setup: the operator needs a Solana agent wallet. Subly does NOT create wallets; either make a local keypair with \`solana-keygen new -o agent.json\` (or export one from an existing wallet) and point SUBLY_DEMO_AGENT_KEYPAIR_PATH at it, or use a custody wallet \u2014 set SUBLY_SIGNER_PROVIDER=circle (Circle developer-controlled wallet: CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET, CIRCLE_WALLET_ID) or =privy (Privy server wallet incl. agentic/owner-key wallets: PRIVY_APP_ID, PRIVY_APP_SECRET, PRIVY_WALLET_ID, plus PRIVY_AUTHORIZATION_KEY for owner-key wallets). With a local keypair the private key never leaves that file; with a custody provider it never enters this machine at all. Then fund the wallet with USDC on Solana mainnet \u2014
|
|
1564
|
+
One-time setup: the operator needs a Solana agent wallet. Subly does NOT create wallets; either make a local keypair with \`solana-keygen new -o agent.json\` (or export one from an existing wallet) and point SUBLY_DEMO_AGENT_KEYPAIR_PATH at it, or use a custody wallet \u2014 set SUBLY_SIGNER_PROVIDER=circle (Circle developer-controlled wallet: CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET, CIRCLE_WALLET_ID) or =privy (Privy server wallet incl. agentic/owner-key wallets: PRIVY_APP_ID, PRIVY_APP_SECRET, PRIVY_WALLET_ID, plus PRIVY_AUTHORIZATION_KEY for owner-key wallets). With a local keypair the private key never leaves that file; with a custody provider it never enters this machine at all. Then fund the wallet with USDC on Solana mainnet \u2014 a funded relayer sponsors vault transaction fees, which do not require agent SOL.
|
|
1111
1565
|
|
|
1112
1566
|
Owner (human) onboarding: deposits require the human owner's approval (Face ID / wallet signature). During the first deposit conversation, agree the spending limits and the first deposit amount in chat, then call create_subly_setup_link and paste the returned setupUrl to the user AS IS (it expires in 10 minutes). The human opens it on their phone, reviews, and confirms once \u2014 that single confirmation activates the spending mandate AND pre-approves the first deposit. Poll check_subly_setup(sessionId) after the user says they finished, then call deposit_to_subly_vault (the pre-approved first deposit is picked up automatically).
|
|
1113
1567
|
|
|
1114
1568
|
From there the agent can do everything with these tools:
|
|
1115
|
-
1. deposit_to_subly_vault(amountRawUsdc) puts wallet USDC into the vault (minimum
|
|
1569
|
+
1. deposit_to_subly_vault(amountRawUsdc) puts wallet USDC into the vault (the minimum depends on the selected vault) so it starts earning yield. If it returns approvalRequired, paste the approveUrl to the user and retry with the approvalId after they approve; if it returns setupRequired, run the owner onboarding above first.
|
|
1116
1570
|
2. get_subly_yield_budget() shows the principal, position value, and the spendable yield a payment can use right now.
|
|
1117
1571
|
3. fetch_with_subly_payment(url) GETs or POSTs a paid resource from a compatible x402 seller (e.g. Nansen): it realizes just enough yield to the agent's USDC ATA and pays the seller's Solana USDC exact challenge, returning the body plus the payment details. If it returns insufficient_yield, that is expected \u2014 yield accrues over time; wait, do not loop. If it returns approvalRequired (payment above the owner's threshold; NOTHING was paid), paste the approveUrl to the user, and once they say they approved, repeat the SAME call adding the approvalId.
|
|
1118
1572
|
4. withdraw_from_subly_vault(amountRawUsdc) exits: moves vault funds (principal included) back to the agent wallet's USDC account. If the owner's mandate requires withdrawal approval it returns approvalRequired \u2014 same paste-approveUrl-then-retry flow as deposits.`;
|
|
1119
|
-
|
|
1120
|
-
const {
|
|
1573
|
+
function createMcpPaymentServer(config) {
|
|
1574
|
+
const { signer: signer2, relayerBaseUrl: relayerBaseUrl2, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
|
|
1121
1575
|
const vaultFlows = config.vaultFlows ?? null;
|
|
1122
1576
|
const server = new Server(
|
|
1123
1577
|
{ name: "subly-payments", version: config.serverVersion ?? "0.3.0" },
|
|
@@ -1132,7 +1586,7 @@ async function runMcpPaymentServer(config) {
|
|
|
1132
1586
|
properties: {
|
|
1133
1587
|
initialDepositRawUsdc: {
|
|
1134
1588
|
type: "string",
|
|
1135
|
-
description: `First deposit bundled into the owner's single Face ID (raw USDC, 6 decimals;
|
|
1589
|
+
description: `First deposit bundled into the owner's single Face ID (raw USDC, 6 decimals; the minimum depends on the vault, e.g. "1010000"). Strongly recommended: without it the first deposit needs a separate approval.`
|
|
1136
1590
|
},
|
|
1137
1591
|
approvalThresholdRawUsdc: {
|
|
1138
1592
|
type: "string",
|
|
@@ -1250,12 +1704,26 @@ async function runMcpPaymentServer(config) {
|
|
|
1250
1704
|
];
|
|
1251
1705
|
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
1252
1706
|
tools: [
|
|
1707
|
+
...config.vaultSelection === void 0 ? [] : [
|
|
1708
|
+
{
|
|
1709
|
+
name: "list_subly_vaults",
|
|
1710
|
+
description: "List locally configured USDC Kamino vaults and the currently selected vault. Names come from on-chain metadata and may differ from Kamino's website.",
|
|
1711
|
+
inputSchema: { type: "object", properties: {} },
|
|
1712
|
+
annotations: { readOnlyHint: true, destructiveHint: false }
|
|
1713
|
+
},
|
|
1714
|
+
{
|
|
1715
|
+
name: "select_subly_vault",
|
|
1716
|
+
description: "Choose a vault from the local catalog for subsequent setup, deposit, budget, withdrawal, and payment tools. Checks the relayer supports the same vault. Does not move existing funds. Use the vault the user chose; never switch automatically based on APY.",
|
|
1717
|
+
inputSchema: { type: "object", properties: { vaultAddress: { type: "string" } }, required: ["vaultAddress"] },
|
|
1718
|
+
annotations: { readOnlyHint: false, destructiveHint: false }
|
|
1719
|
+
}
|
|
1720
|
+
],
|
|
1253
1721
|
...vaultTools,
|
|
1254
1722
|
{
|
|
1255
1723
|
name: TOOL_NAME,
|
|
1256
1724
|
description: `Fetch a URL (GET or POST), automatically paying a standard x402 (HTTP 402) challenge from a seller that offers Solana USDC \`exact\` with \`extra.feePayer\` (Nansen, etc.) out of the agent wallet's Kamino vault yield. Subly realizes just enough yield to the agent's USDC ATA (sponsored) and pays the seller's challenge; the seller needs no Subly integration. Returns the response body and, when a payment was made, the payment details (amount, payee, realize tx). Challenges above maxAmountRawUsdc (default ${defaultMaxAmountRawUsdc2} raw = ${formatRawUsdcAmount(
|
|
1257
1725
|
defaultMaxAmountRawUsdc2
|
|
1258
|
-
)} USDC) are refused without paying. Payments are refused when the spendable yield budget cannot cover them \u2014
|
|
1726
|
+
)} USDC) are refused without paying. Payments are refused when the spendable yield budget cannot cover them \u2014 principal is excluded by the relayer policy. Use only for URLs you intend to purchase access to.`,
|
|
1259
1727
|
inputSchema: {
|
|
1260
1728
|
type: "object",
|
|
1261
1729
|
properties: {
|
|
@@ -1353,6 +1821,22 @@ async function runMcpPaymentServer(config) {
|
|
|
1353
1821
|
);
|
|
1354
1822
|
};
|
|
1355
1823
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1824
|
+
if (config.vaultSelection && request.params.name === "list_subly_vaults") {
|
|
1825
|
+
return textResult(config.vaultSelection.list());
|
|
1826
|
+
}
|
|
1827
|
+
if (config.vaultSelection && request.params.name === "select_subly_vault") {
|
|
1828
|
+
const address3 = request.params.arguments?.vaultAddress;
|
|
1829
|
+
if (typeof address3 !== "string") return textResult({ message: "vaultAddress is required" }, true);
|
|
1830
|
+
try {
|
|
1831
|
+
return textResult(await config.vaultSelection.select(address3, relayerBaseUrl2));
|
|
1832
|
+
} catch (error) {
|
|
1833
|
+
return vaultFlowFailure(error);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
const session = config.vaultSelection?.current();
|
|
1837
|
+
const signer3 = session?.signer ?? config.signer;
|
|
1838
|
+
const vaultFlows2 = session?.vaultFlows ?? config.vaultFlows ?? null;
|
|
1839
|
+
const payer2 = session?.payer ?? config.payer;
|
|
1356
1840
|
const vaultToolNames = [
|
|
1357
1841
|
BUDGET_TOOL_NAME,
|
|
1358
1842
|
DEPOSIT_TOOL_NAME,
|
|
@@ -1360,18 +1844,18 @@ async function runMcpPaymentServer(config) {
|
|
|
1360
1844
|
SETUP_TOOL_NAME,
|
|
1361
1845
|
SETUP_STATUS_TOOL_NAME
|
|
1362
1846
|
];
|
|
1363
|
-
if (
|
|
1847
|
+
if (vaultFlows2 !== null && vaultToolNames.includes(request.params.name)) {
|
|
1364
1848
|
const needsChainSync = request.params.name !== SETUP_TOOL_NAME && request.params.name !== SETUP_STATUS_TOOL_NAME;
|
|
1365
1849
|
if (needsChainSync) {
|
|
1366
1850
|
try {
|
|
1367
|
-
await ensureWalletOnboarded({ relayerBaseUrl: relayerBaseUrl2, signer:
|
|
1851
|
+
await ensureWalletOnboarded({ relayerBaseUrl: relayerBaseUrl2, signer: signer3 });
|
|
1368
1852
|
} catch {
|
|
1369
1853
|
}
|
|
1370
1854
|
}
|
|
1371
1855
|
const args2 = request.params.arguments ?? {};
|
|
1372
1856
|
if (request.params.name === BUDGET_TOOL_NAME) {
|
|
1373
1857
|
try {
|
|
1374
|
-
const budget = await
|
|
1858
|
+
const budget = await vaultFlows2.getBudget();
|
|
1375
1859
|
return textResult({
|
|
1376
1860
|
...budget,
|
|
1377
1861
|
principalUsdc: formatRawUsdcAmount(
|
|
@@ -1402,7 +1886,7 @@ async function runMcpPaymentServer(config) {
|
|
|
1402
1886
|
policy[key] = value;
|
|
1403
1887
|
}
|
|
1404
1888
|
}
|
|
1405
|
-
const created = await
|
|
1889
|
+
const created = await vaultFlows2.createSetupSession({
|
|
1406
1890
|
...Object.keys(policy).length === 0 ? {} : { policy },
|
|
1407
1891
|
...typeof args2.mandateTtlDays === "number" ? { mandateTtlDays: args2.mandateTtlDays } : {},
|
|
1408
1892
|
...typeof args2.initialDepositRawUsdc === "string" ? { initialDepositRawUsdc: args2.initialDepositRawUsdc } : {}
|
|
@@ -1424,7 +1908,7 @@ async function runMcpPaymentServer(config) {
|
|
|
1424
1908
|
);
|
|
1425
1909
|
}
|
|
1426
1910
|
try {
|
|
1427
|
-
return textResult(await
|
|
1911
|
+
return textResult(await vaultFlows2.getSetupSession(sessionId));
|
|
1428
1912
|
} catch (error) {
|
|
1429
1913
|
return vaultFlowFailure(error);
|
|
1430
1914
|
}
|
|
@@ -1442,7 +1926,7 @@ async function runMcpPaymentServer(config) {
|
|
|
1442
1926
|
const flowApprovalId = typeof args2.approvalId === "string" && args2.approvalId.length > 0 ? args2.approvalId : void 0;
|
|
1443
1927
|
try {
|
|
1444
1928
|
if (request.params.name === DEPOSIT_TOOL_NAME) {
|
|
1445
|
-
const outcome2 = await
|
|
1929
|
+
const outcome2 = await vaultFlows2.deposit({
|
|
1446
1930
|
amountRawUsdc,
|
|
1447
1931
|
...flowApprovalId === void 0 ? {} : { approvalId: flowApprovalId }
|
|
1448
1932
|
});
|
|
@@ -1452,7 +1936,7 @@ async function runMcpPaymentServer(config) {
|
|
|
1452
1936
|
)
|
|
1453
1937
|
});
|
|
1454
1938
|
}
|
|
1455
|
-
const outcome = await
|
|
1939
|
+
const outcome = await vaultFlows2.withdraw({
|
|
1456
1940
|
amountRawUsdc,
|
|
1457
1941
|
...flowApprovalId === void 0 ? {} : { approvalId: flowApprovalId }
|
|
1458
1942
|
});
|
|
@@ -1585,6 +2069,11 @@ async function runMcpPaymentServer(config) {
|
|
|
1585
2069
|
};
|
|
1586
2070
|
}
|
|
1587
2071
|
});
|
|
2072
|
+
return server;
|
|
2073
|
+
}
|
|
2074
|
+
async function runMcpPaymentServer(config) {
|
|
2075
|
+
const server = createMcpPaymentServer(config);
|
|
2076
|
+
const { signer: signer2, relayerBaseUrl: relayerBaseUrl2, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
|
|
1588
2077
|
try {
|
|
1589
2078
|
await ensureWalletOnboarded({ relayerBaseUrl: relayerBaseUrl2, signer: signer2 });
|
|
1590
2079
|
console.error("[subly-mcp] wallet registered and synced at the relayer");
|
|
@@ -1600,136 +2089,6 @@ async function runMcpPaymentServer(config) {
|
|
|
1600
2089
|
);
|
|
1601
2090
|
}
|
|
1602
2091
|
|
|
1603
|
-
// ../../src/client/relayer-yield-realizer.ts
|
|
1604
|
-
var REALIZE_OVERHEAD_RAW_USDC = 2500n;
|
|
1605
|
-
var RelayerRealizeError = class extends Error {
|
|
1606
|
-
constructor(code, message, detail = null) {
|
|
1607
|
-
super(message);
|
|
1608
|
-
this.code = code;
|
|
1609
|
-
this.detail = detail;
|
|
1610
|
-
this.name = "RelayerRealizeError";
|
|
1611
|
-
}
|
|
1612
|
-
code;
|
|
1613
|
-
detail;
|
|
1614
|
-
};
|
|
1615
|
-
var RelayerYieldRealizer = class {
|
|
1616
|
-
vaultFlows;
|
|
1617
|
-
constructor(config) {
|
|
1618
|
-
this.vaultFlows = new VaultFlowClient({
|
|
1619
|
-
relayerBaseUrl: config.relayerBaseUrl,
|
|
1620
|
-
signer: config.signer,
|
|
1621
|
-
rpc: config.rpc,
|
|
1622
|
-
...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
|
|
1623
|
-
...config.lookupTablesFor === void 0 ? {} : { lookupTablesFor: config.lookupTablesFor }
|
|
1624
|
-
});
|
|
1625
|
-
}
|
|
1626
|
-
async ensureUsdcAvailable(input) {
|
|
1627
|
-
const shortfallRawUsdc = input.amountRawUsdc;
|
|
1628
|
-
await this.assertSpendableYield(shortfallRawUsdc);
|
|
1629
|
-
let outcome;
|
|
1630
|
-
try {
|
|
1631
|
-
outcome = await this.vaultFlows.withdraw({
|
|
1632
|
-
amountRawUsdc: shortfallRawUsdc,
|
|
1633
|
-
// The relayer refuses to prepare this withdrawal beyond the spendable
|
|
1634
|
-
// yield — the principal-protection guard the client cannot bypass.
|
|
1635
|
-
purpose: "yield_realize",
|
|
1636
|
-
// Declares what is being paid so the relayer's spending-mandate layer
|
|
1637
|
-
// can enforce caps/payee and keep the mandate → payment audit chain.
|
|
1638
|
-
...input.payment === void 0 ? {} : { payment: input.payment },
|
|
1639
|
-
...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
|
|
1640
|
-
});
|
|
1641
|
-
} catch (error) {
|
|
1642
|
-
throw this.mapWithdrawError(error);
|
|
1643
|
-
}
|
|
1644
|
-
if (outcome.status !== "confirmed" || outcome.txSignature === null) {
|
|
1645
|
-
throw new RelayerRealizeError(
|
|
1646
|
-
"realize_not_confirmed",
|
|
1647
|
-
`yield realize withdrawal did not confirm (status=${outcome.status})`,
|
|
1648
|
-
outcome
|
|
1649
|
-
);
|
|
1650
|
-
}
|
|
1651
|
-
return {
|
|
1652
|
-
realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
|
|
1653
|
-
txSignature: outcome.txSignature,
|
|
1654
|
-
withdrawalId: outcome.withdrawalId
|
|
1655
|
-
};
|
|
1656
|
-
}
|
|
1657
|
-
/**
|
|
1658
|
-
* Best-effort report-back of the x402 payment tx this realize funded —
|
|
1659
|
-
* closes the relayer's mandate → realize → payment audit chain. Callers
|
|
1660
|
-
* must never let a failure here affect the payment result.
|
|
1661
|
-
*/
|
|
1662
|
-
async reportPayment(input) {
|
|
1663
|
-
await this.vaultFlows.reportPayment(input);
|
|
1664
|
-
}
|
|
1665
|
-
/**
|
|
1666
|
-
* Refuses to realize more than the ledger's spendable yield (principal).
|
|
1667
|
-
* getBudget syncs the relayer's ledger from chain first (best-effort), so a
|
|
1668
|
-
* long-running client sees yield as it accrues instead of a frozen view.
|
|
1669
|
-
*/
|
|
1670
|
-
async assertSpendableYield(shortfallRawUsdc) {
|
|
1671
|
-
let spendable;
|
|
1672
|
-
try {
|
|
1673
|
-
const budget = await this.vaultFlows.getBudget();
|
|
1674
|
-
spendable = BigInt(budget.spendableYieldRawUsdc);
|
|
1675
|
-
} catch (error) {
|
|
1676
|
-
throw new RelayerRealizeError(
|
|
1677
|
-
"budget_unavailable",
|
|
1678
|
-
"could not read the spendable-yield budget",
|
|
1679
|
-
error
|
|
1680
|
-
);
|
|
1681
|
-
}
|
|
1682
|
-
const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
|
|
1683
|
-
if (spendable < requiredRawUsdc) {
|
|
1684
|
-
throw new RelayerRealizeError(
|
|
1685
|
-
"insufficient_yield",
|
|
1686
|
-
`spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC plus the ${REALIZE_OVERHEAD_RAW_USDC} raw fee headroom; the principal is never spent \u2014 wait for more yield`,
|
|
1687
|
-
{ spendableYieldRawUsdc: spendable.toString() }
|
|
1688
|
-
);
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
mapWithdrawError(error) {
|
|
1692
|
-
if (!(error instanceof VaultFlowClientError)) {
|
|
1693
|
-
return new RelayerRealizeError(
|
|
1694
|
-
"prepare_failed",
|
|
1695
|
-
`yield realize failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1696
|
-
error
|
|
1697
|
-
);
|
|
1698
|
-
}
|
|
1699
|
-
const serverCode = error.code ?? errorCodeFrom(error.detail);
|
|
1700
|
-
if (serverCode === "approval_required") {
|
|
1701
|
-
return new RelayerRealizeError(
|
|
1702
|
-
"approval_required",
|
|
1703
|
-
"this payment exceeds the owner-approval threshold; nothing was realized or paid. Ask the owner to approve, then retry with the approvalId",
|
|
1704
|
-
error.errorDetails ?? error.detail
|
|
1705
|
-
);
|
|
1706
|
-
}
|
|
1707
|
-
if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
|
|
1708
|
-
return new RelayerRealizeError(
|
|
1709
|
-
"insufficient_yield",
|
|
1710
|
-
"the relayer refused to realize beyond the spendable yield; the principal is never spent \u2014 wait for more yield",
|
|
1711
|
-
error.detail
|
|
1712
|
-
);
|
|
1713
|
-
}
|
|
1714
|
-
return new RelayerRealizeError(
|
|
1715
|
-
error.step === "submit" ? "submit_failed" : "prepare_failed",
|
|
1716
|
-
error.message,
|
|
1717
|
-
error.detail
|
|
1718
|
-
);
|
|
1719
|
-
}
|
|
1720
|
-
};
|
|
1721
|
-
function errorCodeFrom(detail) {
|
|
1722
|
-
if (typeof detail !== "string") {
|
|
1723
|
-
return null;
|
|
1724
|
-
}
|
|
1725
|
-
try {
|
|
1726
|
-
const parsed = JSON.parse(detail);
|
|
1727
|
-
return typeof parsed.error?.code === "string" ? parsed.error.code : null;
|
|
1728
|
-
} catch {
|
|
1729
|
-
return null;
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
2092
|
// ../../src/client/relayer-payer.ts
|
|
1734
2093
|
function createRelayerX402Payer(config) {
|
|
1735
2094
|
const realizer = new RelayerYieldRealizer({
|
|
@@ -1749,22 +2108,22 @@ function createRelayerX402Payer(config) {
|
|
|
1749
2108
|
import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
|
|
1750
2109
|
|
|
1751
2110
|
// ../../src/solana/keys.ts
|
|
1752
|
-
import { readFileSync } from "node:fs";
|
|
1753
|
-
import
|
|
2111
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
2112
|
+
import bs583 from "bs58";
|
|
1754
2113
|
import {
|
|
1755
2114
|
createKeyPairSignerFromBytes
|
|
1756
2115
|
} from "@solana/kit";
|
|
1757
2116
|
function loadSecretKeyBytes(params) {
|
|
1758
2117
|
const { base58Secret, jsonFilePath, label } = params;
|
|
1759
2118
|
if (base58Secret !== void 0 && base58Secret.length > 0) {
|
|
1760
|
-
const bytes =
|
|
2119
|
+
const bytes = bs583.decode(base58Secret);
|
|
1761
2120
|
if (bytes.length !== 64) {
|
|
1762
2121
|
throw new Error(`${label} base58 secret must decode to 64 bytes`);
|
|
1763
2122
|
}
|
|
1764
2123
|
return bytes;
|
|
1765
2124
|
}
|
|
1766
2125
|
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
1767
|
-
const raw = JSON.parse(
|
|
2126
|
+
const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
|
|
1768
2127
|
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
1769
2128
|
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
1770
2129
|
}
|
|
@@ -1779,7 +2138,7 @@ import bs587 from "bs58";
|
|
|
1779
2138
|
import nacl3 from "tweetnacl";
|
|
1780
2139
|
|
|
1781
2140
|
// ../../src/solana/tx.ts
|
|
1782
|
-
import
|
|
2141
|
+
import bs584 from "bs58";
|
|
1783
2142
|
import {
|
|
1784
2143
|
appendTransactionMessageInstructions,
|
|
1785
2144
|
compileTransaction,
|
|
@@ -1826,11 +2185,11 @@ function signatureBase58ForSigner(transaction, signer2) {
|
|
|
1826
2185
|
if (signature === null || signature === void 0) {
|
|
1827
2186
|
return null;
|
|
1828
2187
|
}
|
|
1829
|
-
return
|
|
2188
|
+
return bs584.encode(signature);
|
|
1830
2189
|
}
|
|
1831
2190
|
|
|
1832
2191
|
// ../../src/client/remote-signer-transport.ts
|
|
1833
|
-
import
|
|
2192
|
+
import bs585 from "bs58";
|
|
1834
2193
|
import nacl2 from "tweetnacl";
|
|
1835
2194
|
var RemoteSigningError = class extends Error {
|
|
1836
2195
|
constructor(provider, message, detail = null) {
|
|
@@ -1845,7 +2204,7 @@ var RemoteSigningError = class extends Error {
|
|
|
1845
2204
|
function ed25519PublicKeyBytes(provider, walletAddress) {
|
|
1846
2205
|
let bytes;
|
|
1847
2206
|
try {
|
|
1848
|
-
bytes =
|
|
2207
|
+
bytes = bs585.decode(walletAddress);
|
|
1849
2208
|
} catch {
|
|
1850
2209
|
throw new RemoteSigningError(
|
|
1851
2210
|
provider,
|
|
@@ -1861,10 +2220,10 @@ function ed25519PublicKeyBytes(provider, walletAddress) {
|
|
|
1861
2220
|
return bytes;
|
|
1862
2221
|
}
|
|
1863
2222
|
function verifiedEd25519Signature(params) {
|
|
1864
|
-
const
|
|
2223
|
+
const publicKey2 = ed25519PublicKeyBytes(params.provider, params.walletAddress);
|
|
1865
2224
|
const encoded = params.encodedSignature.trim();
|
|
1866
2225
|
for (const candidate of decodeSignatureCandidates(encoded)) {
|
|
1867
|
-
if (nacl2.sign.detached.verify(params.message, candidate,
|
|
2226
|
+
if (nacl2.sign.detached.verify(params.message, candidate, publicKey2)) {
|
|
1868
2227
|
return candidate;
|
|
1869
2228
|
}
|
|
1870
2229
|
}
|
|
@@ -1880,7 +2239,7 @@ function decodeSignatureCandidates(encoded) {
|
|
|
1880
2239
|
candidates.push(Uint8Array.from(Buffer.from(hex, "hex")));
|
|
1881
2240
|
}
|
|
1882
2241
|
try {
|
|
1883
|
-
const fromBase58 =
|
|
2242
|
+
const fromBase58 = bs585.decode(encoded);
|
|
1884
2243
|
if (fromBase58.length === 64) {
|
|
1885
2244
|
candidates.push(fromBase58);
|
|
1886
2245
|
}
|
|
@@ -1918,8 +2277,8 @@ async function requestVerifiedTransactionSignature(params) {
|
|
|
1918
2277
|
`signed transaction is missing the signature for ${transport.walletAddress}`
|
|
1919
2278
|
);
|
|
1920
2279
|
}
|
|
1921
|
-
const
|
|
1922
|
-
if (!nacl2.sign.detached.verify(params.messageBytes, signature,
|
|
2280
|
+
const publicKey2 = params.publicKey ?? ed25519PublicKeyBytes(transport.provider, transport.walletAddress);
|
|
2281
|
+
if (!nacl2.sign.detached.verify(params.messageBytes, signature, publicKey2)) {
|
|
1923
2282
|
throw new RemoteSigningError(
|
|
1924
2283
|
transport.provider,
|
|
1925
2284
|
"returned signature does not verify over the requested transaction"
|
|
@@ -1941,7 +2300,7 @@ async function externallySignedAgentTransaction(params) {
|
|
|
1941
2300
|
});
|
|
1942
2301
|
return {
|
|
1943
2302
|
serializedTransaction: attached.serializedBase64,
|
|
1944
|
-
agentSignature:
|
|
2303
|
+
agentSignature: bs585.encode(signature)
|
|
1945
2304
|
};
|
|
1946
2305
|
}
|
|
1947
2306
|
async function providerJsonRequest(params) {
|
|
@@ -1985,98 +2344,6 @@ function computeRequestBindingHash(fields) {
|
|
|
1985
2344
|
});
|
|
1986
2345
|
}
|
|
1987
2346
|
|
|
1988
|
-
// ../../src/lib/associated-token-account.ts
|
|
1989
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
1990
|
-
import bs585 from "bs58";
|
|
1991
|
-
var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
|
|
1992
|
-
var ED25519_P = (1n << 255n) - 19n;
|
|
1993
|
-
var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
|
|
1994
|
-
function deriveAssociatedTokenAddress(params) {
|
|
1995
|
-
const owner = decodePublicKey(params.owner, "owner");
|
|
1996
|
-
const mint = decodePublicKey(params.mint ?? SUBLY_VAULT.usdcMint, "mint");
|
|
1997
|
-
const tokenProgramId = decodePublicKey(
|
|
1998
|
-
params.tokenProgramId ?? SPL_TOKEN_PROGRAM_ID,
|
|
1999
|
-
"tokenProgramId"
|
|
2000
|
-
);
|
|
2001
|
-
const associatedTokenProgramId = decodePublicKey(
|
|
2002
|
-
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
2003
|
-
"associatedTokenProgramId"
|
|
2004
|
-
);
|
|
2005
|
-
for (let bump = 255; bump >= 0; bump -= 1) {
|
|
2006
|
-
const address3 = createProgramAddress(
|
|
2007
|
-
[owner, tokenProgramId, mint, Uint8Array.of(bump)],
|
|
2008
|
-
associatedTokenProgramId
|
|
2009
|
-
);
|
|
2010
|
-
if (address3 !== null) {
|
|
2011
|
-
return bs585.encode(address3);
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
throw new Error("Unable to derive associated token account address");
|
|
2015
|
-
}
|
|
2016
|
-
function createProgramAddress(seeds, programId) {
|
|
2017
|
-
const hash = createHash4("sha256");
|
|
2018
|
-
for (const seed of seeds) {
|
|
2019
|
-
hash.update(seed);
|
|
2020
|
-
}
|
|
2021
|
-
hash.update(programId);
|
|
2022
|
-
hash.update(PDA_MARKER);
|
|
2023
|
-
const digest = hash.digest();
|
|
2024
|
-
return isEd25519Point(digest) ? null : new Uint8Array(digest);
|
|
2025
|
-
}
|
|
2026
|
-
function decodePublicKey(value, fieldName) {
|
|
2027
|
-
const decoded = bs585.decode(value);
|
|
2028
|
-
if (decoded.length !== 32) {
|
|
2029
|
-
throw new Error(`${fieldName} must be a 32-byte public key`);
|
|
2030
|
-
}
|
|
2031
|
-
return decoded;
|
|
2032
|
-
}
|
|
2033
|
-
function isEd25519Point(bytes) {
|
|
2034
|
-
if (bytes.length !== 32) {
|
|
2035
|
-
return false;
|
|
2036
|
-
}
|
|
2037
|
-
const yBytes = Uint8Array.from(bytes);
|
|
2038
|
-
yBytes[31] = yBytes[31] & 127;
|
|
2039
|
-
const y = littleEndianToBigInt(yBytes);
|
|
2040
|
-
if (y >= ED25519_P) {
|
|
2041
|
-
return false;
|
|
2042
|
-
}
|
|
2043
|
-
const ySquared = mod(y * y, ED25519_P);
|
|
2044
|
-
const numerator = mod(ySquared - 1n, ED25519_P);
|
|
2045
|
-
const denominator = mod(ED25519_D * ySquared + 1n, ED25519_P);
|
|
2046
|
-
if (denominator === 0n) {
|
|
2047
|
-
return false;
|
|
2048
|
-
}
|
|
2049
|
-
const xSquared = mod(
|
|
2050
|
-
numerator * modPow(denominator, ED25519_P - 2n, ED25519_P),
|
|
2051
|
-
ED25519_P
|
|
2052
|
-
);
|
|
2053
|
-
return xSquared === 0n || modPow(xSquared, (ED25519_P - 1n) / 2n, ED25519_P) === 1n;
|
|
2054
|
-
}
|
|
2055
|
-
function littleEndianToBigInt(bytes) {
|
|
2056
|
-
let value = 0n;
|
|
2057
|
-
for (let index = bytes.length - 1; index >= 0; index -= 1) {
|
|
2058
|
-
value = (value << 8n) + BigInt(bytes[index]);
|
|
2059
|
-
}
|
|
2060
|
-
return value;
|
|
2061
|
-
}
|
|
2062
|
-
function mod(value, modulus) {
|
|
2063
|
-
const result = value % modulus;
|
|
2064
|
-
return result >= 0n ? result : result + modulus;
|
|
2065
|
-
}
|
|
2066
|
-
function modPow(base, exponent, modulus) {
|
|
2067
|
-
let result = 1n;
|
|
2068
|
-
let nextBase = mod(base, modulus);
|
|
2069
|
-
let nextExponent = exponent;
|
|
2070
|
-
while (nextExponent > 0n) {
|
|
2071
|
-
if ((nextExponent & 1n) === 1n) {
|
|
2072
|
-
result = mod(result * nextBase, modulus);
|
|
2073
|
-
}
|
|
2074
|
-
nextBase = mod(nextBase * nextBase, modulus);
|
|
2075
|
-
nextExponent >>= 1n;
|
|
2076
|
-
}
|
|
2077
|
-
return result;
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
2347
|
// ../../src/client/transaction-intent-validator.ts
|
|
2081
2348
|
var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
|
|
2082
2349
|
var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
|
|
@@ -2219,16 +2486,16 @@ function validatePaymentIntentTransaction(params) {
|
|
|
2219
2486
|
if (intent.network !== SOLANA_MAINNET_NETWORK) {
|
|
2220
2487
|
reject("network_mismatch", "Unsupported network");
|
|
2221
2488
|
}
|
|
2222
|
-
if (intent.vault !==
|
|
2489
|
+
if (intent.vault !== policy.vault.address) {
|
|
2223
2490
|
reject("vault_mismatch", "Unsupported vault");
|
|
2224
2491
|
}
|
|
2225
|
-
if (intent.shareMint !==
|
|
2492
|
+
if (intent.shareMint !== policy.vault.shareMint) {
|
|
2226
2493
|
reject("share_mint_mismatch", "Unsupported share mint");
|
|
2227
2494
|
}
|
|
2228
|
-
if (intent.farm !==
|
|
2495
|
+
if (intent.farm !== policy.vault.farm) {
|
|
2229
2496
|
reject("farm_mismatch", "Unsupported Kamino farm");
|
|
2230
2497
|
}
|
|
2231
|
-
if (intent.asset !==
|
|
2498
|
+
if (intent.asset !== policy.vault.usdcMint) {
|
|
2232
2499
|
reject("asset_mismatch", "Only USDC payments are supported");
|
|
2233
2500
|
}
|
|
2234
2501
|
if (intent.memo !== intent.paymentId) {
|
|
@@ -2344,7 +2611,7 @@ function validateDepositIntentTransaction(params) {
|
|
|
2344
2611
|
if (new Date(intent.expiresAt).getTime() <= now) {
|
|
2345
2612
|
reject("expired", "Deposit intent has expired");
|
|
2346
2613
|
}
|
|
2347
|
-
assertVaultIntentTargets(intent);
|
|
2614
|
+
assertVaultIntentTargets(intent, policy.vault);
|
|
2348
2615
|
const decoded = decodeIntentTransaction({
|
|
2349
2616
|
serializedTransaction: params.serializedTransaction,
|
|
2350
2617
|
...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
|
|
@@ -2367,6 +2634,9 @@ function validateDepositIntentTransaction(params) {
|
|
|
2367
2634
|
case MEMO_PROGRAM_ID:
|
|
2368
2635
|
break;
|
|
2369
2636
|
case KVAULT_PROGRAM_ID: {
|
|
2637
|
+
if (sawDeposit) {
|
|
2638
|
+
reject("duplicate_deposit", "A deposit intent authorizes exactly one KVault deposit");
|
|
2639
|
+
}
|
|
2370
2640
|
if (!bytesStartWith(ix.data, KVAULT_DEPOSIT_DISCRIMINATOR)) {
|
|
2371
2641
|
reject("unexpected_instruction", "Unexpected KVault instruction in deposit");
|
|
2372
2642
|
}
|
|
@@ -2417,7 +2687,7 @@ function validateWithdrawalIntentTransaction(params) {
|
|
|
2417
2687
|
if (new Date(intent.expiresAt).getTime() <= now) {
|
|
2418
2688
|
reject("expired", "Withdrawal intent has expired");
|
|
2419
2689
|
}
|
|
2420
|
-
assertVaultIntentTargets(intent);
|
|
2690
|
+
assertVaultIntentTargets(intent, policy.vault);
|
|
2421
2691
|
const expectedDestination = deriveAssociatedTokenAddress({
|
|
2422
2692
|
owner: intent.wallet,
|
|
2423
2693
|
mint: intent.asset
|
|
@@ -2515,22 +2785,23 @@ function validateWithdrawalIntentTransaction(params) {
|
|
|
2515
2785
|
);
|
|
2516
2786
|
}
|
|
2517
2787
|
}
|
|
2518
|
-
function assertVaultIntentTargets(intent) {
|
|
2519
|
-
if (intent.vault !==
|
|
2788
|
+
function assertVaultIntentTargets(intent, vault) {
|
|
2789
|
+
if (intent.vault !== vault.address) {
|
|
2520
2790
|
reject("vault_mismatch", "Unsupported vault");
|
|
2521
2791
|
}
|
|
2522
|
-
if (intent.shareMint !==
|
|
2792
|
+
if (intent.shareMint !== vault.shareMint) {
|
|
2523
2793
|
reject("share_mint_mismatch", "Unsupported share mint");
|
|
2524
2794
|
}
|
|
2525
|
-
if (intent.farm !==
|
|
2795
|
+
if (intent.farm !== vault.farm) {
|
|
2526
2796
|
reject("farm_mismatch", "Unsupported Kamino farm");
|
|
2527
2797
|
}
|
|
2528
|
-
if (intent.asset !==
|
|
2798
|
+
if (intent.asset !== vault.usdcMint) {
|
|
2529
2799
|
reject("asset_mismatch", "Only USDC is supported");
|
|
2530
2800
|
}
|
|
2531
2801
|
}
|
|
2532
2802
|
function resolveIntentValidationPolicy(policy) {
|
|
2533
2803
|
const resolved = {
|
|
2804
|
+
vault: policy?.vault ?? SUBLY_VAULT,
|
|
2534
2805
|
maxComputeUnitLimit: policy?.maxComputeUnitLimit ?? DEFAULT_MAX_COMPUTE_UNIT_LIMIT,
|
|
2535
2806
|
maxComputeUnitPriceMicroLamports: policy?.maxComputeUnitPriceMicroLamports ?? DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
|
|
2536
2807
|
maxTemporaryAccountLamports: policy?.maxTemporaryAccountLamports ?? MAX_TEMP_ACCOUNT_LAMPORTS
|
|
@@ -2882,10 +3153,12 @@ function readShortVec(bytes, startOffset) {
|
|
|
2882
3153
|
|
|
2883
3154
|
// ../../src/client/agent-wallet-signer.ts
|
|
2884
3155
|
var IntentValidatingAgentWalletSigner = class {
|
|
3156
|
+
vault;
|
|
2885
3157
|
validationMode = "structured_intent_transaction";
|
|
2886
3158
|
validationPolicy;
|
|
2887
3159
|
constructor(validationPolicy) {
|
|
2888
|
-
this.
|
|
3160
|
+
this.vault = Object.freeze({ ...validationPolicy?.vault ?? SUBLY_VAULT });
|
|
3161
|
+
this.validationPolicy = { ...validationPolicy, vault: this.vault };
|
|
2889
3162
|
}
|
|
2890
3163
|
async signPayment(params) {
|
|
2891
3164
|
this.assertIntentWallet(params.intent.wallet);
|
|
@@ -3043,15 +3316,15 @@ async function createCircleSignerTransport(config) {
|
|
|
3043
3316
|
const entitySecretCiphertext = async () => {
|
|
3044
3317
|
if (entityPublicKey === null) {
|
|
3045
3318
|
const data = await request("GET", "/v1/w3s/config/entity/publicKey");
|
|
3046
|
-
const
|
|
3047
|
-
if (typeof
|
|
3319
|
+
const publicKey2 = data.publicKey;
|
|
3320
|
+
if (typeof publicKey2 !== "string") {
|
|
3048
3321
|
throw new RemoteSigningError(
|
|
3049
3322
|
PROVIDER,
|
|
3050
3323
|
"entity public key response has no publicKey",
|
|
3051
3324
|
data
|
|
3052
3325
|
);
|
|
3053
3326
|
}
|
|
3054
|
-
entityPublicKey = createPublicKey(
|
|
3327
|
+
entityPublicKey = createPublicKey(publicKey2);
|
|
3055
3328
|
}
|
|
3056
3329
|
return publicEncrypt(
|
|
3057
3330
|
{
|
|
@@ -3322,16 +3595,40 @@ async function agentWalletSignerFromEnv(env = process.env) {
|
|
|
3322
3595
|
`unknown SUBLY_SIGNER_PROVIDER "${provider}" (expected local, circle, or privy)`
|
|
3323
3596
|
);
|
|
3324
3597
|
}
|
|
3598
|
+
async function signerBundleForVault(bundle2, vault) {
|
|
3599
|
+
const signer2 = bundle2.provider === "local" ? new LocalKeypairAgentWalletSigner(await createKeyPairSignerFromBytes2(bundle2.localSecretKey), { vault }) : new RemoteAgentWalletSigner(bundle2.transport, { vault });
|
|
3600
|
+
return { ...bundle2, signer: signer2 };
|
|
3601
|
+
}
|
|
3325
3602
|
|
|
3326
3603
|
// ../../src/client/standard-x402-state-store.ts
|
|
3327
|
-
import { mkdirSync, readFileSync as
|
|
3604
|
+
import { closeSync, mkdirSync, openSync, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3328
3605
|
import { basename, dirname, join } from "node:path";
|
|
3329
3606
|
function fileStandardX402StateStore(path) {
|
|
3330
3607
|
return {
|
|
3608
|
+
async withExclusiveLock(operation) {
|
|
3609
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
3610
|
+
const lockPath = `${path}.lock`;
|
|
3611
|
+
let fd;
|
|
3612
|
+
try {
|
|
3613
|
+
fd = openSync(lockPath, "wx", 384);
|
|
3614
|
+
} catch (error) {
|
|
3615
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
|
|
3616
|
+
throw new Error(`Payment state is locked: ${lockPath}. Another client may be paying. After a crash, stop all clients before removing only this .lock file; preserve the payment state JSON.`);
|
|
3617
|
+
}
|
|
3618
|
+
throw error;
|
|
3619
|
+
}
|
|
3620
|
+
try {
|
|
3621
|
+
writeFileSync(fd, JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
3622
|
+
return await operation();
|
|
3623
|
+
} finally {
|
|
3624
|
+
closeSync(fd);
|
|
3625
|
+
unlinkSync(lockPath);
|
|
3626
|
+
}
|
|
3627
|
+
},
|
|
3331
3628
|
load() {
|
|
3332
3629
|
let text;
|
|
3333
3630
|
try {
|
|
3334
|
-
text =
|
|
3631
|
+
text = readFileSync3(path, "utf8");
|
|
3335
3632
|
} catch (error) {
|
|
3336
3633
|
if (isMissingFileError(error)) {
|
|
3337
3634
|
return [];
|
|
@@ -3353,12 +3650,12 @@ function fileStandardX402StateStore(path) {
|
|
|
3353
3650
|
},
|
|
3354
3651
|
save(records) {
|
|
3355
3652
|
const directory = dirname(path);
|
|
3356
|
-
mkdirSync(directory, { recursive: true });
|
|
3653
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
3357
3654
|
const tempPath = join(
|
|
3358
3655
|
directory,
|
|
3359
3656
|
`.${basename(path)}.${process.pid}.${Date.now()}.tmp`
|
|
3360
3657
|
);
|
|
3361
|
-
writeFileSync(tempPath, JSON.stringify(records, null, 2));
|
|
3658
|
+
writeFileSync(tempPath, JSON.stringify(records, null, 2), { mode: 384, flag: "wx" });
|
|
3362
3659
|
renameSync(tempPath, path);
|
|
3363
3660
|
}
|
|
3364
3661
|
};
|
|
@@ -3394,7 +3691,7 @@ async function svmTransactionSignerFromBundle(bundle2) {
|
|
|
3394
3691
|
}
|
|
3395
3692
|
function remoteSvmTransactionSigner(transport) {
|
|
3396
3693
|
const signerAddress = address2(transport.walletAddress);
|
|
3397
|
-
const
|
|
3694
|
+
const publicKey2 = ed25519PublicKeyBytes(
|
|
3398
3695
|
transport.provider,
|
|
3399
3696
|
transport.walletAddress
|
|
3400
3697
|
);
|
|
@@ -3407,7 +3704,7 @@ function remoteSvmTransactionSigner(transport) {
|
|
|
3407
3704
|
transport,
|
|
3408
3705
|
serializedTransactionBase64: getBase64EncodedWireTransaction2(transaction),
|
|
3409
3706
|
messageBytes: transaction.messageBytes,
|
|
3410
|
-
publicKey
|
|
3707
|
+
publicKey: publicKey2
|
|
3411
3708
|
});
|
|
3412
3709
|
dictionaries.push(
|
|
3413
3710
|
Object.freeze({ [signerAddress]: signature })
|
|
@@ -3467,14 +3764,27 @@ var payer = createRelayerX402Payer({
|
|
|
3467
3764
|
defaultMaxAmountRawUsdc,
|
|
3468
3765
|
stateStore: fileStandardX402StateStore(pendingStatePath)
|
|
3469
3766
|
});
|
|
3767
|
+
var catalog = vaultCatalogFromEnv();
|
|
3768
|
+
var sessions = [];
|
|
3769
|
+
for (const vault of catalog.vaults) {
|
|
3770
|
+
const selectedBundle = await signerBundleForVault(bundle, vault);
|
|
3771
|
+
const vaultSigner = selectedBundle.signer;
|
|
3772
|
+
const realizer = new RelayerYieldRealizer({ relayerBaseUrl, signer: vaultSigner, rpc });
|
|
3773
|
+
sessions.push({
|
|
3774
|
+
vault,
|
|
3775
|
+
signer: vaultSigner,
|
|
3776
|
+
// One payer owns deduplication and pending state across ALL vault selections.
|
|
3777
|
+
payer: { pay: (input) => payer.pay(input, realizer) },
|
|
3778
|
+
vaultFlows: new VaultFlowClient({ relayerBaseUrl, signer: vaultSigner, rpc })
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3781
|
+
var vaultSelection = new McpVaultSelection(sessions, catalog.defaultVault);
|
|
3782
|
+
var selected = vaultSelection.current();
|
|
3470
3783
|
await runMcpPaymentServer({
|
|
3471
|
-
payer,
|
|
3472
|
-
signer,
|
|
3784
|
+
payer: selected.payer,
|
|
3785
|
+
signer: selected.signer,
|
|
3473
3786
|
relayerBaseUrl,
|
|
3474
3787
|
defaultMaxAmountRawUsdc,
|
|
3475
|
-
vaultFlows:
|
|
3476
|
-
|
|
3477
|
-
signer,
|
|
3478
|
-
rpc
|
|
3479
|
-
})
|
|
3788
|
+
vaultFlows: selected.vaultFlows,
|
|
3789
|
+
vaultSelection
|
|
3480
3790
|
});
|