@subly_fi/pay 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +98 -61
- package/dist/budget.js +2267 -0
- package/dist/cli.js +37 -20
- package/dist/deposit.js +1031 -183
- package/dist/doctor.js +188 -0
- package/dist/mcp-server.js +3404 -2415
- package/dist/pay.js +2764 -1873
- package/dist/setup-link.js +950 -104
- package/dist/vaults.js +129 -0
- package/dist/withdraw.js +950 -104
- package/package.json +25 -6
package/dist/pay.js
CHANGED
|
@@ -2,104 +2,238 @@
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join as join2 } from "node:path";
|
|
4
4
|
|
|
5
|
-
// ../../src/
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
// ../../src/solana/tx.ts
|
|
10
|
-
import bs58 from "bs58";
|
|
11
|
-
import {
|
|
12
|
-
appendTransactionMessageInstructions,
|
|
13
|
-
compileTransaction,
|
|
14
|
-
compressTransactionMessageUsingAddressLookupTables,
|
|
15
|
-
createTransactionMessage,
|
|
16
|
-
getBase64EncodedWireTransaction,
|
|
17
|
-
getTransactionDecoder,
|
|
18
|
-
partiallySignTransaction,
|
|
19
|
-
pipe,
|
|
20
|
-
setTransactionMessageFeePayer,
|
|
21
|
-
setTransactionMessageLifetimeUsingBlockhash
|
|
22
|
-
} from "@solana/kit";
|
|
5
|
+
// ../../src/config/vault-catalog.ts
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { z } from "zod";
|
|
23
8
|
|
|
24
|
-
// ../../src/lib/
|
|
25
|
-
|
|
26
|
-
var
|
|
27
|
-
|
|
28
|
-
|
|
9
|
+
// ../../src/lib/solana-address.ts
|
|
10
|
+
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
11
|
+
var BASE58_LOOKUP = new Map(
|
|
12
|
+
[...BASE58_ALPHABET].map((character, index) => [character, BigInt(index)])
|
|
13
|
+
);
|
|
14
|
+
function assertSolanaAddress(value, fieldName) {
|
|
15
|
+
if (value.length < 32 || value.length > 44) {
|
|
16
|
+
throw new Error(`${fieldName} must be a valid Solana public key`);
|
|
17
|
+
}
|
|
18
|
+
if (decodeBase58(value).length !== 32) {
|
|
19
|
+
throw new Error(`${fieldName} must be a valid Solana public key`);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
29
22
|
}
|
|
30
|
-
function
|
|
31
|
-
if (value ===
|
|
32
|
-
return
|
|
23
|
+
function decodeBase58(value) {
|
|
24
|
+
if (value.length === 0) {
|
|
25
|
+
return new Uint8Array();
|
|
33
26
|
}
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
let decoded = 0n;
|
|
28
|
+
for (const character of value) {
|
|
29
|
+
const digit = BASE58_LOOKUP.get(character);
|
|
30
|
+
if (digit === void 0) {
|
|
31
|
+
return new Uint8Array();
|
|
32
|
+
}
|
|
33
|
+
decoded = decoded * 58n + digit;
|
|
36
34
|
}
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
const bytes = [];
|
|
36
|
+
while (decoded > 0n) {
|
|
37
|
+
bytes.push(Number(decoded & 0xffn));
|
|
38
|
+
decoded >>= 8n;
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
for (const character of value) {
|
|
41
|
+
if (character !== "1") {
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
bytes.push(0);
|
|
42
45
|
}
|
|
43
|
-
|
|
44
|
-
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
45
|
-
}
|
|
46
|
-
function hashStableJson(value) {
|
|
47
|
-
return sha256TaggedHex(stableStringify(value));
|
|
46
|
+
return Uint8Array.from(bytes.reverse());
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
// ../../src/
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
// ../../src/config/vault.ts
|
|
50
|
+
var MAINNET_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
51
|
+
var KAMINO_VAULT_PROGRAM_ID = "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd";
|
|
52
|
+
var NO_VAULT_FARM = "11111111111111111111111111111111";
|
|
53
|
+
var DEFAULT_VAULT_CONFIG = Object.freeze({
|
|
54
|
+
address: "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr",
|
|
55
|
+
programId: KAMINO_VAULT_PROGRAM_ID,
|
|
56
|
+
usdcMint: MAINNET_USDC_MINT,
|
|
57
|
+
shareMint: "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a",
|
|
58
|
+
farm: "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
|
|
59
|
+
});
|
|
60
|
+
function vaultConfigFromEnv(env = process.env) {
|
|
61
|
+
const value = (name) => env[name]?.trim() || void 0;
|
|
62
|
+
const vaultAddress = value("SUBLY_VAULT_ADDRESS") ?? DEFAULT_VAULT_CONFIG.address;
|
|
63
|
+
const customVault = vaultAddress !== DEFAULT_VAULT_CONFIG.address;
|
|
64
|
+
const anchor = (name, fallback) => {
|
|
65
|
+
const configured = value(name);
|
|
66
|
+
if (customVault && configured === void 0) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`${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.`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return assertSolanaAddress(configured ?? fallback, name);
|
|
60
72
|
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
const usdcMint = value("SUBLY_VAULT_USDC_MINT") ?? MAINNET_USDC_MINT;
|
|
74
|
+
if (usdcMint !== MAINNET_USDC_MINT) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"SUBLY_VAULT_USDC_MINT must be mainnet USDC; other deposit assets are not supported"
|
|
77
|
+
);
|
|
66
78
|
}
|
|
67
|
-
return
|
|
79
|
+
return Object.freeze({
|
|
80
|
+
address: assertSolanaAddress(vaultAddress, "SUBLY_VAULT_ADDRESS"),
|
|
81
|
+
programId: KAMINO_VAULT_PROGRAM_ID,
|
|
82
|
+
usdcMint,
|
|
83
|
+
shareMint: anchor("SUBLY_VAULT_SHARE_MINT", DEFAULT_VAULT_CONFIG.shareMint),
|
|
84
|
+
farm: anchor("SUBLY_VAULT_FARM", DEFAULT_VAULT_CONFIG.farm)
|
|
85
|
+
});
|
|
68
86
|
}
|
|
69
87
|
|
|
70
|
-
// ../../src/
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
// ../../src/config/vault-catalog.ts
|
|
89
|
+
var publicKey = z.string().refine((value) => {
|
|
90
|
+
try {
|
|
91
|
+
assertSolanaAddress(value, "vault catalog address");
|
|
92
|
+
return true;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}, "Invalid Solana public key");
|
|
97
|
+
var catalogSchema = z.object({
|
|
98
|
+
version: z.literal(1),
|
|
99
|
+
defaultVault: publicKey,
|
|
100
|
+
vaults: z.array(z.object({
|
|
101
|
+
address: publicKey,
|
|
102
|
+
programId: z.literal(KAMINO_VAULT_PROGRAM_ID),
|
|
103
|
+
usdcMint: z.literal(MAINNET_USDC_MINT),
|
|
104
|
+
shareMint: publicKey,
|
|
105
|
+
farm: publicKey,
|
|
106
|
+
name: z.string().min(1).max(128).optional(),
|
|
107
|
+
depositsEnabled: z.boolean().optional(),
|
|
108
|
+
extraLookupTables: z.array(publicKey).max(16).optional()
|
|
109
|
+
}).strict()).min(1).max(100)
|
|
110
|
+
}).strict();
|
|
111
|
+
function parseVaultCatalog(value) {
|
|
112
|
+
const catalog = catalogSchema.parse(value);
|
|
113
|
+
const addresses = new Set(catalog.vaults.map((vault) => vault.address));
|
|
114
|
+
if (addresses.size !== catalog.vaults.length) throw new Error("Duplicate vault in catalog");
|
|
115
|
+
if (!addresses.has(catalog.defaultVault)) throw new Error("defaultVault must be in the vault catalog");
|
|
116
|
+
return { ...catalog, vaults: catalog.vaults.map((vault) => Object.freeze(vault)) };
|
|
117
|
+
}
|
|
118
|
+
function vaultCatalogFromEnv(env = process.env) {
|
|
119
|
+
const path = env.SUBLY_VAULTS_FILE?.trim();
|
|
120
|
+
if (!path) {
|
|
121
|
+
const vault = vaultConfigFromEnv(env);
|
|
122
|
+
return { version: 1, defaultVault: vault.address, vaults: [vault] };
|
|
123
|
+
}
|
|
124
|
+
const catalog = parseVaultCatalog(JSON.parse(readFileSync(path, "utf8")));
|
|
125
|
+
const selected = env.SUBLY_VAULT_ADDRESS?.trim() || catalog.defaultVault;
|
|
126
|
+
if (!catalog.vaults.some((vault) => vault.address === selected)) {
|
|
127
|
+
throw new Error("SUBLY_VAULT_ADDRESS must be in SUBLY_VAULTS_FILE");
|
|
128
|
+
}
|
|
129
|
+
return { ...catalog, defaultVault: selected };
|
|
130
|
+
}
|
|
131
|
+
function defaultCatalogVault(catalog) {
|
|
132
|
+
return catalog.vaults.find((vault) => vault.address === catalog.defaultVault);
|
|
133
|
+
}
|
|
73
134
|
|
|
74
135
|
// ../../src/config/constants.ts
|
|
75
136
|
var PAYMENT_SCHEME = "subly-yield-exact";
|
|
76
137
|
var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
77
138
|
var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
78
139
|
var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
79
|
-
var SUBLY_VAULT =
|
|
80
|
-
name: "Subly USDC Payment Vault Alpha",
|
|
81
|
-
address: "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr",
|
|
82
|
-
programId: "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd",
|
|
83
|
-
usdcMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
84
|
-
shareMint: "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a",
|
|
85
|
-
lookupTable: "7UbXhDnpK7WVnwsfivzQRENoqKqAULQ5s19gS1xJrQEo",
|
|
86
|
-
farm: "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
|
|
87
|
-
};
|
|
140
|
+
var SUBLY_VAULT = defaultCatalogVault(vaultCatalogFromEnv());
|
|
88
141
|
var USDC_DECIMALS = 6;
|
|
89
142
|
|
|
90
|
-
// ../../src/
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
143
|
+
// ../../src/api/wallet-auth.ts
|
|
144
|
+
import { createHash } from "node:crypto";
|
|
145
|
+
import bs58 from "bs58";
|
|
146
|
+
import nacl from "tweetnacl";
|
|
147
|
+
var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
|
|
148
|
+
var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
|
|
149
|
+
var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
|
|
150
|
+
function sha256Hex(data) {
|
|
151
|
+
return createHash("sha256").update(data, "utf8").digest("hex");
|
|
152
|
+
}
|
|
153
|
+
function walletAuthMessage(params) {
|
|
154
|
+
return new TextEncoder().encode(
|
|
155
|
+
`subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
|
|
156
|
+
params.rawBody
|
|
157
|
+
)}:${params.signedAtMs}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ../../src/client/wallet-auth-headers.ts
|
|
162
|
+
async function walletAuthHeaders(params) {
|
|
163
|
+
const signedAtMs = String(Date.now());
|
|
164
|
+
const message = walletAuthMessage({
|
|
165
|
+
method: params.method,
|
|
166
|
+
path: (() => {
|
|
167
|
+
const url2 = new URL(params.url);
|
|
168
|
+
return url2.pathname + url2.search;
|
|
169
|
+
})(),
|
|
170
|
+
rawBody: params.body ?? "",
|
|
171
|
+
signedAtMs
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
[WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
|
|
175
|
+
[WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
|
|
176
|
+
[WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ../../src/client/onboarding.ts
|
|
181
|
+
var SELF_SERVE_POLICY_ID = "self-serve";
|
|
182
|
+
var OnboardingError = class extends Error {
|
|
183
|
+
constructor(step, message, detail = null) {
|
|
184
|
+
super(message);
|
|
185
|
+
this.step = step;
|
|
186
|
+
this.detail = detail;
|
|
187
|
+
this.name = "OnboardingError";
|
|
188
|
+
}
|
|
189
|
+
step;
|
|
190
|
+
detail;
|
|
191
|
+
};
|
|
192
|
+
async function ensureWalletOnboarded(params) {
|
|
193
|
+
const fetchImpl = params.fetchImpl ?? fetch;
|
|
194
|
+
const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
|
|
195
|
+
const post = async (step, path, body2) => {
|
|
196
|
+
const url2 = `${baseUrl}${path}`;
|
|
197
|
+
const serialized = JSON.stringify(body2);
|
|
198
|
+
const response = await fetchImpl(url2, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: {
|
|
201
|
+
...await walletAuthHeaders({
|
|
202
|
+
signer: params.signer,
|
|
203
|
+
method: "POST",
|
|
204
|
+
url: url2,
|
|
205
|
+
body: serialized
|
|
206
|
+
}),
|
|
207
|
+
"content-type": "application/json"
|
|
208
|
+
},
|
|
209
|
+
body: serialized
|
|
210
|
+
});
|
|
211
|
+
if (response.status !== 200) {
|
|
212
|
+
let detail = null;
|
|
213
|
+
try {
|
|
214
|
+
detail = await response.json();
|
|
215
|
+
} catch {
|
|
216
|
+
detail = null;
|
|
217
|
+
}
|
|
218
|
+
throw new OnboardingError(
|
|
219
|
+
step,
|
|
220
|
+
`wallet onboarding ${step} failed with ${response.status}`,
|
|
221
|
+
detail
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const wallet = params.signer.walletAddress;
|
|
226
|
+
const vault = params.vault ?? params.signer.vault?.address ?? SUBLY_VAULT.address;
|
|
227
|
+
await post("register", "/v1/wallets/agent", {
|
|
228
|
+
wallet,
|
|
229
|
+
vault,
|
|
230
|
+
signingPolicyId: SELF_SERVE_POLICY_ID,
|
|
231
|
+
signingMode: "non_interactive",
|
|
232
|
+
signerValidationMode: params.signer.validationMode,
|
|
233
|
+
signerProvider: params.signer.provider ?? "local-keypair",
|
|
234
|
+
activateForPayments: true
|
|
102
235
|
});
|
|
236
|
+
await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain", vault });
|
|
103
237
|
}
|
|
104
238
|
|
|
105
239
|
// ../../src/lib/associated-token-account.ts
|
|
@@ -120,12 +254,12 @@ function deriveAssociatedTokenAddress(params) {
|
|
|
120
254
|
"associatedTokenProgramId"
|
|
121
255
|
);
|
|
122
256
|
for (let bump = 255; bump >= 0; bump -= 1) {
|
|
123
|
-
const
|
|
257
|
+
const address3 = createProgramAddress(
|
|
124
258
|
[owner, tokenProgramId, mint, Uint8Array.of(bump)],
|
|
125
259
|
associatedTokenProgramId
|
|
126
260
|
);
|
|
127
|
-
if (
|
|
128
|
-
return bs582.encode(
|
|
261
|
+
if (address3 !== null) {
|
|
262
|
+
return bs582.encode(address3);
|
|
129
263
|
}
|
|
130
264
|
}
|
|
131
265
|
throw new Error("Unable to derive associated token account address");
|
|
@@ -194,1931 +328,2702 @@ function modPow(base, exponent, modulus) {
|
|
|
194
328
|
return result;
|
|
195
329
|
}
|
|
196
330
|
|
|
197
|
-
// ../../src/client/
|
|
198
|
-
var
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
225,
|
|
231
|
-
242,
|
|
232
|
-
182
|
|
233
|
-
]);
|
|
234
|
-
var U64_MAX = 18446744073709551615n;
|
|
235
|
-
var MAX_TEMP_ACCOUNT_LAMPORTS = 10000000n;
|
|
236
|
-
var DEFAULT_MAX_COMPUTE_UNIT_LIMIT = 14e5;
|
|
237
|
-
var DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS = 100000n;
|
|
238
|
-
var IntentValidationError = class extends Error {
|
|
239
|
-
reason;
|
|
240
|
-
constructor(reason, message) {
|
|
241
|
-
super(message);
|
|
242
|
-
this.name = "IntentValidationError";
|
|
243
|
-
this.reason = reason;
|
|
331
|
+
// ../../src/client/withdrawal-preview.ts
|
|
332
|
+
var ROUNDING_RAW_USDC = 10n;
|
|
333
|
+
async function assertWithdrawalPreview(input) {
|
|
334
|
+
const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
|
|
335
|
+
const simulation = await input.rpc.simulateTransaction(
|
|
336
|
+
input.serializedTransaction,
|
|
337
|
+
{
|
|
338
|
+
encoding: "base64",
|
|
339
|
+
commitment: "confirmed",
|
|
340
|
+
sigVerify: false,
|
|
341
|
+
replaceRecentBlockhash: false,
|
|
342
|
+
innerInstructions: true
|
|
343
|
+
}
|
|
344
|
+
).send({ abortSignal: AbortSignal.timeout(15e3) });
|
|
345
|
+
if (simulation.value.err !== null) {
|
|
346
|
+
throw new Error("Withdrawal preview failed on the client RPC; no transaction was signed. Check liquidity, RPC and blockhash, then prepare again.");
|
|
347
|
+
}
|
|
348
|
+
let received = 0n;
|
|
349
|
+
for (const group of simulation.value.innerInstructions ?? []) {
|
|
350
|
+
for (const instruction of group.instructions) {
|
|
351
|
+
if (!("parsed" in instruction) || instruction.programId !== SPL_TOKEN_PROGRAM_ID) continue;
|
|
352
|
+
const parsed = instruction.parsed;
|
|
353
|
+
if (parsed.type !== "transfer" && parsed.type !== "transferChecked") continue;
|
|
354
|
+
const info = parsed.info;
|
|
355
|
+
if (!info || info.destination !== destination && info.source !== destination) continue;
|
|
356
|
+
const raw = parsed.type === "transferChecked" ? info.tokenAmount?.amount : info.amount;
|
|
357
|
+
if (typeof raw !== "string" || !/^\d+$/.test(raw)) {
|
|
358
|
+
throw new Error("Withdrawal preview returned an invalid token amount");
|
|
359
|
+
}
|
|
360
|
+
const amount = BigInt(raw);
|
|
361
|
+
if (info.destination === destination) received += amount;
|
|
362
|
+
if (info.source === destination) received -= amount;
|
|
363
|
+
}
|
|
244
364
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
throw new IntentValidationError(reason, message);
|
|
248
|
-
}
|
|
249
|
-
function decodeIntentTransaction(params) {
|
|
250
|
-
const wire = Buffer.from(params.serializedTransaction, "base64");
|
|
251
|
-
const signatureCount = readShortVec(wire, 0);
|
|
252
|
-
if (signatureCount === null) {
|
|
253
|
-
reject("invalid_transaction_encoding", "Cannot parse signature count");
|
|
365
|
+
if (received <= 0n || received > input.amountRawUsdc + ROUNDING_RAW_USDC || received < input.amountRawUsdc - ROUNDING_RAW_USDC) {
|
|
366
|
+
throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
|
|
254
367
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ../../src/client/lookup-tables.ts
|
|
371
|
+
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
|
|
372
|
+
import { address, getCompiledTransactionMessageDecoder } from "@solana/kit";
|
|
373
|
+
function lookupTableAddressesForTransaction(serializedTransaction) {
|
|
374
|
+
const wire = Buffer.from(serializedTransaction, "base64");
|
|
375
|
+
let offset = 0;
|
|
376
|
+
let signatureCount = 0;
|
|
377
|
+
let shift = 0;
|
|
378
|
+
while (offset < wire.length) {
|
|
379
|
+
const byte = wire[offset];
|
|
380
|
+
signatureCount |= (byte & 127) << shift;
|
|
381
|
+
offset += 1;
|
|
382
|
+
if ((byte & 128) === 0) {
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
shift += 7;
|
|
258
386
|
}
|
|
259
|
-
const messageBytes = wire.subarray(
|
|
387
|
+
const messageBytes = wire.subarray(offset + signatureCount * 64);
|
|
260
388
|
const compiled = getCompiledTransactionMessageDecoder().decode(messageBytes);
|
|
261
|
-
if (compiled.version !== 0) {
|
|
262
|
-
reject("unsupported_transaction_version", "Only v0 transactions are supported");
|
|
263
|
-
}
|
|
264
|
-
const staticAccounts = compiled.staticAccounts.map(String);
|
|
265
|
-
const loadedWritable = [];
|
|
266
|
-
const loadedReadonly = [];
|
|
267
389
|
const lookups = compiled.addressTableLookups ?? [];
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
`Transaction references unknown lookup table ${lookup.lookupTableAddress}`
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
const writableIndexes = lookup.writableIndexes ?? lookup.writableIndices ?? [];
|
|
278
|
-
const readonlyIndexes = lookup.readonlyIndexes ?? lookup.readableIndices ?? [];
|
|
279
|
-
for (const index of writableIndexes) {
|
|
280
|
-
const resolved = table[index];
|
|
281
|
-
if (resolved === void 0) {
|
|
282
|
-
reject("lookup_table_unresolved", "Lookup table index out of range");
|
|
283
|
-
}
|
|
284
|
-
loadedWritable.push(String(resolved));
|
|
285
|
-
}
|
|
286
|
-
for (const index of readonlyIndexes) {
|
|
287
|
-
const resolved = table[index];
|
|
288
|
-
if (resolved === void 0) {
|
|
289
|
-
reject("lookup_table_unresolved", "Lookup table index out of range");
|
|
290
|
-
}
|
|
291
|
-
loadedReadonly.push(String(resolved));
|
|
292
|
-
}
|
|
390
|
+
return lookups.map((lookup) => String(lookup.lookupTableAddress));
|
|
391
|
+
}
|
|
392
|
+
async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
|
|
393
|
+
const addresses = lookupTableAddressesForTransaction(serializedTransaction);
|
|
394
|
+
if (addresses.length === 0) {
|
|
395
|
+
return {};
|
|
293
396
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
(
|
|
297
|
-
const programAddress = orderedAccounts[instruction.programAddressIndex];
|
|
298
|
-
if (programAddress === void 0) {
|
|
299
|
-
reject("invalid_transaction_encoding", "Program index out of range");
|
|
300
|
-
}
|
|
301
|
-
const accounts = (instruction.accountIndices ?? []).map((index) => {
|
|
302
|
-
const account = orderedAccounts[index];
|
|
303
|
-
if (account === void 0) {
|
|
304
|
-
reject("invalid_transaction_encoding", "Account index out of range");
|
|
305
|
-
}
|
|
306
|
-
return account;
|
|
307
|
-
});
|
|
308
|
-
return {
|
|
309
|
-
programAddress,
|
|
310
|
-
accounts,
|
|
311
|
-
data: instruction.data === void 0 ? new Uint8Array() : Uint8Array.from(instruction.data)
|
|
312
|
-
};
|
|
313
|
-
}
|
|
397
|
+
const tables = await fetchAllMaybeAddressLookupTable(
|
|
398
|
+
rpc2,
|
|
399
|
+
addresses.map((value) => address(value))
|
|
314
400
|
);
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
401
|
+
const result = {};
|
|
402
|
+
for (const table of tables) {
|
|
403
|
+
if (table.exists) {
|
|
404
|
+
result[table.address] = table.data.addresses.map(String);
|
|
405
|
+
}
|
|
318
406
|
}
|
|
319
|
-
return
|
|
320
|
-
feePayer,
|
|
321
|
-
requiredSigners: staticAccounts.slice(0, compiled.header.numSignerAccounts),
|
|
322
|
-
instructions,
|
|
323
|
-
messageHash: sha256TaggedHex(Buffer.from(messageBytes))
|
|
324
|
-
};
|
|
407
|
+
return result;
|
|
325
408
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if (intent.network !== SOLANA_MAINNET_NETWORK) {
|
|
337
|
-
reject("network_mismatch", "Unsupported network");
|
|
338
|
-
}
|
|
339
|
-
if (intent.vault !== SUBLY_VAULT.address) {
|
|
340
|
-
reject("vault_mismatch", "Unsupported vault");
|
|
341
|
-
}
|
|
342
|
-
if (intent.shareMint !== SUBLY_VAULT.shareMint) {
|
|
343
|
-
reject("share_mint_mismatch", "Unsupported share mint");
|
|
344
|
-
}
|
|
345
|
-
if (intent.asset !== SUBLY_VAULT.usdcMint) {
|
|
346
|
-
reject("asset_mismatch", "Only USDC payments are supported");
|
|
409
|
+
|
|
410
|
+
// ../../src/client/vault-flows.ts
|
|
411
|
+
var VaultFlowClientError = class extends Error {
|
|
412
|
+
constructor(step, message, detail = null, code = null, errorDetails = null) {
|
|
413
|
+
super(message);
|
|
414
|
+
this.step = step;
|
|
415
|
+
this.detail = detail;
|
|
416
|
+
this.code = code;
|
|
417
|
+
this.errorDetails = errorDetails;
|
|
418
|
+
this.name = "VaultFlowClientError";
|
|
347
419
|
}
|
|
348
|
-
|
|
349
|
-
|
|
420
|
+
step;
|
|
421
|
+
detail;
|
|
422
|
+
code;
|
|
423
|
+
errorDetails;
|
|
424
|
+
};
|
|
425
|
+
var VaultFlowClient = class {
|
|
426
|
+
vault;
|
|
427
|
+
rpc;
|
|
428
|
+
baseUrl;
|
|
429
|
+
signer;
|
|
430
|
+
fetchImpl;
|
|
431
|
+
lookupTablesFor;
|
|
432
|
+
pollTimeoutMs;
|
|
433
|
+
pollIntervalMs;
|
|
434
|
+
constructor(config) {
|
|
435
|
+
this.rpc = config.rpc;
|
|
436
|
+
this.vault = config.vault ?? config.signer.vault ?? SUBLY_VAULT;
|
|
437
|
+
if (config.signer.vault && config.signer.vault.address !== this.vault.address) {
|
|
438
|
+
throw new Error("Vault flow client and signer must select the same vault");
|
|
439
|
+
}
|
|
440
|
+
this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
|
|
441
|
+
this.signer = config.signer;
|
|
442
|
+
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
443
|
+
this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
|
|
444
|
+
this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
|
|
445
|
+
this.pollIntervalMs = config.pollIntervalMs ?? 2500;
|
|
350
446
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
447
|
+
/**
|
|
448
|
+
* Moves USDC from the agent wallet into the vault (fee sponsored). Under
|
|
449
|
+
* depositPolicy "owner_approval_required" the relayer refuses to prepare
|
|
450
|
+
* without an owner approval; when the caller passes none, an already
|
|
451
|
+
* APPROVED deposit approval for this exact amount (e.g. the mandate's
|
|
452
|
+
* initialDeposit — "one Face ID covers mandate + first deposit") is looked
|
|
453
|
+
* up and used automatically before surfacing deposit_approval_required.
|
|
454
|
+
*/
|
|
455
|
+
async deposit(input) {
|
|
456
|
+
let approvalId2 = input.approvalId;
|
|
457
|
+
let prepared;
|
|
458
|
+
try {
|
|
459
|
+
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
460
|
+
wallet: this.signer.walletAddress,
|
|
461
|
+
vault: this.vault.address,
|
|
462
|
+
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
463
|
+
...approvalId2 === void 0 ? {} : { approvalId: approvalId2 }
|
|
464
|
+
});
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId2 !== void 0) {
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
469
|
+
approvalId2 = await this.findApprovedDepositApproval(input.amountRawUsdc);
|
|
470
|
+
if (approvalId2 === void 0) {
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
474
|
+
wallet: this.signer.walletAddress,
|
|
475
|
+
vault: this.vault.address,
|
|
476
|
+
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
477
|
+
approvalId: approvalId2
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.signingIntent.amountRawUsdc !== input.amountRawUsdc.toString()) {
|
|
481
|
+
throw new VaultFlowClientError("prepare", "Prepared deposit differs from the requested wallet, vault or amount");
|
|
482
|
+
}
|
|
483
|
+
const signed = await this.signer.signDeposit({
|
|
484
|
+
intent: prepared.signingIntent,
|
|
485
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
486
|
+
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
487
|
+
});
|
|
488
|
+
let outcome = await this.postJson("submit", "/v1/deposits/submit", {
|
|
489
|
+
depositId: prepared.depositId,
|
|
490
|
+
serializedTransaction: signed.serializedTransaction,
|
|
491
|
+
agentSignature: signed.agentSignature
|
|
492
|
+
});
|
|
493
|
+
if (outcome.status === "submitted") {
|
|
494
|
+
outcome = await this.pollUntilTerminal(
|
|
495
|
+
`/v1/deposits/${prepared.depositId}`,
|
|
496
|
+
outcome
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
depositId: prepared.depositId,
|
|
501
|
+
status: outcome.status,
|
|
502
|
+
txSignature: outcome.txSignature ?? null,
|
|
503
|
+
actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
|
|
504
|
+
sharesMintedRaw: outcome.sharesMintedRaw ?? null,
|
|
505
|
+
errorCode: outcome.errorCode ?? null
|
|
506
|
+
};
|
|
367
507
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
508
|
+
/**
|
|
509
|
+
* Moves USDC from the vault back to the agent wallet's USDC ATA (fee
|
|
510
|
+
* sponsored). A plain withdrawal is the exit path and MAY spend principal;
|
|
511
|
+
* with purpose "yield_realize" the relayer refuses anything beyond the
|
|
512
|
+
* spendable yield (the payment path, via RelayerYieldRealizer).
|
|
513
|
+
*/
|
|
514
|
+
async withdraw(input) {
|
|
515
|
+
const prepared = await this.postJson(
|
|
516
|
+
"prepare",
|
|
517
|
+
"/v1/withdrawals/prepare",
|
|
518
|
+
{
|
|
519
|
+
wallet: this.signer.walletAddress,
|
|
520
|
+
vault: this.vault.address,
|
|
521
|
+
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
522
|
+
...input.purpose === void 0 ? {} : { purpose: input.purpose },
|
|
523
|
+
...input.payment === void 0 ? {} : { payment: input.payment },
|
|
524
|
+
...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
|
|
525
|
+
}
|
|
376
526
|
);
|
|
527
|
+
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) {
|
|
528
|
+
throw new VaultFlowClientError("prepare", "Prepared withdrawal differs from the requested operation");
|
|
529
|
+
}
|
|
530
|
+
await assertWithdrawalPreview({
|
|
531
|
+
rpc: this.rpc,
|
|
532
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
533
|
+
wallet: this.signer.walletAddress,
|
|
534
|
+
vault: this.vault,
|
|
535
|
+
amountRawUsdc: input.amountRawUsdc
|
|
536
|
+
});
|
|
537
|
+
const signed = await this.signer.signWithdrawal({
|
|
538
|
+
intent: prepared.signingIntent,
|
|
539
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
540
|
+
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
541
|
+
});
|
|
542
|
+
let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
|
|
543
|
+
withdrawalId: prepared.withdrawalId,
|
|
544
|
+
serializedTransaction: signed.serializedTransaction,
|
|
545
|
+
agentSignature: signed.agentSignature
|
|
546
|
+
});
|
|
547
|
+
if (outcome.status === "submitted") {
|
|
548
|
+
outcome = await this.pollUntilTerminal(
|
|
549
|
+
`/v1/withdrawals/${prepared.withdrawalId}`,
|
|
550
|
+
outcome
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
withdrawalId: prepared.withdrawalId,
|
|
555
|
+
status: outcome.status,
|
|
556
|
+
txSignature: outcome.txSignature ?? null,
|
|
557
|
+
destinationUsdcAta: prepared.destinationUsdcAta,
|
|
558
|
+
actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
|
|
559
|
+
actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
|
|
560
|
+
errorCode: outcome.errorCode ?? null
|
|
561
|
+
};
|
|
377
562
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
563
|
+
/**
|
|
564
|
+
* Reads the yield budget. Syncs the relayer's ledger from chain first (so
|
|
565
|
+
* yield accrued since the last sync shows up); the sync is best-effort and
|
|
566
|
+
* on failure the last-synced view is returned.
|
|
567
|
+
*/
|
|
568
|
+
async getBudget(options = {}) {
|
|
569
|
+
if (options.refreshFromChain !== false) {
|
|
570
|
+
try {
|
|
571
|
+
await this.postJson(
|
|
572
|
+
"sync",
|
|
573
|
+
`/v1/wallets/${this.signer.walletAddress}/sync`,
|
|
574
|
+
{ source: "chain", vault: this.vault.address }
|
|
575
|
+
);
|
|
576
|
+
} catch {
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const url2 = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget?vault=${this.vault.address}`;
|
|
580
|
+
const response = await this.fetchImpl(url2, {
|
|
581
|
+
headers: await walletAuthHeaders({
|
|
582
|
+
signer: this.signer,
|
|
583
|
+
method: "GET",
|
|
584
|
+
url: url2
|
|
585
|
+
})
|
|
586
|
+
});
|
|
587
|
+
const text = await response.text();
|
|
588
|
+
if (response.status !== 200) {
|
|
589
|
+
throw new VaultFlowClientError(
|
|
590
|
+
"budget",
|
|
591
|
+
`budget endpoint returned ${response.status}: ${text}`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
let parsed;
|
|
595
|
+
try {
|
|
596
|
+
parsed = JSON.parse(text);
|
|
597
|
+
} catch {
|
|
598
|
+
throw new VaultFlowClientError(
|
|
599
|
+
"budget",
|
|
600
|
+
"budget endpoint returned 200 with a non-JSON body",
|
|
601
|
+
text
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
const body2 = parsed;
|
|
605
|
+
if (body2.position?.vault !== void 0 && body2.position.vault !== this.vault.address) {
|
|
606
|
+
throw new VaultFlowClientError("budget", "Relayer returned the budget for a different vault");
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
wallet: this.signer.walletAddress,
|
|
610
|
+
vault: this.vault.address,
|
|
611
|
+
principalBasisRawUsdc: body2.position?.principalBasisRawUsdc ?? "0",
|
|
612
|
+
positionValueRawUsdc: body2.budget?.positionValueRawUsdc ?? "0",
|
|
613
|
+
grossYieldRawUsdc: body2.budget?.grossYieldRawUsdc ?? "0",
|
|
614
|
+
spendableYieldRawUsdc: body2.budget?.spendableYieldRawUsdc ?? "0"
|
|
615
|
+
};
|
|
408
616
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
wallet: intent.wallet,
|
|
416
|
-
vault: intent.vault,
|
|
417
|
-
shareMint: intent.shareMint,
|
|
418
|
-
asset: intent.asset,
|
|
419
|
-
userTokenAccount: intent.temporarySettlementTokenAccount,
|
|
420
|
-
maxSharesToRedeemRaw: BigInt(intent.maxSharesToRedeemRaw),
|
|
421
|
-
allowFullExit: false
|
|
422
|
-
});
|
|
423
|
-
expectTransferChecked(ixs, {
|
|
424
|
-
source: intent.temporarySettlementTokenAccount,
|
|
425
|
-
mint: intent.asset,
|
|
426
|
-
destination: intent.sellerUsdcAta,
|
|
427
|
-
authority: intent.wallet,
|
|
428
|
-
amount: BigInt(intent.amountRawUsdc),
|
|
429
|
-
label: "seller transfer"
|
|
430
|
-
});
|
|
431
|
-
if (ixs[0] !== void 0 && ixs[0].programAddress === SPL_TOKEN_PROGRAM_ID && ixs[0].data[0] === 12) {
|
|
432
|
-
expectTransferChecked(ixs, {
|
|
433
|
-
source: intent.temporarySettlementTokenAccount,
|
|
434
|
-
mint: intent.asset,
|
|
435
|
-
destination: intent.dustRecipientUsdcAta,
|
|
436
|
-
authority: intent.wallet,
|
|
437
|
-
amount: null,
|
|
438
|
-
label: "dust sweep"
|
|
617
|
+
/** Best-effort audit link: reports the x402 payment tx a realize funded. */
|
|
618
|
+
async reportPayment(input) {
|
|
619
|
+
await this.postJson("submit", "/v1/payments/report", {
|
|
620
|
+
wallet: this.signer.walletAddress,
|
|
621
|
+
withdrawalId: input.withdrawalId,
|
|
622
|
+
paymentTxSignature: input.paymentTxSignature
|
|
439
623
|
});
|
|
440
624
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
});
|
|
446
|
-
expectMemo(ixs, intent.memo);
|
|
447
|
-
if (ixs.length > 0) {
|
|
448
|
-
reject(
|
|
449
|
-
"unexpected_instruction",
|
|
450
|
-
`Transaction contains ${ixs.length} unexpected trailing instruction(s)`
|
|
625
|
+
/** Wallet's approvals as the relayer sees them (optionally by status). */
|
|
626
|
+
async listApprovals(status) {
|
|
627
|
+
const body2 = await this.getJson(
|
|
628
|
+
`/v1/wallets/${this.signer.walletAddress}/approvals${`?vault=${this.vault.address}${status === void 0 ? "" : `&status=${encodeURIComponent(status)}`}`}`
|
|
451
629
|
);
|
|
630
|
+
return body2.approvals ?? [];
|
|
452
631
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
632
|
+
/**
|
|
633
|
+
* Creates the owner-onboarding setup link (wallet-auth pins the agreed
|
|
634
|
+
* policy + initial deposit). Paste `setupUrl` into the chat verbatim.
|
|
635
|
+
*/
|
|
636
|
+
async createSetupSession(input) {
|
|
637
|
+
const session = await this.postJson(
|
|
638
|
+
"prepare",
|
|
639
|
+
`/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
|
|
640
|
+
{
|
|
641
|
+
vault: this.vault.address,
|
|
642
|
+
...input.policy === void 0 ? {} : { policy: input.policy },
|
|
643
|
+
...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
|
|
644
|
+
...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
|
|
645
|
+
...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
|
|
646
|
+
}
|
|
647
|
+
);
|
|
648
|
+
if (session.vault !== this.vault.address || session.wallet !== this.signer.walletAddress) {
|
|
649
|
+
throw new VaultFlowClientError("prepare", "Relayer returned a setup session for a different wallet or vault");
|
|
650
|
+
}
|
|
651
|
+
return session;
|
|
460
652
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
653
|
+
/** Polls a setup session (public capability URL — no auth needed). */
|
|
654
|
+
async getSetupSession(sessionId) {
|
|
655
|
+
const url2 = `${this.baseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`;
|
|
656
|
+
const response = await this.fetchImpl(url2);
|
|
657
|
+
const text = await response.text();
|
|
658
|
+
if (response.status !== 200) {
|
|
659
|
+
const parsed = parseRelayerError(text);
|
|
660
|
+
throw new VaultFlowClientError(
|
|
661
|
+
"read",
|
|
662
|
+
parsed.message ?? `setup session read failed with ${response.status}`,
|
|
663
|
+
text,
|
|
664
|
+
parsed.code,
|
|
665
|
+
parsed.details
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
return JSON.parse(text);
|
|
468
669
|
}
|
|
469
|
-
|
|
470
|
-
|
|
670
|
+
/**
|
|
671
|
+
* Finds an APPROVED, unconsumed deposit approval bound to exactly this
|
|
672
|
+
* amount — the shape the mandate's initialDeposit approval has.
|
|
673
|
+
*/
|
|
674
|
+
async findApprovedDepositApproval(amountRawUsdc) {
|
|
675
|
+
try {
|
|
676
|
+
const approvals = await this.listApprovals("approved");
|
|
677
|
+
const match = approvals.find((approval) => {
|
|
678
|
+
const binding = approval.binding;
|
|
679
|
+
return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc.toString();
|
|
680
|
+
});
|
|
681
|
+
return match?.approvalId;
|
|
682
|
+
} catch {
|
|
683
|
+
return void 0;
|
|
684
|
+
}
|
|
471
685
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
if (ix.accounts[5] !== intent.shareMint) {
|
|
501
|
-
reject("share_mint_mismatch", "Deposit share mint mismatch");
|
|
502
|
-
}
|
|
503
|
-
const expectedSourceAta = deriveAssociatedTokenAddress({
|
|
504
|
-
owner: intent.wallet,
|
|
505
|
-
mint: intent.asset
|
|
506
|
-
});
|
|
507
|
-
if (ix.accounts[6] !== expectedSourceAta) {
|
|
508
|
-
reject(
|
|
509
|
-
"source_ata_mismatch",
|
|
510
|
-
"Deposit source must be the agent wallet's USDC ATA"
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
sawDeposit = true;
|
|
514
|
-
break;
|
|
686
|
+
/**
|
|
687
|
+
* Polls the reconciling GET endpoint until the intent leaves "submitted"
|
|
688
|
+
* (each read looks the tx up on-chain) or the timeout elapses.
|
|
689
|
+
*/
|
|
690
|
+
async pollUntilTerminal(path, last) {
|
|
691
|
+
const deadline = Date.now() + this.pollTimeoutMs;
|
|
692
|
+
let latest = last;
|
|
693
|
+
while (Date.now() < deadline) {
|
|
694
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
695
|
+
const url2 = `${this.baseUrl}${path}`;
|
|
696
|
+
const response = await this.fetchImpl(url2, {
|
|
697
|
+
headers: await walletAuthHeaders({
|
|
698
|
+
signer: this.signer,
|
|
699
|
+
method: "GET",
|
|
700
|
+
url: url2
|
|
701
|
+
})
|
|
702
|
+
});
|
|
703
|
+
if (response.status !== 200) {
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
latest = await response.json();
|
|
708
|
+
} catch {
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if (latest.status !== "submitted") {
|
|
712
|
+
return latest;
|
|
515
713
|
}
|
|
516
|
-
default:
|
|
517
|
-
reject(
|
|
518
|
-
"unexpected_instruction",
|
|
519
|
-
`Unexpected program ${ix.programAddress} in deposit transaction`
|
|
520
|
-
);
|
|
521
714
|
}
|
|
715
|
+
return latest;
|
|
522
716
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
717
|
+
async postJson(step, path, body2) {
|
|
718
|
+
const url2 = `${this.baseUrl}${path}`;
|
|
719
|
+
const serialized = JSON.stringify(body2);
|
|
720
|
+
const response = await this.fetchImpl(url2, {
|
|
721
|
+
method: "POST",
|
|
722
|
+
headers: {
|
|
723
|
+
...await walletAuthHeaders({
|
|
724
|
+
signer: this.signer,
|
|
725
|
+
method: "POST",
|
|
726
|
+
url: url2,
|
|
727
|
+
body: serialized
|
|
728
|
+
}),
|
|
729
|
+
"content-type": "application/json"
|
|
730
|
+
},
|
|
731
|
+
body: serialized
|
|
732
|
+
});
|
|
733
|
+
const text = await response.text();
|
|
734
|
+
if (response.status !== 200) {
|
|
735
|
+
const parsed = parseRelayerError(text);
|
|
736
|
+
throw new VaultFlowClientError(
|
|
737
|
+
step,
|
|
738
|
+
parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
|
|
739
|
+
text,
|
|
740
|
+
parsed.code,
|
|
741
|
+
parsed.details
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
return JSON.parse(text);
|
|
746
|
+
} catch {
|
|
747
|
+
throw new VaultFlowClientError(
|
|
748
|
+
step,
|
|
749
|
+
`${path} returned 200 with a non-JSON body`,
|
|
750
|
+
text
|
|
751
|
+
);
|
|
752
|
+
}
|
|
554
753
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
validateKvaultWithdrawInstruction(ix, {
|
|
584
|
-
wallet: intent.wallet,
|
|
585
|
-
vault: intent.vault,
|
|
586
|
-
shareMint: intent.shareMint,
|
|
587
|
-
asset: intent.asset,
|
|
588
|
-
userTokenAccount: intent.destinationUsdcAta,
|
|
589
|
-
maxSharesToRedeemRaw: BigInt(intent.maxSharesToRedeemRaw),
|
|
590
|
-
allowFullExit: intent.allowFullExit
|
|
591
|
-
});
|
|
592
|
-
sawWithdraw = true;
|
|
593
|
-
break;
|
|
594
|
-
}
|
|
595
|
-
default:
|
|
596
|
-
reject(
|
|
597
|
-
"unexpected_instruction",
|
|
598
|
-
`Unexpected program ${ix.programAddress} in withdrawal transaction`
|
|
599
|
-
);
|
|
754
|
+
async getJson(path) {
|
|
755
|
+
const url2 = `${this.baseUrl}${path}`;
|
|
756
|
+
const response = await this.fetchImpl(url2, {
|
|
757
|
+
headers: await walletAuthHeaders({
|
|
758
|
+
signer: this.signer,
|
|
759
|
+
method: "GET",
|
|
760
|
+
url: url2
|
|
761
|
+
})
|
|
762
|
+
});
|
|
763
|
+
const text = await response.text();
|
|
764
|
+
if (response.status !== 200) {
|
|
765
|
+
const parsed = parseRelayerError(text);
|
|
766
|
+
throw new VaultFlowClientError(
|
|
767
|
+
"read",
|
|
768
|
+
parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
|
|
769
|
+
text,
|
|
770
|
+
parsed.code,
|
|
771
|
+
parsed.details
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
return JSON.parse(text);
|
|
776
|
+
} catch {
|
|
777
|
+
throw new VaultFlowClientError(
|
|
778
|
+
"read",
|
|
779
|
+
`${path} returned 200 with a non-JSON body`,
|
|
780
|
+
text
|
|
781
|
+
);
|
|
600
782
|
}
|
|
601
783
|
}
|
|
602
|
-
|
|
603
|
-
|
|
784
|
+
};
|
|
785
|
+
function parseRelayerError(text) {
|
|
786
|
+
try {
|
|
787
|
+
const parsed = JSON.parse(text);
|
|
788
|
+
return {
|
|
789
|
+
code: typeof parsed.error?.code === "string" ? parsed.error.code : null,
|
|
790
|
+
message: typeof parsed.error?.message === "string" ? parsed.error.message : null,
|
|
791
|
+
details: parsed.error?.details ?? null
|
|
792
|
+
};
|
|
793
|
+
} catch {
|
|
794
|
+
return { code: null, message: null, details: null };
|
|
604
795
|
}
|
|
605
796
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
797
|
+
|
|
798
|
+
// ../../src/client/relayer-yield-realizer.ts
|
|
799
|
+
var REALIZE_OVERHEAD_RAW_USDC = 2500n;
|
|
800
|
+
var RelayerRealizeError = class extends Error {
|
|
801
|
+
constructor(code, message, detail = null) {
|
|
802
|
+
super(message);
|
|
803
|
+
this.code = code;
|
|
804
|
+
this.detail = detail;
|
|
805
|
+
this.name = "RelayerRealizeError";
|
|
612
806
|
}
|
|
613
|
-
|
|
614
|
-
|
|
807
|
+
code;
|
|
808
|
+
detail;
|
|
809
|
+
};
|
|
810
|
+
var RelayerYieldRealizer = class {
|
|
811
|
+
get vault() {
|
|
812
|
+
return this.vaultFlows.vault.address;
|
|
615
813
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
814
|
+
vaultFlows;
|
|
815
|
+
constructor(config) {
|
|
816
|
+
this.vaultFlows = new VaultFlowClient({
|
|
817
|
+
relayerBaseUrl: config.relayerBaseUrl,
|
|
818
|
+
signer: config.signer,
|
|
819
|
+
rpc: config.rpc,
|
|
820
|
+
...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
|
|
821
|
+
...config.lookupTablesFor === void 0 ? {} : { lookupTablesFor: config.lookupTablesFor }
|
|
822
|
+
});
|
|
625
823
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
824
|
+
async ensureUsdcAvailable(input) {
|
|
825
|
+
const shortfallRawUsdc = input.amountRawUsdc;
|
|
826
|
+
await this.assertSpendableYield(shortfallRawUsdc);
|
|
827
|
+
let outcome;
|
|
828
|
+
try {
|
|
829
|
+
outcome = await this.vaultFlows.withdraw({
|
|
830
|
+
amountRawUsdc: shortfallRawUsdc,
|
|
831
|
+
// The relayer refuses to prepare this withdrawal beyond the spendable
|
|
832
|
+
// yield — the principal-protection guard the client cannot bypass.
|
|
833
|
+
purpose: "yield_realize",
|
|
834
|
+
// Declares what is being paid so the relayer's spending-mandate layer
|
|
835
|
+
// can enforce caps/payee and keep the mandate → payment audit chain.
|
|
836
|
+
...input.payment === void 0 ? {} : { payment: input.payment },
|
|
837
|
+
...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
|
|
838
|
+
});
|
|
839
|
+
} catch (error) {
|
|
840
|
+
throw this.mapWithdrawError(error);
|
|
841
|
+
}
|
|
842
|
+
if (outcome.status !== "confirmed" || outcome.txSignature === null) {
|
|
843
|
+
throw new RelayerRealizeError(
|
|
844
|
+
"realize_not_confirmed",
|
|
845
|
+
`yield realize withdrawal did not confirm (status=${outcome.status})`,
|
|
846
|
+
outcome
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
|
|
851
|
+
txSignature: outcome.txSignature,
|
|
852
|
+
withdrawalId: outcome.withdrawalId
|
|
853
|
+
};
|
|
631
854
|
}
|
|
632
|
-
|
|
633
|
-
|
|
855
|
+
/**
|
|
856
|
+
* Best-effort report-back of the x402 payment tx this realize funded —
|
|
857
|
+
* closes the relayer's mandate → realize → payment audit chain. Callers
|
|
858
|
+
* must never let a failure here affect the payment result.
|
|
859
|
+
*/
|
|
860
|
+
async reportPayment(input) {
|
|
861
|
+
await this.vaultFlows.reportPayment(input);
|
|
634
862
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
863
|
+
/**
|
|
864
|
+
* Refuses to realize more than the ledger's spendable yield (principal).
|
|
865
|
+
* getBudget syncs the relayer's ledger from chain first (best-effort), so a
|
|
866
|
+
* long-running client sees yield as it accrues instead of a frozen view.
|
|
867
|
+
*/
|
|
868
|
+
async assertSpendableYield(shortfallRawUsdc) {
|
|
869
|
+
let spendable;
|
|
870
|
+
try {
|
|
871
|
+
const budget = await this.vaultFlows.getBudget();
|
|
872
|
+
spendable = BigInt(budget.spendableYieldRawUsdc);
|
|
873
|
+
} catch (error) {
|
|
874
|
+
throw new RelayerRealizeError(
|
|
875
|
+
"budget_unavailable",
|
|
876
|
+
"could not read the spendable-yield budget",
|
|
877
|
+
error
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
|
|
881
|
+
if (spendable < requiredRawUsdc) {
|
|
882
|
+
throw new RelayerRealizeError(
|
|
883
|
+
"insufficient_yield",
|
|
884
|
+
`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`,
|
|
885
|
+
{ spendableYieldRawUsdc: spendable.toString() }
|
|
644
886
|
);
|
|
645
887
|
}
|
|
646
|
-
validateComputeBudgetInstruction(ix, policy);
|
|
647
888
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
"compute_budget_mismatch",
|
|
656
|
-
`Compute unit limit ${units} exceeds policy maximum ${policy.maxComputeUnitLimit}`
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
break;
|
|
889
|
+
mapWithdrawError(error) {
|
|
890
|
+
if (!(error instanceof VaultFlowClientError)) {
|
|
891
|
+
return new RelayerRealizeError(
|
|
892
|
+
"prepare_failed",
|
|
893
|
+
`yield realize failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
894
|
+
error
|
|
895
|
+
);
|
|
660
896
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
}
|
|
669
|
-
break;
|
|
897
|
+
const serverCode = error.code ?? errorCodeFrom(error.detail);
|
|
898
|
+
if (serverCode === "approval_required") {
|
|
899
|
+
return new RelayerRealizeError(
|
|
900
|
+
"approval_required",
|
|
901
|
+
"this payment exceeds the owner-approval threshold; nothing was realized or paid. Ask the owner to approve, then retry with the approvalId",
|
|
902
|
+
error.errorDetails ?? error.detail
|
|
903
|
+
);
|
|
670
904
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const lamports = readU64LE(ix.data, 4);
|
|
684
|
-
const space = readU64LE(ix.data, 12);
|
|
685
|
-
const owner = bs583.encode(ix.data.subarray(20, 52));
|
|
686
|
-
if (space !== 165n) {
|
|
687
|
-
reject("temp_account_mismatch", "Temporary account space must be 165 bytes");
|
|
688
|
-
}
|
|
689
|
-
if (owner !== SPL_TOKEN_PROGRAM_ID) {
|
|
690
|
-
reject("temp_account_mismatch", "Temporary account owner must be the token program");
|
|
691
|
-
}
|
|
692
|
-
if (lamports > policy.maxTemporaryAccountLamports) {
|
|
693
|
-
reject("temp_account_mismatch", "Temporary account rent exceeds the cap");
|
|
905
|
+
if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
|
|
906
|
+
return new RelayerRealizeError(
|
|
907
|
+
"insufficient_yield",
|
|
908
|
+
"the relayer refused to realize beyond the spendable yield; the principal is never spent \u2014 wait for more yield",
|
|
909
|
+
error.detail
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
return new RelayerRealizeError(
|
|
913
|
+
error.step === "submit" ? "submit_failed" : "prepare_failed",
|
|
914
|
+
error.message,
|
|
915
|
+
error.detail
|
|
916
|
+
);
|
|
694
917
|
}
|
|
695
|
-
|
|
696
|
-
|
|
918
|
+
};
|
|
919
|
+
function errorCodeFrom(detail) {
|
|
920
|
+
if (typeof detail !== "string") {
|
|
921
|
+
return null;
|
|
697
922
|
}
|
|
698
|
-
|
|
699
|
-
|
|
923
|
+
try {
|
|
924
|
+
const parsed = JSON.parse(detail);
|
|
925
|
+
return typeof parsed.error?.code === "string" ? parsed.error.code : null;
|
|
926
|
+
} catch {
|
|
927
|
+
return null;
|
|
700
928
|
}
|
|
701
929
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
930
|
+
|
|
931
|
+
// ../../src/lib/canonical-json.ts
|
|
932
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
933
|
+
|
|
934
|
+
// ../../src/lib/hash.ts
|
|
935
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
936
|
+
var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
937
|
+
function sha256TaggedHex(data) {
|
|
938
|
+
return `sha256-${createHash3("sha256").update(data).digest("hex")}`;
|
|
939
|
+
}
|
|
940
|
+
function stableStringify(value) {
|
|
941
|
+
if (value === null) {
|
|
942
|
+
return "null";
|
|
706
943
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
reject(
|
|
710
|
-
"temp_account_mismatch",
|
|
711
|
-
"Temporary account token authority must be the agent wallet"
|
|
712
|
-
);
|
|
944
|
+
if (typeof value === "bigint") {
|
|
945
|
+
return JSON.stringify(value.toString());
|
|
713
946
|
}
|
|
714
|
-
if (
|
|
715
|
-
|
|
947
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
948
|
+
return JSON.stringify(value);
|
|
716
949
|
}
|
|
717
|
-
if (
|
|
718
|
-
|
|
950
|
+
if (Array.isArray(value)) {
|
|
951
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
719
952
|
}
|
|
953
|
+
const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
|
|
954
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
720
955
|
}
|
|
721
|
-
function
|
|
722
|
-
|
|
723
|
-
const ix = ixs.shift();
|
|
724
|
-
if (!ix.accounts.includes(wallet)) {
|
|
725
|
-
reject(
|
|
726
|
-
"farm_instruction_mismatch",
|
|
727
|
-
"Farm unstake instruction does not reference the agent wallet"
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
956
|
+
function hashStableJson(value) {
|
|
957
|
+
return sha256TaggedHex(stableStringify(value));
|
|
731
958
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
737
|
-
validateKvaultWithdrawInstruction(ix, expectation);
|
|
959
|
+
|
|
960
|
+
// ../../src/lib/canonical-json.ts
|
|
961
|
+
function sha256HexOf(data) {
|
|
962
|
+
return createHash4("sha256").update(data, "utf8").digest("hex");
|
|
738
963
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
if (fullExit && !expectation.allowFullExit) {
|
|
751
|
-
reject("withdraw_mismatch", "Full-exit share burn is not allowed for this intent");
|
|
964
|
+
|
|
965
|
+
// ../../src/x402/headers.ts
|
|
966
|
+
import { z as z2 } from "zod";
|
|
967
|
+
var PAYMENT_REQUIRED_HEADER = "payment-required";
|
|
968
|
+
var MAX_HEADER_JSON_BYTES = 16384;
|
|
969
|
+
var X402HeaderError = class extends Error {
|
|
970
|
+
reason;
|
|
971
|
+
constructor(reason, message) {
|
|
972
|
+
super(message);
|
|
973
|
+
this.name = "X402HeaderError";
|
|
974
|
+
this.reason = reason;
|
|
752
975
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
976
|
+
};
|
|
977
|
+
var sublyPaymentRequirementsSchema = z2.object({
|
|
978
|
+
scheme: z2.literal(PAYMENT_SCHEME),
|
|
979
|
+
network: z2.string().min(1),
|
|
980
|
+
asset: z2.string().min(32),
|
|
981
|
+
/** Exact seller amount in raw USDC; the scheme settles exactly this. */
|
|
982
|
+
amountRawUsdc: z2.string().regex(/^[1-9]\d*$/),
|
|
983
|
+
resource: z2.string().url(),
|
|
984
|
+
description: z2.string().optional(),
|
|
985
|
+
mimeType: z2.string().optional(),
|
|
986
|
+
payTo: z2.string().min(32),
|
|
987
|
+
maxTimeoutSeconds: z2.number().int().positive(),
|
|
988
|
+
extra: z2.object({
|
|
989
|
+
sellerRequestId: z2.string().min(1),
|
|
990
|
+
seller: z2.string().min(32),
|
|
991
|
+
sellerUsdcAta: z2.string().min(32),
|
|
992
|
+
vault: z2.string().min(32),
|
|
993
|
+
shareMint: z2.string().min(32)
|
|
994
|
+
})
|
|
995
|
+
}).loose();
|
|
996
|
+
var paymentRequiredSchema = z2.object({
|
|
997
|
+
x402Version: z2.number().int(),
|
|
998
|
+
accepts: z2.array(z2.unknown()),
|
|
999
|
+
error: z2.string().optional()
|
|
1000
|
+
}).loose();
|
|
1001
|
+
var sublyPaymentPayloadSchema = z2.object({
|
|
1002
|
+
x402Version: z2.number().int(),
|
|
1003
|
+
scheme: z2.literal(PAYMENT_SCHEME),
|
|
1004
|
+
network: z2.string().min(1),
|
|
1005
|
+
payload: z2.object({
|
|
1006
|
+
paymentId: z2.string().min(1),
|
|
1007
|
+
requestBindingHash: z2.string().min(1),
|
|
1008
|
+
preparedMessageHash: z2.string().min(1),
|
|
1009
|
+
serializedTransaction: z2.string().min(1).max(4096),
|
|
1010
|
+
agentSignature: z2.string().min(1).max(128),
|
|
1011
|
+
temporarySettlementSignature: z2.string().min(1).max(128)
|
|
1012
|
+
})
|
|
1013
|
+
}).loose();
|
|
1014
|
+
function decodeX402Header(headerValue) {
|
|
1015
|
+
if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
|
|
1016
|
+
throw new X402HeaderError(
|
|
1017
|
+
"header_too_large",
|
|
1018
|
+
`x402 header exceeds ${MAX_HEADER_JSON_BYTES} encoded bytes`
|
|
757
1019
|
);
|
|
758
1020
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
reject(
|
|
767
|
-
"withdraw_mismatch",
|
|
768
|
-
"Withdraw token destination is not the approved account"
|
|
1021
|
+
const json = Buffer.from(headerValue, "base64").toString("utf8");
|
|
1022
|
+
try {
|
|
1023
|
+
return JSON.parse(json);
|
|
1024
|
+
} catch {
|
|
1025
|
+
throw new X402HeaderError(
|
|
1026
|
+
"invalid_header_encoding",
|
|
1027
|
+
"x402 header is not base64-encoded JSON"
|
|
769
1028
|
);
|
|
770
1029
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
owner: expectation.wallet,
|
|
776
|
-
mint: expectation.shareMint
|
|
777
|
-
});
|
|
778
|
-
if (ix.accounts[7] !== expectedSharesAta) {
|
|
779
|
-
reject("withdraw_mismatch", "Withdraw share source must be the agent share ATA");
|
|
780
|
-
}
|
|
781
|
-
if (ix.accounts[8] !== expectation.shareMint) {
|
|
782
|
-
reject("withdraw_mismatch", "Withdraw share mint mismatch");
|
|
1030
|
+
}
|
|
1031
|
+
function requestBodyHashFor(body2) {
|
|
1032
|
+
if (body2 === null || body2 === void 0 || body2.length === 0) {
|
|
1033
|
+
return EMPTY_BODY_HASH;
|
|
783
1034
|
}
|
|
1035
|
+
return sha256TaggedHex(
|
|
1036
|
+
typeof body2 === "string" ? Buffer.from(body2, "utf8") : Buffer.from(body2)
|
|
1037
|
+
);
|
|
784
1038
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1039
|
+
|
|
1040
|
+
// ../../src/x402/standard-requirements.ts
|
|
1041
|
+
import { z as z3 } from "zod";
|
|
1042
|
+
var STANDARD_EXACT_SCHEME = "exact";
|
|
1043
|
+
var standardExactRequirementSchema = z3.object({
|
|
1044
|
+
scheme: z3.literal(STANDARD_EXACT_SCHEME),
|
|
1045
|
+
/** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
|
|
1046
|
+
network: z3.string().min(1),
|
|
1047
|
+
/** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
|
|
1048
|
+
asset: z3.string().min(1),
|
|
1049
|
+
/** Exact price in the asset's atomic units, as a decimal string. */
|
|
1050
|
+
amount: z3.string().regex(/^[1-9]\d*$/),
|
|
1051
|
+
/** Recipient wallet; the transfer destination ATA is derived from it. */
|
|
1052
|
+
payTo: z3.string().min(1),
|
|
1053
|
+
maxTimeoutSeconds: z3.number().int().positive().optional(),
|
|
1054
|
+
extra: z3.object({
|
|
1055
|
+
/** Facilitator address that pays the tx fee (gas sponsorship). */
|
|
1056
|
+
feePayer: z3.string().min(1).optional()
|
|
1057
|
+
}).loose().optional()
|
|
1058
|
+
}).loose();
|
|
1059
|
+
var standardPaymentRequiredSchema = z3.object({
|
|
1060
|
+
x402Version: z3.number().int(),
|
|
1061
|
+
accepts: z3.array(z3.unknown()),
|
|
1062
|
+
error: z3.string().optional(),
|
|
1063
|
+
resource: z3.object({ url: z3.string().optional() }).loose().optional()
|
|
1064
|
+
}).loose();
|
|
1065
|
+
var StandardX402ChallengeError = class extends Error {
|
|
1066
|
+
reason;
|
|
1067
|
+
constructor(reason, message) {
|
|
1068
|
+
super(message);
|
|
1069
|
+
this.name = "StandardX402ChallengeError";
|
|
1070
|
+
this.reason = reason;
|
|
789
1071
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1072
|
+
};
|
|
1073
|
+
function parseStandardChallenge(challenge) {
|
|
1074
|
+
const parsed = standardPaymentRequiredSchema.safeParse(challenge);
|
|
1075
|
+
if (!parsed.success) {
|
|
1076
|
+
throw new StandardX402ChallengeError(
|
|
1077
|
+
"invalid_payment_required",
|
|
1078
|
+
"Response is not a valid x402 PaymentRequired object"
|
|
796
1079
|
);
|
|
797
1080
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
}
|
|
807
|
-
if (ix.accounts[2] !== expectation.destination) {
|
|
808
|
-
reject("transfer_mismatch", `${expectation.label} destination mismatch`);
|
|
809
|
-
}
|
|
810
|
-
if (ix.accounts[3] !== expectation.authority) {
|
|
811
|
-
reject("transfer_mismatch", `${expectation.label} authority mismatch`);
|
|
812
|
-
}
|
|
1081
|
+
const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
|
|
1082
|
+
const requirement = standardExactRequirementSchema.safeParse(candidate);
|
|
1083
|
+
if (!requirement.success) {
|
|
1084
|
+
return [];
|
|
1085
|
+
}
|
|
1086
|
+
return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
|
|
1087
|
+
});
|
|
1088
|
+
return { paymentRequired: parsed.data, solanaExactRequirements };
|
|
813
1089
|
}
|
|
814
|
-
function
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
reject("close_mismatch", "CloseAccount rent destination must be the sponsor");
|
|
824
|
-
}
|
|
825
|
-
if (ix.accounts[2] !== expectation.owner) {
|
|
826
|
-
reject("close_mismatch", "CloseAccount authority must be the agent wallet");
|
|
1090
|
+
function decodeStandardPaymentRequiredHeader(headerValue) {
|
|
1091
|
+
let decoded;
|
|
1092
|
+
try {
|
|
1093
|
+
decoded = decodeX402Header(headerValue);
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
throw new StandardX402ChallengeError(
|
|
1096
|
+
error instanceof X402HeaderError ? error.reason : "invalid_header",
|
|
1097
|
+
"Cannot decode the payment-required header"
|
|
1098
|
+
);
|
|
827
1099
|
}
|
|
1100
|
+
return parseStandardChallenge(decoded);
|
|
828
1101
|
}
|
|
829
|
-
function
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
1102
|
+
function selectPayableSolanaRequirement(requirements, options) {
|
|
1103
|
+
const network = options?.network ?? SOLANA_MAINNET_NETWORK;
|
|
1104
|
+
const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
|
|
1105
|
+
const matchingRequirements = requirements.filter(
|
|
1106
|
+
(candidate) => candidate.network === network && candidate.asset === usdcMint
|
|
1107
|
+
);
|
|
1108
|
+
if (matchingRequirements.length === 0) {
|
|
1109
|
+
throw new StandardX402ChallengeError(
|
|
1110
|
+
"no_payable_requirement",
|
|
1111
|
+
`The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
|
|
1112
|
+
);
|
|
833
1113
|
}
|
|
834
|
-
|
|
835
|
-
|
|
1114
|
+
const requirement = matchingRequirements.find(
|
|
1115
|
+
(candidate) => candidate.extra?.feePayer !== void 0
|
|
1116
|
+
) ?? null;
|
|
1117
|
+
if (requirement === null) {
|
|
1118
|
+
throw new StandardX402ChallengeError(
|
|
1119
|
+
"missing_svm_fee_payer",
|
|
1120
|
+
"The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
|
|
1121
|
+
);
|
|
836
1122
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
"
|
|
842
|
-
"Associated token account creation for a foreign owner"
|
|
1123
|
+
const feePayer = requirement.extra?.feePayer;
|
|
1124
|
+
if (feePayer === void 0) {
|
|
1125
|
+
throw new StandardX402ChallengeError(
|
|
1126
|
+
"missing_svm_fee_payer",
|
|
1127
|
+
"The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
|
|
843
1128
|
);
|
|
844
1129
|
}
|
|
1130
|
+
return {
|
|
1131
|
+
requirement,
|
|
1132
|
+
amountRawUsdc: BigInt(requirement.amount),
|
|
1133
|
+
payTo: requirement.payTo,
|
|
1134
|
+
feePayer
|
|
1135
|
+
};
|
|
845
1136
|
}
|
|
846
|
-
function
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
}
|
|
850
|
-
return prefix.every((byte, index) => data[index] === byte);
|
|
1137
|
+
function standardRequirementMatchesSelected(candidate, selected) {
|
|
1138
|
+
const parsed = standardExactRequirementSchema.safeParse(candidate);
|
|
1139
|
+
return parsed.success && stableJson(parsed.data) === stableJson(selected.requirement);
|
|
851
1140
|
}
|
|
852
|
-
function
|
|
853
|
-
|
|
854
|
-
reject("invalid_transaction_encoding", "Instruction data too short for u64");
|
|
855
|
-
}
|
|
856
|
-
return Buffer.from(data.subarray(offset, offset + 8)).readBigUInt64LE(0);
|
|
1141
|
+
function stableJson(value) {
|
|
1142
|
+
return JSON.stringify(sortJson(value));
|
|
857
1143
|
}
|
|
858
|
-
function
|
|
859
|
-
if (
|
|
860
|
-
|
|
1144
|
+
function sortJson(value) {
|
|
1145
|
+
if (Array.isArray(value)) {
|
|
1146
|
+
return value.map(sortJson);
|
|
861
1147
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
let shift = 0;
|
|
867
|
-
let offset = startOffset;
|
|
868
|
-
while (offset < bytes.length) {
|
|
869
|
-
const byte = bytes[offset];
|
|
870
|
-
value |= (byte & 127) << shift;
|
|
871
|
-
offset += 1;
|
|
872
|
-
if ((byte & 128) === 0) {
|
|
873
|
-
return { value, nextOffset: offset };
|
|
874
|
-
}
|
|
875
|
-
shift += 7;
|
|
876
|
-
if (shift > 28) {
|
|
877
|
-
return null;
|
|
878
|
-
}
|
|
1148
|
+
if (value !== null && typeof value === "object") {
|
|
1149
|
+
return Object.fromEntries(
|
|
1150
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortJson(entry)])
|
|
1151
|
+
);
|
|
879
1152
|
}
|
|
880
|
-
return
|
|
1153
|
+
return value;
|
|
881
1154
|
}
|
|
882
1155
|
|
|
883
|
-
// ../../src/client/
|
|
884
|
-
var
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
this.
|
|
890
|
-
this.validationPolicy = validationPolicy;
|
|
891
|
-
}
|
|
892
|
-
get walletAddress() {
|
|
893
|
-
return this.keyPairSigner.address;
|
|
1156
|
+
// ../../src/client/standard-x402-payer.ts
|
|
1157
|
+
var StandardX402PayError = class extends Error {
|
|
1158
|
+
constructor(reason, message, detail = null) {
|
|
1159
|
+
super(message);
|
|
1160
|
+
this.reason = reason;
|
|
1161
|
+
this.detail = detail;
|
|
1162
|
+
this.name = "StandardX402PayError";
|
|
894
1163
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1164
|
+
reason;
|
|
1165
|
+
detail;
|
|
1166
|
+
};
|
|
1167
|
+
var StandardX402Payer = class {
|
|
1168
|
+
realizer;
|
|
1169
|
+
x402Fetch;
|
|
1170
|
+
probeFetch;
|
|
1171
|
+
defaultMaxAmountRawUsdc;
|
|
1172
|
+
network;
|
|
1173
|
+
usdcMint;
|
|
1174
|
+
stateStore;
|
|
1175
|
+
pending = /* @__PURE__ */ new Map();
|
|
1176
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
1177
|
+
nowMs;
|
|
1178
|
+
constructor(config) {
|
|
1179
|
+
this.realizer = config.realizer;
|
|
1180
|
+
this.x402Fetch = config.x402Fetch;
|
|
1181
|
+
this.probeFetch = config.probeFetch ?? fetch;
|
|
1182
|
+
this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
|
|
1183
|
+
this.network = config.network ?? SOLANA_MAINNET_NETWORK;
|
|
1184
|
+
this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
|
|
1185
|
+
this.stateStore = config.stateStore ?? null;
|
|
1186
|
+
this.nowMs = config.nowMs ?? (() => Date.now());
|
|
1187
|
+
if (this.stateStore !== null) {
|
|
1188
|
+
for (const record of this.stateStore.load()) {
|
|
1189
|
+
this.pending.set(record.key, record);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
902
1192
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1193
|
+
pay(input, realizer = this.realizer) {
|
|
1194
|
+
const method2 = (input.method ?? "GET").toUpperCase();
|
|
1195
|
+
const requestBodyHash = requestBodyHashFor(input.body ?? null);
|
|
1196
|
+
const pendingKey = pendingPaymentKey({
|
|
1197
|
+
url: input.url,
|
|
1198
|
+
method: method2,
|
|
1199
|
+
requestBodyHash
|
|
908
1200
|
});
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1201
|
+
const existingFlow = this.inFlight.get(pendingKey);
|
|
1202
|
+
if (existingFlow !== void 0) {
|
|
1203
|
+
return existingFlow;
|
|
1204
|
+
}
|
|
1205
|
+
const run = async () => {
|
|
1206
|
+
if (this.stateStore?.withExclusiveLock) {
|
|
1207
|
+
this.pending.clear();
|
|
1208
|
+
for (const record of this.stateStore.load()) this.pending.set(record.key, record);
|
|
1209
|
+
}
|
|
1210
|
+
return this.run(input, { method: method2, requestBodyHash, pendingKey }, realizer);
|
|
1211
|
+
};
|
|
1212
|
+
const flow = (this.stateStore?.withExclusiveLock ? this.stateStore.withExclusiveLock(run) : run()).finally(() => {
|
|
1213
|
+
this.inFlight.delete(pendingKey);
|
|
916
1214
|
});
|
|
917
|
-
|
|
1215
|
+
this.inFlight.set(pendingKey, flow);
|
|
1216
|
+
return flow;
|
|
918
1217
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1218
|
+
async run(input, computed, realizer) {
|
|
1219
|
+
const { method: method2, requestBodyHash, pendingKey } = computed;
|
|
1220
|
+
const existingPending = this.pending.get(pendingKey);
|
|
1221
|
+
if (existingPending !== void 0 && input.forceNewPayment !== true) {
|
|
1222
|
+
throw new StandardX402PayError(
|
|
1223
|
+
"payment_outcome_unknown",
|
|
1224
|
+
"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.",
|
|
1225
|
+
existingPending
|
|
924
1226
|
);
|
|
925
1227
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
serializedBase64: serializedTransaction,
|
|
937
|
-
signers: [this.keyPairSigner.keyPair]
|
|
938
|
-
});
|
|
939
|
-
const agentSignature = signatureBase58ForSigner(
|
|
940
|
-
transaction,
|
|
941
|
-
this.keyPairSigner.address
|
|
942
|
-
);
|
|
943
|
-
if (agentSignature === null) {
|
|
944
|
-
throw new IntentValidationError(
|
|
945
|
-
"signing_failed",
|
|
946
|
-
"Agent signature was not produced"
|
|
947
|
-
);
|
|
1228
|
+
if (existingPending !== void 0 && input.forceNewPayment === true) {
|
|
1229
|
+
try {
|
|
1230
|
+
this.untrack(pendingKey);
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
throw new StandardX402PayError(
|
|
1233
|
+
"state_persist_failed",
|
|
1234
|
+
"could not clear the previous pending x402 marker before forcing a new payment",
|
|
1235
|
+
error
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
948
1238
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
|
|
958
|
-
var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
|
|
959
|
-
var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
|
|
960
|
-
function sha256Hex(data) {
|
|
961
|
-
return createHash3("sha256").update(data, "utf8").digest("hex");
|
|
962
|
-
}
|
|
963
|
-
function walletAuthMessage(params) {
|
|
964
|
-
return new TextEncoder().encode(
|
|
965
|
-
`subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
|
|
966
|
-
params.rawBody
|
|
967
|
-
)}:${params.signedAtMs}`
|
|
968
|
-
);
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// ../../src/client/wallet-auth-headers.ts
|
|
972
|
-
async function walletAuthHeaders(params) {
|
|
973
|
-
const signedAtMs = String(Date.now());
|
|
974
|
-
const message = walletAuthMessage({
|
|
975
|
-
method: params.method,
|
|
976
|
-
path: new URL(params.url).pathname,
|
|
977
|
-
rawBody: params.body ?? "",
|
|
978
|
-
signedAtMs
|
|
979
|
-
});
|
|
980
|
-
return {
|
|
981
|
-
[WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
|
|
982
|
-
[WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
|
|
983
|
-
[WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
|
|
984
|
-
};
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
// ../../src/client/onboarding.ts
|
|
988
|
-
var SELF_SERVE_POLICY_ID = "self-serve";
|
|
989
|
-
var OnboardingError = class extends Error {
|
|
990
|
-
constructor(step, message, detail = null) {
|
|
991
|
-
super(message);
|
|
992
|
-
this.step = step;
|
|
993
|
-
this.detail = detail;
|
|
994
|
-
this.name = "OnboardingError";
|
|
995
|
-
}
|
|
996
|
-
step;
|
|
997
|
-
detail;
|
|
998
|
-
};
|
|
999
|
-
async function ensureWalletOnboarded(params) {
|
|
1000
|
-
const fetchImpl = params.fetchImpl ?? fetch;
|
|
1001
|
-
const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
|
|
1002
|
-
const post = async (step, path, body2) => {
|
|
1003
|
-
const url2 = `${baseUrl}${path}`;
|
|
1004
|
-
const serialized = JSON.stringify(body2);
|
|
1005
|
-
const response = await fetchImpl(url2, {
|
|
1006
|
-
method: "POST",
|
|
1007
|
-
headers: {
|
|
1008
|
-
...await walletAuthHeaders({
|
|
1009
|
-
signer: params.signer,
|
|
1010
|
-
method: "POST",
|
|
1011
|
-
url: url2,
|
|
1012
|
-
body: serialized
|
|
1013
|
-
}),
|
|
1014
|
-
"content-type": "application/json"
|
|
1015
|
-
},
|
|
1016
|
-
body: serialized
|
|
1017
|
-
});
|
|
1018
|
-
if (response.status !== 200) {
|
|
1019
|
-
let detail = null;
|
|
1020
|
-
try {
|
|
1021
|
-
detail = await response.json();
|
|
1022
|
-
} catch {
|
|
1023
|
-
detail = null;
|
|
1024
|
-
}
|
|
1025
|
-
throw new OnboardingError(
|
|
1026
|
-
step,
|
|
1027
|
-
`wallet onboarding ${step} failed with ${response.status}`,
|
|
1028
|
-
detail
|
|
1029
|
-
);
|
|
1030
|
-
}
|
|
1031
|
-
};
|
|
1032
|
-
const wallet = params.signer.walletAddress;
|
|
1033
|
-
await post("register", "/v1/wallets/agent", {
|
|
1034
|
-
wallet,
|
|
1035
|
-
signingPolicyId: SELF_SERVE_POLICY_ID,
|
|
1036
|
-
signingMode: "non_interactive",
|
|
1037
|
-
signerValidationMode: params.signer.validationMode,
|
|
1038
|
-
signerProvider: "local-keypair",
|
|
1039
|
-
activateForPayments: true
|
|
1040
|
-
});
|
|
1041
|
-
await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// ../../src/client/lookup-tables.ts
|
|
1045
|
-
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
|
|
1046
|
-
import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
|
|
1047
|
-
function lookupTableAddressesForTransaction(serializedTransaction) {
|
|
1048
|
-
const wire = Buffer.from(serializedTransaction, "base64");
|
|
1049
|
-
let offset = 0;
|
|
1050
|
-
let signatureCount = 0;
|
|
1051
|
-
let shift = 0;
|
|
1052
|
-
while (offset < wire.length) {
|
|
1053
|
-
const byte = wire[offset];
|
|
1054
|
-
signatureCount |= (byte & 127) << shift;
|
|
1055
|
-
offset += 1;
|
|
1056
|
-
if ((byte & 128) === 0) {
|
|
1057
|
-
break;
|
|
1239
|
+
const init = {
|
|
1240
|
+
method: method2,
|
|
1241
|
+
...input.body === void 0 ? {} : { body: input.body },
|
|
1242
|
+
...input.headers === void 0 ? {} : { headers: input.headers }
|
|
1243
|
+
};
|
|
1244
|
+
const probe = await this.probeFetch(input.url, init);
|
|
1245
|
+
if (probe.status !== 402) {
|
|
1246
|
+
return { paid: false, status: probe.status, body: await probe.text() };
|
|
1058
1247
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
const addresses = lookupTableAddressesForTransaction(serializedTransaction);
|
|
1068
|
-
if (addresses.length === 0) {
|
|
1069
|
-
return {};
|
|
1070
|
-
}
|
|
1071
|
-
const tables = await fetchAllMaybeAddressLookupTable(
|
|
1072
|
-
rpc2,
|
|
1073
|
-
addresses.map((value) => address(value))
|
|
1074
|
-
);
|
|
1075
|
-
const result = {};
|
|
1076
|
-
for (const table of tables) {
|
|
1077
|
-
if (table.exists) {
|
|
1078
|
-
result[table.address] = table.data.addresses.map(String);
|
|
1248
|
+
const selected = await this.selectRequirement(probe);
|
|
1249
|
+
const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
|
|
1250
|
+
if (selected.amountRawUsdc > cap) {
|
|
1251
|
+
throw new StandardX402PayError(
|
|
1252
|
+
"amount_exceeds_client_cap",
|
|
1253
|
+
`the challenge demands ${selected.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
|
|
1254
|
+
{ amountRawUsdc: selected.amountRawUsdc.toString(), payTo: selected.payTo }
|
|
1255
|
+
);
|
|
1079
1256
|
}
|
|
1080
|
-
|
|
1081
|
-
return result;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
// ../../src/client/vault-flows.ts
|
|
1085
|
-
var VaultFlowClientError = class extends Error {
|
|
1086
|
-
constructor(step, message, detail = null, code = null, errorDetails = null) {
|
|
1087
|
-
super(message);
|
|
1088
|
-
this.step = step;
|
|
1089
|
-
this.detail = detail;
|
|
1090
|
-
this.code = code;
|
|
1091
|
-
this.errorDetails = errorDetails;
|
|
1092
|
-
this.name = "VaultFlowClientError";
|
|
1093
|
-
}
|
|
1094
|
-
step;
|
|
1095
|
-
detail;
|
|
1096
|
-
code;
|
|
1097
|
-
errorDetails;
|
|
1098
|
-
};
|
|
1099
|
-
var VaultFlowClient = class {
|
|
1100
|
-
baseUrl;
|
|
1101
|
-
signer;
|
|
1102
|
-
fetchImpl;
|
|
1103
|
-
lookupTablesFor;
|
|
1104
|
-
pollTimeoutMs;
|
|
1105
|
-
pollIntervalMs;
|
|
1106
|
-
constructor(config) {
|
|
1107
|
-
this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
|
|
1108
|
-
this.signer = config.signer;
|
|
1109
|
-
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1110
|
-
this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
|
|
1111
|
-
this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
|
|
1112
|
-
this.pollIntervalMs = config.pollIntervalMs ?? 2500;
|
|
1113
|
-
}
|
|
1114
|
-
/**
|
|
1115
|
-
* Moves USDC from the agent wallet into the vault (fee sponsored). Under
|
|
1116
|
-
* depositPolicy "owner_approval_required" the relayer refuses to prepare
|
|
1117
|
-
* without an owner approval; when the caller passes none, an already
|
|
1118
|
-
* APPROVED deposit approval for this exact amount (e.g. the mandate's
|
|
1119
|
-
* initialDeposit — "one Face ID covers mandate + first deposit") is looked
|
|
1120
|
-
* up and used automatically before surfacing deposit_approval_required.
|
|
1121
|
-
*/
|
|
1122
|
-
async deposit(input) {
|
|
1123
|
-
let approvalId2 = input.approvalId;
|
|
1124
|
-
let prepared;
|
|
1257
|
+
let realized;
|
|
1125
1258
|
try {
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1259
|
+
realized = await realizer.ensureUsdcAvailable({
|
|
1260
|
+
amountRawUsdc: selected.amountRawUsdc,
|
|
1261
|
+
payment: {
|
|
1262
|
+
payTo: selected.payTo,
|
|
1263
|
+
amountRawUsdc: selected.amountRawUsdc.toString(),
|
|
1264
|
+
resourceUrlHash: sha256HexOf(input.url),
|
|
1265
|
+
method: method2
|
|
1266
|
+
},
|
|
1267
|
+
...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
|
|
1130
1268
|
});
|
|
1131
1269
|
} catch (error) {
|
|
1132
|
-
if (
|
|
1133
|
-
throw
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1270
|
+
if (error.code === "approval_required") {
|
|
1271
|
+
throw new StandardX402PayError(
|
|
1272
|
+
"approval_required",
|
|
1273
|
+
"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",
|
|
1274
|
+
error.detail ?? null
|
|
1275
|
+
);
|
|
1138
1276
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
amountRawUsdc:
|
|
1142
|
-
|
|
1143
|
-
});
|
|
1144
|
-
}
|
|
1145
|
-
const signed = await this.signer.signDeposit({
|
|
1146
|
-
intent: prepared.signingIntent,
|
|
1147
|
-
serializedTransaction: prepared.serializedTransaction,
|
|
1148
|
-
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
1149
|
-
});
|
|
1150
|
-
let outcome = await this.postJson("submit", "/v1/deposits/submit", {
|
|
1151
|
-
depositId: prepared.depositId,
|
|
1152
|
-
serializedTransaction: signed.serializedTransaction,
|
|
1153
|
-
agentSignature: signed.agentSignature
|
|
1154
|
-
});
|
|
1155
|
-
if (outcome.status === "submitted") {
|
|
1156
|
-
outcome = await this.pollUntilTerminal(
|
|
1157
|
-
`/v1/deposits/${prepared.depositId}`,
|
|
1158
|
-
outcome
|
|
1277
|
+
throw new StandardX402PayError(
|
|
1278
|
+
"realize_failed",
|
|
1279
|
+
`could not realize yield to cover ${selected.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
|
|
1280
|
+
error
|
|
1159
1281
|
);
|
|
1160
1282
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1283
|
+
const pendingRecord = {
|
|
1284
|
+
key: pendingKey,
|
|
1285
|
+
url: input.url,
|
|
1286
|
+
method: method2,
|
|
1287
|
+
requestBodyHash,
|
|
1288
|
+
amountRawUsdc: selected.amountRawUsdc.toString(),
|
|
1289
|
+
payTo: selected.payTo,
|
|
1290
|
+
feePayer: selected.feePayer,
|
|
1291
|
+
realizedRawUsdc: realized.realizedRawUsdc.toString(),
|
|
1292
|
+
realizeTxSignature: realized.txSignature,
|
|
1293
|
+
status: "realized",
|
|
1294
|
+
createdAtMs: this.nowMs(),
|
|
1295
|
+
updatedAtMs: this.nowMs()
|
|
1168
1296
|
};
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
outcome
|
|
1297
|
+
try {
|
|
1298
|
+
this.track(pendingRecord);
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
throw new StandardX402PayError(
|
|
1301
|
+
"state_persist_failed",
|
|
1302
|
+
"could not persist the pending x402 marker; refusing to attempt the external payment because a restart would not be double-payment safe",
|
|
1303
|
+
{ error, pendingPayment: pendingRecord }
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
let response;
|
|
1307
|
+
try {
|
|
1308
|
+
response = await this.x402Fetch(input.url, init, selected);
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
const persistError = this.tryMarkUnknown(pendingKey, {
|
|
1311
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1312
|
+
});
|
|
1313
|
+
throw new StandardX402PayError(
|
|
1314
|
+
"payment_outcome_unknown",
|
|
1315
|
+
`the x402 payment attempt failed after yield was realized; verify whether it settled before paying again: ${error instanceof Error ? error.message : String(error)}`,
|
|
1316
|
+
{ error, persistError }
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
const bodyText = await response.text();
|
|
1320
|
+
if (response.status !== 200) {
|
|
1321
|
+
const persistError = this.tryMarkUnknown(pendingKey, {
|
|
1322
|
+
status: response.status,
|
|
1323
|
+
body: bodyText
|
|
1324
|
+
});
|
|
1325
|
+
throw new StandardX402PayError(
|
|
1326
|
+
"payment_outcome_unknown",
|
|
1327
|
+
`the x402 payment attempt returned ${response.status} after yield was realized; verify whether it settled before paying again`,
|
|
1328
|
+
{ status: response.status, body: bodyText, persistError }
|
|
1202
1329
|
);
|
|
1203
1330
|
}
|
|
1331
|
+
this.clearDelivered(pendingKey);
|
|
1332
|
+
const paymentTxSignature = extractSettledPaymentTxSignature(response);
|
|
1333
|
+
if (paymentTxSignature !== null && typeof realized.withdrawalId === "string" && realizer.reportPayment !== void 0) {
|
|
1334
|
+
try {
|
|
1335
|
+
await realizer.reportPayment({
|
|
1336
|
+
withdrawalId: realized.withdrawalId,
|
|
1337
|
+
paymentTxSignature
|
|
1338
|
+
});
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
console.error(
|
|
1341
|
+
`[subly-x402] payment report-back failed (audit only, payment ok): ${error instanceof Error ? error.message : String(error)}`
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1204
1345
|
return {
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1346
|
+
paid: true,
|
|
1347
|
+
...realizer.vault === void 0 ? {} : { fundingVault: realizer.vault },
|
|
1348
|
+
status: response.status,
|
|
1349
|
+
body: bodyText,
|
|
1350
|
+
payment: {
|
|
1351
|
+
amountRawUsdc: selected.amountRawUsdc.toString(),
|
|
1352
|
+
payTo: selected.payTo,
|
|
1353
|
+
feePayer: selected.feePayer,
|
|
1354
|
+
realizedRawUsdc: realized.realizedRawUsdc.toString(),
|
|
1355
|
+
realizeTxSignature: realized.txSignature,
|
|
1356
|
+
paymentTxSignature
|
|
1357
|
+
}
|
|
1212
1358
|
};
|
|
1213
1359
|
}
|
|
1214
|
-
/**
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1360
|
+
/** Reads the challenge from the header (preferred) or the JSON body. */
|
|
1361
|
+
async selectRequirement(probe) {
|
|
1362
|
+
const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
1363
|
+
let requirements;
|
|
1364
|
+
try {
|
|
1365
|
+
if (header !== null) {
|
|
1366
|
+
requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
|
|
1367
|
+
} else {
|
|
1368
|
+
requirements = parseStandardChallenge(
|
|
1369
|
+
await probe.json()
|
|
1370
|
+
).solanaExactRequirements;
|
|
1371
|
+
}
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
throw new StandardX402PayError(
|
|
1374
|
+
error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
|
|
1375
|
+
"could not parse the x402 402 challenge",
|
|
1376
|
+
error
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
try {
|
|
1380
|
+
return selectPayableSolanaRequirement(requirements, {
|
|
1381
|
+
network: this.network,
|
|
1382
|
+
usdcMint: this.usdcMint
|
|
1383
|
+
});
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
throw new StandardX402PayError(
|
|
1386
|
+
"no_payable_requirement",
|
|
1387
|
+
error instanceof Error ? error.message : String(error),
|
|
1388
|
+
error
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
track(record) {
|
|
1393
|
+
const previous = this.pending.get(record.key);
|
|
1394
|
+
this.pending.set(record.key, record);
|
|
1395
|
+
try {
|
|
1396
|
+
this.persist();
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
if (previous === void 0) {
|
|
1399
|
+
this.pending.delete(record.key);
|
|
1400
|
+
} else {
|
|
1401
|
+
this.pending.set(record.key, previous);
|
|
1402
|
+
}
|
|
1403
|
+
throw error;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
markUnknown(key, detail) {
|
|
1407
|
+
const current = this.pending.get(key);
|
|
1408
|
+
if (current === void 0) {
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
const next = {
|
|
1412
|
+
...current,
|
|
1413
|
+
status: "external_outcome_unknown",
|
|
1414
|
+
updatedAtMs: this.nowMs(),
|
|
1415
|
+
detail
|
|
1416
|
+
};
|
|
1417
|
+
this.pending.set(key, next);
|
|
1418
|
+
try {
|
|
1419
|
+
this.persist();
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
this.pending.set(key, current);
|
|
1422
|
+
throw error;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
tryMarkUnknown(key, detail) {
|
|
1426
|
+
try {
|
|
1427
|
+
this.markUnknown(key, detail);
|
|
1428
|
+
return null;
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
return error;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
untrack(key) {
|
|
1434
|
+
const previous = this.pending.get(key);
|
|
1435
|
+
const existed = previous !== void 0;
|
|
1436
|
+
this.pending.delete(key);
|
|
1437
|
+
try {
|
|
1438
|
+
this.persist();
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
if (existed) {
|
|
1441
|
+
this.pending.set(key, previous);
|
|
1442
|
+
}
|
|
1443
|
+
throw error;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
clearDelivered(key) {
|
|
1447
|
+
const previous = this.pending.get(key);
|
|
1448
|
+
this.pending.delete(key);
|
|
1449
|
+
try {
|
|
1450
|
+
this.persist();
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
if (previous !== void 0) {
|
|
1453
|
+
this.pending.set(key, previous);
|
|
1454
|
+
}
|
|
1455
|
+
console.error(
|
|
1456
|
+
`[subly-x402] payment delivered but pending marker could not be cleared: ${error instanceof Error ? error.message : String(error)}`
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
persist() {
|
|
1461
|
+
if (this.stateStore === null) {
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
this.stateStore.save([...this.pending.values()]);
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
function pendingPaymentKey(input) {
|
|
1468
|
+
return `${input.method}:${input.url}:${input.requestBodyHash}`;
|
|
1469
|
+
}
|
|
1470
|
+
function extractSettledPaymentTxSignature(response) {
|
|
1471
|
+
const header = response.headers.get("x-payment-response");
|
|
1472
|
+
if (header === null || header.length === 0) {
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
try {
|
|
1476
|
+
const decoded = JSON.parse(
|
|
1477
|
+
Buffer.from(header, "base64").toString("utf8")
|
|
1478
|
+
);
|
|
1479
|
+
if (typeof decoded.transaction === "string" && decoded.transaction.length > 0) {
|
|
1480
|
+
return decoded.transaction;
|
|
1481
|
+
}
|
|
1482
|
+
if (typeof decoded.txHash === "string" && decoded.txHash.length > 0) {
|
|
1483
|
+
return decoded.txHash;
|
|
1484
|
+
}
|
|
1485
|
+
return null;
|
|
1486
|
+
} catch {
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// ../../src/client/relayer-payer.ts
|
|
1492
|
+
function createRelayerX402Payer(config) {
|
|
1493
|
+
const realizer = new RelayerYieldRealizer({
|
|
1494
|
+
relayerBaseUrl: config.relayerBaseUrl,
|
|
1495
|
+
signer: config.signer,
|
|
1496
|
+
rpc: config.rpc
|
|
1497
|
+
});
|
|
1498
|
+
return new StandardX402Payer({
|
|
1499
|
+
realizer,
|
|
1500
|
+
x402Fetch: config.x402Fetch,
|
|
1501
|
+
defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc,
|
|
1502
|
+
...config.stateStore === void 0 ? {} : { stateStore: config.stateStore }
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// ../../src/client/signer-env.ts
|
|
1507
|
+
import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
|
|
1508
|
+
|
|
1509
|
+
// ../../src/solana/keys.ts
|
|
1510
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1511
|
+
import bs583 from "bs58";
|
|
1512
|
+
import {
|
|
1513
|
+
createKeyPairSignerFromBytes
|
|
1514
|
+
} from "@solana/kit";
|
|
1515
|
+
function loadSecretKeyBytes(params) {
|
|
1516
|
+
const { base58Secret, jsonFilePath, label } = params;
|
|
1517
|
+
if (base58Secret !== void 0 && base58Secret.length > 0) {
|
|
1518
|
+
const bytes = bs583.decode(base58Secret);
|
|
1519
|
+
if (bytes.length !== 64) {
|
|
1520
|
+
throw new Error(`${label} base58 secret must decode to 64 bytes`);
|
|
1521
|
+
}
|
|
1522
|
+
return bytes;
|
|
1523
|
+
}
|
|
1524
|
+
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
1525
|
+
const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
|
|
1526
|
+
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
1527
|
+
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
1528
|
+
}
|
|
1529
|
+
return Uint8Array.from(raw);
|
|
1530
|
+
}
|
|
1531
|
+
throw new Error(`${label} keypair is not configured`);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// ../../src/client/agent-wallet-signer.ts
|
|
1535
|
+
import { signBytes } from "@solana/kit";
|
|
1536
|
+
import bs587 from "bs58";
|
|
1537
|
+
import nacl3 from "tweetnacl";
|
|
1538
|
+
|
|
1539
|
+
// ../../src/solana/tx.ts
|
|
1540
|
+
import bs584 from "bs58";
|
|
1541
|
+
import {
|
|
1542
|
+
appendTransactionMessageInstructions,
|
|
1543
|
+
compileTransaction,
|
|
1544
|
+
compressTransactionMessageUsingAddressLookupTables,
|
|
1545
|
+
createTransactionMessage,
|
|
1546
|
+
getBase64EncodedWireTransaction,
|
|
1547
|
+
getTransactionDecoder,
|
|
1548
|
+
partiallySignTransaction,
|
|
1549
|
+
pipe,
|
|
1550
|
+
setTransactionMessageFeePayer,
|
|
1551
|
+
setTransactionMessageLifetimeUsingBlockhash
|
|
1552
|
+
} from "@solana/kit";
|
|
1553
|
+
function decodeSerializedTransaction(serializedBase64) {
|
|
1554
|
+
return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
|
|
1555
|
+
}
|
|
1556
|
+
function attachExternalSignatureToTransaction(params) {
|
|
1557
|
+
if (!(params.signer in params.transaction.signatures)) {
|
|
1558
|
+
throw new Error(
|
|
1559
|
+
`transaction does not expect a signature from ${params.signer}`
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
const transaction = Object.freeze({
|
|
1563
|
+
...params.transaction,
|
|
1564
|
+
signatures: Object.freeze({
|
|
1565
|
+
...params.transaction.signatures,
|
|
1566
|
+
[params.signer]: params.signature
|
|
1567
|
+
})
|
|
1568
|
+
});
|
|
1569
|
+
return {
|
|
1570
|
+
serializedBase64: getBase64EncodedWireTransaction(transaction),
|
|
1571
|
+
transaction
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
async function addSignaturesToSerializedTransaction(params) {
|
|
1575
|
+
const decoded = decodeSerializedTransaction(params.serializedBase64);
|
|
1576
|
+
const signed = await partiallySignTransaction(params.signers, decoded);
|
|
1577
|
+
return {
|
|
1578
|
+
serializedBase64: getBase64EncodedWireTransaction(signed),
|
|
1579
|
+
transaction: signed
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
function signatureBase58ForSigner(transaction, signer2) {
|
|
1583
|
+
const signature = transaction.signatures[signer2];
|
|
1584
|
+
if (signature === null || signature === void 0) {
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
return bs584.encode(signature);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// ../../src/client/remote-signer-transport.ts
|
|
1591
|
+
import bs585 from "bs58";
|
|
1592
|
+
import nacl2 from "tweetnacl";
|
|
1593
|
+
var RemoteSigningError = class extends Error {
|
|
1594
|
+
constructor(provider, message, detail = null) {
|
|
1595
|
+
super(`[${provider}] ${message}`);
|
|
1596
|
+
this.provider = provider;
|
|
1597
|
+
this.detail = detail;
|
|
1598
|
+
this.name = "RemoteSigningError";
|
|
1599
|
+
}
|
|
1600
|
+
provider;
|
|
1601
|
+
detail;
|
|
1602
|
+
};
|
|
1603
|
+
function ed25519PublicKeyBytes(provider, walletAddress) {
|
|
1604
|
+
let bytes;
|
|
1605
|
+
try {
|
|
1606
|
+
bytes = bs585.decode(walletAddress);
|
|
1607
|
+
} catch {
|
|
1608
|
+
throw new RemoteSigningError(
|
|
1609
|
+
provider,
|
|
1610
|
+
`wallet address ${walletAddress} is not base58`
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
if (bytes.length !== 32) {
|
|
1614
|
+
throw new RemoteSigningError(
|
|
1615
|
+
provider,
|
|
1616
|
+
`wallet address ${walletAddress} is not a 32-byte ed25519 key`
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
return bytes;
|
|
1620
|
+
}
|
|
1621
|
+
function verifiedEd25519Signature(params) {
|
|
1622
|
+
const publicKey2 = ed25519PublicKeyBytes(params.provider, params.walletAddress);
|
|
1623
|
+
const encoded = params.encodedSignature.trim();
|
|
1624
|
+
for (const candidate of decodeSignatureCandidates(encoded)) {
|
|
1625
|
+
if (nacl2.sign.detached.verify(params.message, candidate, publicKey2)) {
|
|
1626
|
+
return candidate;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
throw new RemoteSigningError(
|
|
1630
|
+
params.provider,
|
|
1631
|
+
`signature did not verify for wallet ${params.walletAddress}`
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
function decodeSignatureCandidates(encoded) {
|
|
1635
|
+
const candidates = [];
|
|
1636
|
+
const hex = encoded.startsWith("0x") ? encoded.slice(2) : encoded;
|
|
1637
|
+
if (/^[0-9a-fA-F]{128}$/.test(hex)) {
|
|
1638
|
+
candidates.push(Uint8Array.from(Buffer.from(hex, "hex")));
|
|
1639
|
+
}
|
|
1640
|
+
try {
|
|
1641
|
+
const fromBase58 = bs585.decode(encoded);
|
|
1642
|
+
if (fromBase58.length === 64) {
|
|
1643
|
+
candidates.push(fromBase58);
|
|
1644
|
+
}
|
|
1645
|
+
} catch {
|
|
1646
|
+
}
|
|
1647
|
+
if (/^[A-Za-z0-9+/=_-]+$/.test(encoded)) {
|
|
1648
|
+
const fromBase64 = Uint8Array.from(
|
|
1649
|
+
Buffer.from(encoded.replace(/-/g, "+").replace(/_/g, "/"), "base64")
|
|
1650
|
+
);
|
|
1651
|
+
if (fromBase64.length === 64) {
|
|
1652
|
+
candidates.push(fromBase64);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return candidates;
|
|
1656
|
+
}
|
|
1657
|
+
async function requestVerifiedTransactionSignature(params) {
|
|
1658
|
+
const { transport } = params;
|
|
1659
|
+
const signedBase64 = await transport.signTransaction(
|
|
1660
|
+
params.serializedTransactionBase64
|
|
1661
|
+
);
|
|
1662
|
+
let returned;
|
|
1663
|
+
try {
|
|
1664
|
+
returned = decodeSerializedTransaction(signedBase64);
|
|
1665
|
+
} catch (error) {
|
|
1666
|
+
throw new RemoteSigningError(
|
|
1667
|
+
transport.provider,
|
|
1668
|
+
"provider returned an undecodable signed transaction",
|
|
1669
|
+
error
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
const signature = returned.signatures[transport.walletAddress] ?? null;
|
|
1673
|
+
if (signature === null) {
|
|
1674
|
+
throw new RemoteSigningError(
|
|
1675
|
+
transport.provider,
|
|
1676
|
+
`signed transaction is missing the signature for ${transport.walletAddress}`
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
const publicKey2 = params.publicKey ?? ed25519PublicKeyBytes(transport.provider, transport.walletAddress);
|
|
1680
|
+
if (!nacl2.sign.detached.verify(params.messageBytes, signature, publicKey2)) {
|
|
1681
|
+
throw new RemoteSigningError(
|
|
1682
|
+
transport.provider,
|
|
1683
|
+
"returned signature does not verify over the requested transaction"
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
return signature;
|
|
1687
|
+
}
|
|
1688
|
+
async function externallySignedAgentTransaction(params) {
|
|
1689
|
+
const original = decodeSerializedTransaction(params.serializedTransaction);
|
|
1690
|
+
const signature = await requestVerifiedTransactionSignature({
|
|
1691
|
+
transport: params.transport,
|
|
1692
|
+
serializedTransactionBase64: params.serializedTransaction,
|
|
1693
|
+
messageBytes: original.messageBytes
|
|
1694
|
+
});
|
|
1695
|
+
const attached = attachExternalSignatureToTransaction({
|
|
1696
|
+
transaction: original,
|
|
1697
|
+
signer: params.transport.walletAddress,
|
|
1698
|
+
signature
|
|
1699
|
+
});
|
|
1700
|
+
return {
|
|
1701
|
+
serializedTransaction: attached.serializedBase64,
|
|
1702
|
+
agentSignature: bs585.encode(signature)
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
async function providerJsonRequest(params) {
|
|
1706
|
+
const response = await params.fetchImpl(`${params.baseUrl}${params.path}`, {
|
|
1707
|
+
method: params.method,
|
|
1708
|
+
headers: { ...params.headers, "content-type": "application/json" },
|
|
1709
|
+
...params.body === void 0 ? {} : { body: JSON.stringify(params.body) }
|
|
1710
|
+
});
|
|
1711
|
+
let json = null;
|
|
1712
|
+
try {
|
|
1713
|
+
json = await response.json();
|
|
1714
|
+
} catch {
|
|
1715
|
+
json = null;
|
|
1716
|
+
}
|
|
1717
|
+
if (!response.ok) {
|
|
1718
|
+
throw new RemoteSigningError(
|
|
1719
|
+
params.provider,
|
|
1720
|
+
`${params.method} ${params.path} failed with ${response.status}`,
|
|
1721
|
+
json
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
return json;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// ../../src/client/transaction-intent-validator.ts
|
|
1728
|
+
import bs586 from "bs58";
|
|
1729
|
+
import { getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
|
|
1730
|
+
|
|
1731
|
+
// ../../src/domain/request-binding.ts
|
|
1732
|
+
function computeRequestBindingHash(fields) {
|
|
1733
|
+
return hashStableJson({
|
|
1734
|
+
sellerRequestId: fields.sellerRequestId,
|
|
1735
|
+
httpMethod: fields.httpMethod.toUpperCase(),
|
|
1736
|
+
canonicalResourceUrl: fields.canonicalResourceUrl,
|
|
1737
|
+
requestBodyHash: fields.requestBodyHash,
|
|
1738
|
+
seller: fields.seller,
|
|
1739
|
+
asset: fields.asset,
|
|
1740
|
+
amountRawUsdc: fields.amountRawUsdc,
|
|
1741
|
+
payTo: fields.payTo,
|
|
1742
|
+
sellerUsdcAta: fields.sellerUsdcAta
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// ../../src/client/transaction-intent-validator.ts
|
|
1747
|
+
var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
|
|
1748
|
+
var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
|
|
1749
|
+
var ASSOCIATED_TOKEN_PROGRAM_ID2 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
1750
|
+
var MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
|
|
1751
|
+
var KVAULT_PROGRAM_ID = "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd";
|
|
1752
|
+
var KAMINO_FARMS_PROGRAM_ID = "FarmsPZpWu9i7Kky8tPN37rs2TpmMrAZrC7S7vJa91Hr";
|
|
1753
|
+
var KVAULT_WITHDRAW_DISCRIMINATOR = Uint8Array.from([
|
|
1754
|
+
183,
|
|
1755
|
+
18,
|
|
1756
|
+
70,
|
|
1757
|
+
156,
|
|
1758
|
+
148,
|
|
1759
|
+
109,
|
|
1760
|
+
161,
|
|
1761
|
+
34
|
|
1762
|
+
]);
|
|
1763
|
+
var KVAULT_WITHDRAW_FROM_AVAILABLE_DISCRIMINATOR = Uint8Array.from([
|
|
1764
|
+
19,
|
|
1765
|
+
131,
|
|
1766
|
+
112,
|
|
1767
|
+
155,
|
|
1768
|
+
170,
|
|
1769
|
+
220,
|
|
1770
|
+
34,
|
|
1771
|
+
57
|
|
1772
|
+
]);
|
|
1773
|
+
var KVAULT_DEPOSIT_DISCRIMINATOR = Uint8Array.from([
|
|
1774
|
+
242,
|
|
1775
|
+
35,
|
|
1776
|
+
198,
|
|
1777
|
+
137,
|
|
1778
|
+
82,
|
|
1779
|
+
225,
|
|
1780
|
+
242,
|
|
1781
|
+
182
|
|
1782
|
+
]);
|
|
1783
|
+
var U64_MAX = 18446744073709551615n;
|
|
1784
|
+
var MAX_TEMP_ACCOUNT_LAMPORTS = 10000000n;
|
|
1785
|
+
var DEFAULT_MAX_COMPUTE_UNIT_LIMIT = 14e5;
|
|
1786
|
+
var DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS = 100000n;
|
|
1787
|
+
var IntentValidationError = class extends Error {
|
|
1788
|
+
reason;
|
|
1789
|
+
constructor(reason, message) {
|
|
1790
|
+
super(message);
|
|
1791
|
+
this.name = "IntentValidationError";
|
|
1792
|
+
this.reason = reason;
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
function reject(reason, message) {
|
|
1796
|
+
throw new IntentValidationError(reason, message);
|
|
1797
|
+
}
|
|
1798
|
+
function decodeIntentTransaction(params) {
|
|
1799
|
+
const wire = Buffer.from(params.serializedTransaction, "base64");
|
|
1800
|
+
const signatureCount = readShortVec(wire, 0);
|
|
1801
|
+
if (signatureCount === null) {
|
|
1802
|
+
reject("invalid_transaction_encoding", "Cannot parse signature count");
|
|
1803
|
+
}
|
|
1804
|
+
const messageOffset = signatureCount.nextOffset + signatureCount.value * 64;
|
|
1805
|
+
if (messageOffset >= wire.length) {
|
|
1806
|
+
reject("invalid_transaction_encoding", "Transaction has no message bytes");
|
|
1807
|
+
}
|
|
1808
|
+
const messageBytes = wire.subarray(messageOffset);
|
|
1809
|
+
const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
|
|
1810
|
+
if (compiled.version !== 0) {
|
|
1811
|
+
reject("unsupported_transaction_version", "Only v0 transactions are supported");
|
|
1812
|
+
}
|
|
1813
|
+
const staticAccounts = compiled.staticAccounts.map(String);
|
|
1814
|
+
const loadedWritable = [];
|
|
1815
|
+
const loadedReadonly = [];
|
|
1816
|
+
const lookups = compiled.addressTableLookups ?? [];
|
|
1817
|
+
for (const rawLookup of lookups) {
|
|
1818
|
+
const lookup = rawLookup;
|
|
1819
|
+
const table = params.lookupTables?.[String(lookup.lookupTableAddress)];
|
|
1820
|
+
if (table === void 0) {
|
|
1821
|
+
reject(
|
|
1822
|
+
"lookup_table_unresolved",
|
|
1823
|
+
`Transaction references unknown lookup table ${lookup.lookupTableAddress}`
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
const writableIndexes = lookup.writableIndexes ?? lookup.writableIndices ?? [];
|
|
1827
|
+
const readonlyIndexes = lookup.readonlyIndexes ?? lookup.readableIndices ?? [];
|
|
1828
|
+
for (const index of writableIndexes) {
|
|
1829
|
+
const resolved = table[index];
|
|
1830
|
+
if (resolved === void 0) {
|
|
1831
|
+
reject("lookup_table_unresolved", "Lookup table index out of range");
|
|
1832
|
+
}
|
|
1833
|
+
loadedWritable.push(String(resolved));
|
|
1834
|
+
}
|
|
1835
|
+
for (const index of readonlyIndexes) {
|
|
1836
|
+
const resolved = table[index];
|
|
1837
|
+
if (resolved === void 0) {
|
|
1838
|
+
reject("lookup_table_unresolved", "Lookup table index out of range");
|
|
1839
|
+
}
|
|
1840
|
+
loadedReadonly.push(String(resolved));
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
const orderedAccounts = [...staticAccounts, ...loadedWritable, ...loadedReadonly];
|
|
1844
|
+
const instructions = compiled.instructions.map(
|
|
1845
|
+
(instruction) => {
|
|
1846
|
+
const programAddress = orderedAccounts[instruction.programAddressIndex];
|
|
1847
|
+
if (programAddress === void 0) {
|
|
1848
|
+
reject("invalid_transaction_encoding", "Program index out of range");
|
|
1849
|
+
}
|
|
1850
|
+
const accounts = (instruction.accountIndices ?? []).map((index) => {
|
|
1851
|
+
const account = orderedAccounts[index];
|
|
1852
|
+
if (account === void 0) {
|
|
1853
|
+
reject("invalid_transaction_encoding", "Account index out of range");
|
|
1854
|
+
}
|
|
1855
|
+
return account;
|
|
1856
|
+
});
|
|
1857
|
+
return {
|
|
1858
|
+
programAddress,
|
|
1859
|
+
accounts,
|
|
1860
|
+
data: instruction.data === void 0 ? new Uint8Array() : Uint8Array.from(instruction.data)
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
);
|
|
1864
|
+
const feePayer = staticAccounts[0];
|
|
1865
|
+
if (feePayer === void 0) {
|
|
1866
|
+
reject("invalid_transaction_encoding", "Transaction has no fee payer");
|
|
1867
|
+
}
|
|
1868
|
+
return {
|
|
1869
|
+
feePayer,
|
|
1870
|
+
requiredSigners: staticAccounts.slice(0, compiled.header.numSignerAccounts),
|
|
1871
|
+
instructions,
|
|
1872
|
+
messageHash: sha256TaggedHex(Buffer.from(messageBytes))
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
function validatePaymentIntentTransaction(params) {
|
|
1876
|
+
const { intent } = params;
|
|
1877
|
+
const now = params.nowMs ?? Date.now();
|
|
1878
|
+
const policy = resolveIntentValidationPolicy(params.policy);
|
|
1879
|
+
if (new Date(intent.expiresAt).getTime() <= now) {
|
|
1880
|
+
reject("expired", "Payment intent has expired");
|
|
1881
|
+
}
|
|
1882
|
+
if (intent.scheme !== PAYMENT_SCHEME) {
|
|
1883
|
+
reject("scheme_mismatch", `scheme must be ${PAYMENT_SCHEME}`);
|
|
1884
|
+
}
|
|
1885
|
+
if (intent.network !== SOLANA_MAINNET_NETWORK) {
|
|
1886
|
+
reject("network_mismatch", "Unsupported network");
|
|
1887
|
+
}
|
|
1888
|
+
if (intent.vault !== policy.vault.address) {
|
|
1889
|
+
reject("vault_mismatch", "Unsupported vault");
|
|
1890
|
+
}
|
|
1891
|
+
if (intent.shareMint !== policy.vault.shareMint) {
|
|
1892
|
+
reject("share_mint_mismatch", "Unsupported share mint");
|
|
1893
|
+
}
|
|
1894
|
+
if (intent.farm !== policy.vault.farm) {
|
|
1895
|
+
reject("farm_mismatch", "Unsupported Kamino farm");
|
|
1896
|
+
}
|
|
1897
|
+
if (intent.asset !== policy.vault.usdcMint) {
|
|
1898
|
+
reject("asset_mismatch", "Only USDC payments are supported");
|
|
1899
|
+
}
|
|
1900
|
+
if (intent.memo !== intent.paymentId) {
|
|
1901
|
+
reject("memo_mismatch", "Memo must equal the paymentId");
|
|
1902
|
+
}
|
|
1903
|
+
const expectedBinding = computeRequestBindingHash({
|
|
1904
|
+
sellerRequestId: intent.sellerRequestId,
|
|
1905
|
+
httpMethod: intent.httpMethod,
|
|
1906
|
+
canonicalResourceUrl: intent.canonicalResourceUrl,
|
|
1907
|
+
requestBodyHash: intent.requestBodyHash,
|
|
1908
|
+
seller: intent.seller,
|
|
1909
|
+
asset: intent.asset,
|
|
1910
|
+
amountRawUsdc: intent.amountRawUsdc,
|
|
1911
|
+
payTo: intent.payTo,
|
|
1912
|
+
sellerUsdcAta: intent.sellerUsdcAta
|
|
1913
|
+
});
|
|
1914
|
+
if (expectedBinding !== intent.requestBindingHash) {
|
|
1915
|
+
reject(
|
|
1916
|
+
"request_binding_mismatch",
|
|
1917
|
+
"requestBindingHash does not match the request fields"
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
const expectedSellerAta = deriveAssociatedTokenAddress({
|
|
1921
|
+
owner: intent.payTo,
|
|
1922
|
+
mint: intent.asset
|
|
1923
|
+
});
|
|
1924
|
+
if (expectedSellerAta !== intent.sellerUsdcAta) {
|
|
1925
|
+
reject(
|
|
1926
|
+
"seller_ata_mismatch",
|
|
1927
|
+
"sellerUsdcAta must be the associated USDC account for payTo"
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
const expectedDustAta = deriveAssociatedTokenAddress({
|
|
1931
|
+
owner: intent.wallet,
|
|
1932
|
+
mint: intent.asset
|
|
1933
|
+
});
|
|
1934
|
+
if (expectedDustAta !== intent.dustRecipientUsdcAta) {
|
|
1935
|
+
reject(
|
|
1936
|
+
"dust_recipient_mismatch",
|
|
1937
|
+
"dustRecipientUsdcAta must be the agent wallet's USDC ATA"
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
const decoded = decodeIntentTransaction({
|
|
1941
|
+
serializedTransaction: params.serializedTransaction,
|
|
1942
|
+
...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
|
|
1943
|
+
});
|
|
1944
|
+
if (decoded.messageHash !== intent.preparedMessageHash) {
|
|
1945
|
+
reject("message_hash_mismatch", "Prepared message hash mismatch");
|
|
1946
|
+
}
|
|
1947
|
+
if (decoded.feePayer !== intent.feePayer) {
|
|
1948
|
+
reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
|
|
1949
|
+
}
|
|
1950
|
+
const expectedSigners = /* @__PURE__ */ new Set([
|
|
1951
|
+
intent.feePayer,
|
|
1952
|
+
intent.wallet,
|
|
1953
|
+
intent.temporarySettlementTokenAccount
|
|
1954
|
+
]);
|
|
1955
|
+
if (decoded.requiredSigners.length !== expectedSigners.size || !decoded.requiredSigners.every((signer2) => expectedSigners.has(signer2))) {
|
|
1956
|
+
reject(
|
|
1957
|
+
"unexpected_signers",
|
|
1958
|
+
"Transaction signers must be exactly the sponsor, the agent wallet, and the temporary settlement account"
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
const ixs = [...decoded.instructions];
|
|
1962
|
+
expectComputeBudgetPair(ixs, policy);
|
|
1963
|
+
expectCreateTemporaryAccount(ixs, intent, policy);
|
|
1964
|
+
expectInitializeTemporaryAccount(ixs, intent);
|
|
1965
|
+
consumeFarmInstructions(ixs, intent);
|
|
1966
|
+
expectKvaultWithdraw(ixs, {
|
|
1967
|
+
wallet: intent.wallet,
|
|
1968
|
+
vault: intent.vault,
|
|
1969
|
+
shareMint: intent.shareMint,
|
|
1970
|
+
asset: intent.asset,
|
|
1971
|
+
userTokenAccount: intent.temporarySettlementTokenAccount,
|
|
1972
|
+
maxSharesToRedeemRaw: BigInt(intent.maxSharesToRedeemRaw),
|
|
1973
|
+
allowFullExit: false
|
|
1974
|
+
});
|
|
1975
|
+
expectTransferChecked(ixs, {
|
|
1976
|
+
source: intent.temporarySettlementTokenAccount,
|
|
1977
|
+
mint: intent.asset,
|
|
1978
|
+
destination: intent.sellerUsdcAta,
|
|
1979
|
+
authority: intent.wallet,
|
|
1980
|
+
amount: BigInt(intent.amountRawUsdc),
|
|
1981
|
+
label: "seller transfer"
|
|
1982
|
+
});
|
|
1983
|
+
if (ixs[0] !== void 0 && ixs[0].programAddress === SPL_TOKEN_PROGRAM_ID && ixs[0].data[0] === 12) {
|
|
1984
|
+
expectTransferChecked(ixs, {
|
|
1985
|
+
source: intent.temporarySettlementTokenAccount,
|
|
1986
|
+
mint: intent.asset,
|
|
1987
|
+
destination: intent.dustRecipientUsdcAta,
|
|
1988
|
+
authority: intent.wallet,
|
|
1989
|
+
amount: null,
|
|
1990
|
+
label: "dust sweep"
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
expectCloseAccount(ixs, {
|
|
1994
|
+
account: intent.temporarySettlementTokenAccount,
|
|
1995
|
+
destination: intent.feePayer,
|
|
1996
|
+
owner: intent.wallet
|
|
1997
|
+
});
|
|
1998
|
+
expectMemo(ixs, intent.memo);
|
|
1999
|
+
if (ixs.length > 0) {
|
|
2000
|
+
reject(
|
|
2001
|
+
"unexpected_instruction",
|
|
2002
|
+
`Transaction contains ${ixs.length} unexpected trailing instruction(s)`
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
function validateDepositIntentTransaction(params) {
|
|
2007
|
+
const { intent } = params;
|
|
2008
|
+
const now = params.nowMs ?? Date.now();
|
|
2009
|
+
const policy = resolveIntentValidationPolicy(params.policy);
|
|
2010
|
+
if (new Date(intent.expiresAt).getTime() <= now) {
|
|
2011
|
+
reject("expired", "Deposit intent has expired");
|
|
2012
|
+
}
|
|
2013
|
+
assertVaultIntentTargets(intent, policy.vault);
|
|
2014
|
+
const decoded = decodeIntentTransaction({
|
|
2015
|
+
serializedTransaction: params.serializedTransaction,
|
|
2016
|
+
...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
|
|
2017
|
+
});
|
|
2018
|
+
if (decoded.messageHash !== intent.preparedMessageHash) {
|
|
2019
|
+
reject("message_hash_mismatch", "Prepared message hash mismatch");
|
|
2020
|
+
}
|
|
2021
|
+
if (decoded.feePayer !== intent.feePayer) {
|
|
2022
|
+
reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
|
|
2023
|
+
}
|
|
2024
|
+
let sawDeposit = false;
|
|
2025
|
+
for (const ix of decoded.instructions) {
|
|
2026
|
+
switch (ix.programAddress) {
|
|
2027
|
+
case COMPUTE_BUDGET_PROGRAM_ID:
|
|
2028
|
+
validateComputeBudgetInstruction(ix, policy);
|
|
2029
|
+
break;
|
|
2030
|
+
case ASSOCIATED_TOKEN_PROGRAM_ID2:
|
|
2031
|
+
expectAtaCreateForOwner(ix, intent.wallet);
|
|
2032
|
+
break;
|
|
2033
|
+
case MEMO_PROGRAM_ID:
|
|
2034
|
+
break;
|
|
2035
|
+
case KVAULT_PROGRAM_ID: {
|
|
2036
|
+
if (sawDeposit) {
|
|
2037
|
+
reject("duplicate_deposit", "A deposit intent authorizes exactly one KVault deposit");
|
|
2038
|
+
}
|
|
2039
|
+
if (!bytesStartWith(ix.data, KVAULT_DEPOSIT_DISCRIMINATOR)) {
|
|
2040
|
+
reject("unexpected_instruction", "Unexpected KVault instruction in deposit");
|
|
2041
|
+
}
|
|
2042
|
+
const maxAmount = readU64LE(ix.data, 8);
|
|
2043
|
+
if (maxAmount !== BigInt(intent.amountRawUsdc)) {
|
|
2044
|
+
reject("amount_mismatch", "Deposit amount does not match the intent");
|
|
2045
|
+
}
|
|
2046
|
+
if (ix.accounts[0] !== intent.wallet) {
|
|
2047
|
+
reject("wallet_mismatch", "Deposit user is not the agent wallet");
|
|
2048
|
+
}
|
|
2049
|
+
if (ix.accounts[1] !== intent.vault) {
|
|
2050
|
+
reject("vault_mismatch", "Deposit vault mismatch");
|
|
2051
|
+
}
|
|
2052
|
+
if (ix.accounts[3] !== intent.asset) {
|
|
2053
|
+
reject("asset_mismatch", "Deposit token mint mismatch");
|
|
2054
|
+
}
|
|
2055
|
+
if (ix.accounts[5] !== intent.shareMint) {
|
|
2056
|
+
reject("share_mint_mismatch", "Deposit share mint mismatch");
|
|
2057
|
+
}
|
|
2058
|
+
const expectedSourceAta = deriveAssociatedTokenAddress({
|
|
2059
|
+
owner: intent.wallet,
|
|
2060
|
+
mint: intent.asset
|
|
2061
|
+
});
|
|
2062
|
+
if (ix.accounts[6] !== expectedSourceAta) {
|
|
2063
|
+
reject(
|
|
2064
|
+
"source_ata_mismatch",
|
|
2065
|
+
"Deposit source must be the agent wallet's USDC ATA"
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
sawDeposit = true;
|
|
2069
|
+
break;
|
|
1228
2070
|
}
|
|
2071
|
+
default:
|
|
2072
|
+
reject(
|
|
2073
|
+
"unexpected_instruction",
|
|
2074
|
+
`Unexpected program ${ix.programAddress} in deposit transaction`
|
|
2075
|
+
);
|
|
1229
2076
|
}
|
|
1230
|
-
const url2 = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
|
|
1231
|
-
const response = await this.fetchImpl(url2, {
|
|
1232
|
-
headers: await walletAuthHeaders({
|
|
1233
|
-
signer: this.signer,
|
|
1234
|
-
method: "GET",
|
|
1235
|
-
url: url2
|
|
1236
|
-
})
|
|
1237
|
-
});
|
|
1238
|
-
const text = await response.text();
|
|
1239
|
-
if (response.status !== 200) {
|
|
1240
|
-
throw new VaultFlowClientError(
|
|
1241
|
-
"budget",
|
|
1242
|
-
`budget endpoint returned ${response.status}: ${text}`
|
|
1243
|
-
);
|
|
1244
|
-
}
|
|
1245
|
-
let parsed;
|
|
1246
|
-
try {
|
|
1247
|
-
parsed = JSON.parse(text);
|
|
1248
|
-
} catch {
|
|
1249
|
-
throw new VaultFlowClientError(
|
|
1250
|
-
"budget",
|
|
1251
|
-
"budget endpoint returned 200 with a non-JSON body",
|
|
1252
|
-
text
|
|
1253
|
-
);
|
|
1254
|
-
}
|
|
1255
|
-
const body2 = parsed;
|
|
1256
|
-
return {
|
|
1257
|
-
wallet: this.signer.walletAddress,
|
|
1258
|
-
principalBasisRawUsdc: body2.position?.principalBasisRawUsdc ?? "0",
|
|
1259
|
-
positionValueRawUsdc: body2.budget?.positionValueRawUsdc ?? "0",
|
|
1260
|
-
grossYieldRawUsdc: body2.budget?.grossYieldRawUsdc ?? "0",
|
|
1261
|
-
spendableYieldRawUsdc: body2.budget?.spendableYieldRawUsdc ?? "0"
|
|
1262
|
-
};
|
|
1263
2077
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
await this.postJson("submit", "/v1/payments/report", {
|
|
1267
|
-
wallet: this.signer.walletAddress,
|
|
1268
|
-
withdrawalId: input.withdrawalId,
|
|
1269
|
-
paymentTxSignature: input.paymentTxSignature
|
|
1270
|
-
});
|
|
2078
|
+
if (!sawDeposit) {
|
|
2079
|
+
reject("missing_instruction", "Deposit transaction has no KVault deposit");
|
|
1271
2080
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
2081
|
+
}
|
|
2082
|
+
function validateWithdrawalIntentTransaction(params) {
|
|
2083
|
+
const { intent } = params;
|
|
2084
|
+
const now = params.nowMs ?? Date.now();
|
|
2085
|
+
const policy = resolveIntentValidationPolicy(params.policy);
|
|
2086
|
+
if (new Date(intent.expiresAt).getTime() <= now) {
|
|
2087
|
+
reject("expired", "Withdrawal intent has expired");
|
|
1278
2088
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
...input.policy === void 0 ? {} : { policy: input.policy },
|
|
1289
|
-
...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
|
|
1290
|
-
...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
|
|
1291
|
-
...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
|
|
1292
|
-
}
|
|
2089
|
+
assertVaultIntentTargets(intent, policy.vault);
|
|
2090
|
+
const expectedDestination = deriveAssociatedTokenAddress({
|
|
2091
|
+
owner: intent.wallet,
|
|
2092
|
+
mint: intent.asset
|
|
2093
|
+
});
|
|
2094
|
+
if (expectedDestination !== intent.destinationUsdcAta) {
|
|
2095
|
+
reject(
|
|
2096
|
+
"destination_mismatch",
|
|
2097
|
+
"Withdrawal destination must be the agent wallet's USDC ATA"
|
|
1293
2098
|
);
|
|
1294
2099
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
const parsed = parseRelayerError(text);
|
|
1302
|
-
throw new VaultFlowClientError(
|
|
1303
|
-
"read",
|
|
1304
|
-
parsed.message ?? `setup session read failed with ${response.status}`,
|
|
1305
|
-
text,
|
|
1306
|
-
parsed.code,
|
|
1307
|
-
parsed.details
|
|
1308
|
-
);
|
|
1309
|
-
}
|
|
1310
|
-
return JSON.parse(text);
|
|
2100
|
+
const decoded = decodeIntentTransaction({
|
|
2101
|
+
serializedTransaction: params.serializedTransaction,
|
|
2102
|
+
...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
|
|
2103
|
+
});
|
|
2104
|
+
if (decoded.messageHash !== intent.preparedMessageHash) {
|
|
2105
|
+
reject("message_hash_mismatch", "Prepared message hash mismatch");
|
|
1311
2106
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
* amount — the shape the mandate's initialDeposit approval has.
|
|
1315
|
-
*/
|
|
1316
|
-
async findApprovedDepositApproval(amountRawUsdc) {
|
|
1317
|
-
try {
|
|
1318
|
-
const approvals = await this.listApprovals("approved");
|
|
1319
|
-
const match = approvals.find((approval) => {
|
|
1320
|
-
const binding = approval.binding;
|
|
1321
|
-
return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc.toString();
|
|
1322
|
-
});
|
|
1323
|
-
return match?.approvalId;
|
|
1324
|
-
} catch {
|
|
1325
|
-
return void 0;
|
|
1326
|
-
}
|
|
2107
|
+
if (decoded.feePayer !== intent.feePayer) {
|
|
2108
|
+
reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
|
|
1327
2109
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
2110
|
+
let sawWithdraw = false;
|
|
2111
|
+
let farmUserState = null;
|
|
2112
|
+
let farmInstructionCount = 0;
|
|
2113
|
+
for (const ix of decoded.instructions) {
|
|
2114
|
+
switch (ix.programAddress) {
|
|
2115
|
+
case COMPUTE_BUDGET_PROGRAM_ID:
|
|
2116
|
+
validateComputeBudgetInstruction(ix, policy);
|
|
2117
|
+
break;
|
|
2118
|
+
case MEMO_PROGRAM_ID:
|
|
2119
|
+
break;
|
|
2120
|
+
case KAMINO_FARMS_PROGRAM_ID:
|
|
2121
|
+
farmInstructionCount += 1;
|
|
2122
|
+
if (farmInstructionCount === 1) {
|
|
2123
|
+
farmUserState = validateFarmUnstakeInstruction(ix, intent);
|
|
2124
|
+
} else if (farmInstructionCount === 2) {
|
|
2125
|
+
validateFarmWithdrawInstruction(ix, intent, farmUserState);
|
|
2126
|
+
} else {
|
|
2127
|
+
reject(
|
|
2128
|
+
"farm_instruction_mismatch",
|
|
2129
|
+
"Withdrawal may contain only one farm unstake and one farm withdrawal"
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
break;
|
|
2133
|
+
case ASSOCIATED_TOKEN_PROGRAM_ID2:
|
|
2134
|
+
expectAtaCreateForOwner(ix, intent.wallet);
|
|
2135
|
+
break;
|
|
2136
|
+
case SPL_TOKEN_PROGRAM_ID: {
|
|
2137
|
+
if (ix.data[0] !== 9) {
|
|
2138
|
+
reject(
|
|
2139
|
+
"unexpected_instruction",
|
|
2140
|
+
"Only CloseAccount token instructions are allowed in withdrawals"
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
if (ix.accounts[1] !== intent.wallet || ix.accounts[2] !== intent.wallet) {
|
|
2144
|
+
reject(
|
|
2145
|
+
"unexpected_instruction",
|
|
2146
|
+
"Withdrawal CloseAccount must pay out to the agent wallet"
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
break;
|
|
1355
2150
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
|
|
1381
|
-
text,
|
|
1382
|
-
parsed.code,
|
|
1383
|
-
parsed.details
|
|
1384
|
-
);
|
|
1385
|
-
}
|
|
1386
|
-
try {
|
|
1387
|
-
return JSON.parse(text);
|
|
1388
|
-
} catch {
|
|
1389
|
-
throw new VaultFlowClientError(
|
|
1390
|
-
step,
|
|
1391
|
-
`${path} returned 200 with a non-JSON body`,
|
|
1392
|
-
text
|
|
1393
|
-
);
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
async getJson(path) {
|
|
1397
|
-
const url2 = `${this.baseUrl}${path}`;
|
|
1398
|
-
const response = await this.fetchImpl(url2, {
|
|
1399
|
-
headers: await walletAuthHeaders({
|
|
1400
|
-
signer: this.signer,
|
|
1401
|
-
method: "GET",
|
|
1402
|
-
url: url2
|
|
1403
|
-
})
|
|
1404
|
-
});
|
|
1405
|
-
const text = await response.text();
|
|
1406
|
-
if (response.status !== 200) {
|
|
1407
|
-
const parsed = parseRelayerError(text);
|
|
1408
|
-
throw new VaultFlowClientError(
|
|
1409
|
-
"read",
|
|
1410
|
-
parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
|
|
1411
|
-
text,
|
|
1412
|
-
parsed.code,
|
|
1413
|
-
parsed.details
|
|
1414
|
-
);
|
|
1415
|
-
}
|
|
1416
|
-
try {
|
|
1417
|
-
return JSON.parse(text);
|
|
1418
|
-
} catch {
|
|
1419
|
-
throw new VaultFlowClientError(
|
|
1420
|
-
"read",
|
|
1421
|
-
`${path} returned 200 with a non-JSON body`,
|
|
1422
|
-
text
|
|
1423
|
-
);
|
|
2151
|
+
case KVAULT_PROGRAM_ID: {
|
|
2152
|
+
if (sawWithdraw) {
|
|
2153
|
+
reject(
|
|
2154
|
+
"withdraw_mismatch",
|
|
2155
|
+
"Withdrawal may contain only one KVault withdraw instruction"
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
validateKvaultWithdrawInstruction(ix, {
|
|
2159
|
+
wallet: intent.wallet,
|
|
2160
|
+
vault: intent.vault,
|
|
2161
|
+
shareMint: intent.shareMint,
|
|
2162
|
+
asset: intent.asset,
|
|
2163
|
+
userTokenAccount: intent.destinationUsdcAta,
|
|
2164
|
+
maxSharesToRedeemRaw: BigInt(intent.maxSharesToRedeemRaw),
|
|
2165
|
+
allowFullExit: intent.allowFullExit
|
|
2166
|
+
});
|
|
2167
|
+
sawWithdraw = true;
|
|
2168
|
+
break;
|
|
2169
|
+
}
|
|
2170
|
+
default:
|
|
2171
|
+
reject(
|
|
2172
|
+
"unexpected_instruction",
|
|
2173
|
+
`Unexpected program ${ix.programAddress} in withdrawal transaction`
|
|
2174
|
+
);
|
|
1424
2175
|
}
|
|
1425
2176
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
};
|
|
1435
|
-
} catch {
|
|
1436
|
-
return { code: null, message: null, details: null };
|
|
2177
|
+
if (!sawWithdraw) {
|
|
2178
|
+
reject("missing_instruction", "Withdrawal transaction has no KVault withdraw");
|
|
2179
|
+
}
|
|
2180
|
+
if (farmInstructionCount === 1) {
|
|
2181
|
+
reject(
|
|
2182
|
+
"farm_instruction_mismatch",
|
|
2183
|
+
"A farm unstake must be followed by a farm withdrawal"
|
|
2184
|
+
);
|
|
1437
2185
|
}
|
|
1438
2186
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
var RelayerRealizeError = class extends Error {
|
|
1443
|
-
constructor(code, message, detail = null) {
|
|
1444
|
-
super(message);
|
|
1445
|
-
this.code = code;
|
|
1446
|
-
this.detail = detail;
|
|
1447
|
-
this.name = "RelayerRealizeError";
|
|
2187
|
+
function assertVaultIntentTargets(intent, vault) {
|
|
2188
|
+
if (intent.vault !== vault.address) {
|
|
2189
|
+
reject("vault_mismatch", "Unsupported vault");
|
|
1448
2190
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
};
|
|
1452
|
-
var RelayerYieldRealizer = class {
|
|
1453
|
-
vaultFlows;
|
|
1454
|
-
constructor(config) {
|
|
1455
|
-
this.vaultFlows = new VaultFlowClient({
|
|
1456
|
-
relayerBaseUrl: config.relayerBaseUrl,
|
|
1457
|
-
signer: config.signer,
|
|
1458
|
-
rpc: config.rpc,
|
|
1459
|
-
...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
|
|
1460
|
-
...config.lookupTablesFor === void 0 ? {} : { lookupTablesFor: config.lookupTablesFor }
|
|
1461
|
-
});
|
|
2191
|
+
if (intent.shareMint !== vault.shareMint) {
|
|
2192
|
+
reject("share_mint_mismatch", "Unsupported share mint");
|
|
1462
2193
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
await this.assertSpendableYield(shortfallRawUsdc);
|
|
1466
|
-
let outcome;
|
|
1467
|
-
try {
|
|
1468
|
-
outcome = await this.vaultFlows.withdraw({
|
|
1469
|
-
amountRawUsdc: shortfallRawUsdc,
|
|
1470
|
-
// The relayer refuses to prepare this withdrawal beyond the spendable
|
|
1471
|
-
// yield — the principal-protection guard the client cannot bypass.
|
|
1472
|
-
purpose: "yield_realize",
|
|
1473
|
-
// Declares what is being paid so the relayer's spending-mandate layer
|
|
1474
|
-
// can enforce caps/payee and keep the mandate → payment audit chain.
|
|
1475
|
-
...input.payment === void 0 ? {} : { payment: input.payment },
|
|
1476
|
-
...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
|
|
1477
|
-
});
|
|
1478
|
-
} catch (error) {
|
|
1479
|
-
throw this.mapWithdrawError(error);
|
|
1480
|
-
}
|
|
1481
|
-
if (outcome.status !== "confirmed" || outcome.txSignature === null) {
|
|
1482
|
-
throw new RelayerRealizeError(
|
|
1483
|
-
"realize_not_confirmed",
|
|
1484
|
-
`yield realize withdrawal did not confirm (status=${outcome.status})`,
|
|
1485
|
-
outcome
|
|
1486
|
-
);
|
|
1487
|
-
}
|
|
1488
|
-
return {
|
|
1489
|
-
realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
|
|
1490
|
-
txSignature: outcome.txSignature,
|
|
1491
|
-
withdrawalId: outcome.withdrawalId
|
|
1492
|
-
};
|
|
2194
|
+
if (intent.farm !== vault.farm) {
|
|
2195
|
+
reject("farm_mismatch", "Unsupported Kamino farm");
|
|
1493
2196
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
* closes the relayer's mandate → realize → payment audit chain. Callers
|
|
1497
|
-
* must never let a failure here affect the payment result.
|
|
1498
|
-
*/
|
|
1499
|
-
async reportPayment(input) {
|
|
1500
|
-
await this.vaultFlows.reportPayment(input);
|
|
2197
|
+
if (intent.asset !== vault.usdcMint) {
|
|
2198
|
+
reject("asset_mismatch", "Only USDC is supported");
|
|
1501
2199
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
} catch (error) {
|
|
1513
|
-
throw new RelayerRealizeError(
|
|
1514
|
-
"budget_unavailable",
|
|
1515
|
-
"could not read the spendable-yield budget",
|
|
1516
|
-
error
|
|
1517
|
-
);
|
|
1518
|
-
}
|
|
1519
|
-
const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
|
|
1520
|
-
if (spendable < requiredRawUsdc) {
|
|
1521
|
-
throw new RelayerRealizeError(
|
|
1522
|
-
"insufficient_yield",
|
|
1523
|
-
`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`,
|
|
1524
|
-
{ spendableYieldRawUsdc: spendable.toString() }
|
|
1525
|
-
);
|
|
1526
|
-
}
|
|
2200
|
+
}
|
|
2201
|
+
function resolveIntentValidationPolicy(policy) {
|
|
2202
|
+
const resolved = {
|
|
2203
|
+
vault: policy?.vault ?? SUBLY_VAULT,
|
|
2204
|
+
maxComputeUnitLimit: policy?.maxComputeUnitLimit ?? DEFAULT_MAX_COMPUTE_UNIT_LIMIT,
|
|
2205
|
+
maxComputeUnitPriceMicroLamports: policy?.maxComputeUnitPriceMicroLamports ?? DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
|
|
2206
|
+
maxTemporaryAccountLamports: policy?.maxTemporaryAccountLamports ?? MAX_TEMP_ACCOUNT_LAMPORTS
|
|
2207
|
+
};
|
|
2208
|
+
if (!Number.isSafeInteger(resolved.maxComputeUnitLimit) || resolved.maxComputeUnitLimit <= 0) {
|
|
2209
|
+
reject("invalid_policy", "maxComputeUnitLimit must be a positive safe integer");
|
|
1527
2210
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
2211
|
+
if (resolved.maxComputeUnitPriceMicroLamports < 0n) {
|
|
2212
|
+
reject(
|
|
2213
|
+
"invalid_policy",
|
|
2214
|
+
"maxComputeUnitPriceMicroLamports must be non-negative"
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
if (resolved.maxTemporaryAccountLamports <= 0n) {
|
|
2218
|
+
reject("invalid_policy", "maxTemporaryAccountLamports must be positive");
|
|
2219
|
+
}
|
|
2220
|
+
return resolved;
|
|
2221
|
+
}
|
|
2222
|
+
function expectComputeBudgetPair(ixs, policy) {
|
|
2223
|
+
for (const discriminator of [2, 3]) {
|
|
2224
|
+
const ix = ixs.shift();
|
|
2225
|
+
if (ix === void 0 || ix.programAddress !== COMPUTE_BUDGET_PROGRAM_ID || ix.data[0] !== discriminator) {
|
|
2226
|
+
reject(
|
|
2227
|
+
"compute_budget_mismatch",
|
|
2228
|
+
"Transaction must start with ComputeBudget limit and price instructions"
|
|
1534
2229
|
);
|
|
1535
2230
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
);
|
|
2231
|
+
validateComputeBudgetInstruction(ix, policy);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
function validateComputeBudgetInstruction(ix, policy) {
|
|
2235
|
+
switch (ix.data[0]) {
|
|
2236
|
+
case 2: {
|
|
2237
|
+
const units = readU32LE(ix.data, 1);
|
|
2238
|
+
if (units <= 0 || units > policy.maxComputeUnitLimit) {
|
|
2239
|
+
reject(
|
|
2240
|
+
"compute_budget_mismatch",
|
|
2241
|
+
`Compute unit limit ${units} exceeds policy maximum ${policy.maxComputeUnitLimit}`
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
break;
|
|
1543
2245
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
2246
|
+
case 3: {
|
|
2247
|
+
const microLamports = readU64LE(ix.data, 1);
|
|
2248
|
+
if (microLamports > policy.maxComputeUnitPriceMicroLamports) {
|
|
2249
|
+
reject(
|
|
2250
|
+
"compute_budget_mismatch",
|
|
2251
|
+
`Compute unit price ${microLamports} exceeds policy maximum ${policy.maxComputeUnitPriceMicroLamports}`
|
|
2252
|
+
);
|
|
2253
|
+
}
|
|
2254
|
+
break;
|
|
1550
2255
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
2256
|
+
default:
|
|
2257
|
+
reject("compute_budget_mismatch", "Unexpected ComputeBudget instruction");
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
function expectCreateTemporaryAccount(ixs, intent, policy) {
|
|
2261
|
+
const ix = ixs.shift();
|
|
2262
|
+
if (ix === void 0 || ix.programAddress !== SYSTEM_PROGRAM_ID) {
|
|
2263
|
+
reject("temp_account_mismatch", "Expected System createAccount instruction");
|
|
2264
|
+
}
|
|
2265
|
+
if (ix.data.length < 52 || readU32LE(ix.data, 0) !== 0) {
|
|
2266
|
+
reject("temp_account_mismatch", "Expected createAccount discriminator");
|
|
2267
|
+
}
|
|
2268
|
+
const lamports = readU64LE(ix.data, 4);
|
|
2269
|
+
const space = readU64LE(ix.data, 12);
|
|
2270
|
+
const owner = bs586.encode(ix.data.subarray(20, 52));
|
|
2271
|
+
if (space !== 165n) {
|
|
2272
|
+
reject("temp_account_mismatch", "Temporary account space must be 165 bytes");
|
|
2273
|
+
}
|
|
2274
|
+
if (owner !== SPL_TOKEN_PROGRAM_ID) {
|
|
2275
|
+
reject("temp_account_mismatch", "Temporary account owner must be the token program");
|
|
2276
|
+
}
|
|
2277
|
+
if (lamports > policy.maxTemporaryAccountLamports) {
|
|
2278
|
+
reject("temp_account_mismatch", "Temporary account rent exceeds the cap");
|
|
2279
|
+
}
|
|
2280
|
+
if (ix.accounts[0] !== intent.feePayer) {
|
|
2281
|
+
reject("temp_account_mismatch", "Temporary account must be funded by the sponsor");
|
|
2282
|
+
}
|
|
2283
|
+
if (ix.accounts[1] !== intent.temporarySettlementTokenAccount) {
|
|
2284
|
+
reject("temp_account_mismatch", "createAccount target is not the temporary account");
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
function expectInitializeTemporaryAccount(ixs, intent) {
|
|
2288
|
+
const ix = ixs.shift();
|
|
2289
|
+
if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 18) {
|
|
2290
|
+
reject("temp_account_mismatch", "Expected InitializeAccount3 instruction");
|
|
2291
|
+
}
|
|
2292
|
+
const owner = bs586.encode(ix.data.subarray(1, 33));
|
|
2293
|
+
if (owner !== intent.wallet) {
|
|
2294
|
+
reject(
|
|
2295
|
+
"temp_account_mismatch",
|
|
2296
|
+
"Temporary account token authority must be the agent wallet"
|
|
1555
2297
|
);
|
|
1556
2298
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
2299
|
+
if (ix.accounts[0] !== intent.temporarySettlementTokenAccount) {
|
|
2300
|
+
reject("temp_account_mismatch", "InitializeAccount3 target mismatch");
|
|
2301
|
+
}
|
|
2302
|
+
if (ix.accounts[1] !== intent.asset) {
|
|
2303
|
+
reject("temp_account_mismatch", "Temporary account mint must be USDC");
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
function consumeFarmInstructions(ixs, intent) {
|
|
2307
|
+
if (ixs[0]?.programAddress !== KAMINO_FARMS_PROGRAM_ID) {
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
const unstake = ixs.shift();
|
|
2311
|
+
const userState = validateFarmUnstakeInstruction(unstake, intent);
|
|
2312
|
+
if (ixs[0]?.programAddress !== KAMINO_FARMS_PROGRAM_ID) {
|
|
2313
|
+
reject(
|
|
2314
|
+
"farm_instruction_mismatch",
|
|
2315
|
+
"A farm unstake must be followed by a farm withdrawal"
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
const withdraw = ixs.shift();
|
|
2319
|
+
validateFarmWithdrawInstruction(withdraw, intent, userState);
|
|
2320
|
+
if (ixs[0]?.programAddress === KAMINO_FARMS_PROGRAM_ID) {
|
|
2321
|
+
reject(
|
|
2322
|
+
"farm_instruction_mismatch",
|
|
2323
|
+
"Payment may contain only one farm unstake and one farm withdrawal"
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
var KAMINO_FARMS_UNSTAKE_DISCRIMINATOR = Uint8Array.from([
|
|
2328
|
+
90,
|
|
2329
|
+
95,
|
|
2330
|
+
107,
|
|
2331
|
+
42,
|
|
2332
|
+
205,
|
|
2333
|
+
124,
|
|
2334
|
+
50,
|
|
2335
|
+
225
|
|
2336
|
+
]);
|
|
2337
|
+
var KAMINO_FARMS_WITHDRAW_UNSTAKED_DISCRIMINATOR = Uint8Array.from([
|
|
2338
|
+
36,
|
|
2339
|
+
102,
|
|
2340
|
+
187,
|
|
2341
|
+
49,
|
|
2342
|
+
220,
|
|
2343
|
+
36,
|
|
2344
|
+
132,
|
|
2345
|
+
67
|
|
2346
|
+
]);
|
|
2347
|
+
function validateFarmUnstakeInstruction(ix, intent) {
|
|
2348
|
+
if (ix.programAddress !== KAMINO_FARMS_PROGRAM_ID || !bytesStartWith(ix.data, KAMINO_FARMS_UNSTAKE_DISCRIMINATOR) || ix.data.length !== 24 || readU128LE(ix.data, 8) <= 0n) {
|
|
2349
|
+
reject(
|
|
2350
|
+
"farm_instruction_mismatch",
|
|
2351
|
+
"Expected a non-zero Kamino farm unstake instruction"
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
if (ix.accounts.length !== 4) {
|
|
2355
|
+
reject(
|
|
2356
|
+
"farm_instruction_mismatch",
|
|
2357
|
+
"Farm unstake account list is not canonical"
|
|
2358
|
+
);
|
|
1561
2359
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
return typeof parsed.error?.code === "string" ? parsed.error.code : null;
|
|
1565
|
-
} catch {
|
|
1566
|
-
return null;
|
|
2360
|
+
if (ix.accounts[0] !== intent.wallet) {
|
|
2361
|
+
reject("farm_instruction_mismatch", "Farm unstake owner must be the agent wallet");
|
|
1567
2362
|
}
|
|
2363
|
+
if (ix.accounts[2] !== intent.farm) {
|
|
2364
|
+
reject("farm_instruction_mismatch", "Farm unstake target is not the approved farm");
|
|
2365
|
+
}
|
|
2366
|
+
return ix.accounts[1];
|
|
1568
2367
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
// ../../src/x402/headers.ts
|
|
1577
|
-
import { z } from "zod";
|
|
1578
|
-
var PAYMENT_REQUIRED_HEADER = "payment-required";
|
|
1579
|
-
var MAX_HEADER_JSON_BYTES = 16384;
|
|
1580
|
-
var X402HeaderError = class extends Error {
|
|
1581
|
-
reason;
|
|
1582
|
-
constructor(reason, message) {
|
|
1583
|
-
super(message);
|
|
1584
|
-
this.name = "X402HeaderError";
|
|
1585
|
-
this.reason = reason;
|
|
2368
|
+
function validateFarmWithdrawInstruction(ix, intent, expectedUserState) {
|
|
2369
|
+
if (ix.programAddress !== KAMINO_FARMS_PROGRAM_ID || !bytesStartWith(ix.data, KAMINO_FARMS_WITHDRAW_UNSTAKED_DISCRIMINATOR) || ix.data.length !== 8) {
|
|
2370
|
+
reject(
|
|
2371
|
+
"farm_instruction_mismatch",
|
|
2372
|
+
"Expected a canonical Kamino farm withdrawal instruction"
|
|
2373
|
+
);
|
|
1586
2374
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
asset: z.string().min(32),
|
|
1592
|
-
/** Exact seller amount in raw USDC; the scheme settles exactly this. */
|
|
1593
|
-
amountRawUsdc: z.string().regex(/^[1-9]\d*$/),
|
|
1594
|
-
resource: z.string().url(),
|
|
1595
|
-
description: z.string().optional(),
|
|
1596
|
-
mimeType: z.string().optional(),
|
|
1597
|
-
payTo: z.string().min(32),
|
|
1598
|
-
maxTimeoutSeconds: z.number().int().positive(),
|
|
1599
|
-
extra: z.object({
|
|
1600
|
-
sellerRequestId: z.string().min(1),
|
|
1601
|
-
seller: z.string().min(32),
|
|
1602
|
-
sellerUsdcAta: z.string().min(32),
|
|
1603
|
-
vault: z.string().min(32),
|
|
1604
|
-
shareMint: z.string().min(32)
|
|
1605
|
-
})
|
|
1606
|
-
}).loose();
|
|
1607
|
-
var paymentRequiredSchema = z.object({
|
|
1608
|
-
x402Version: z.number().int(),
|
|
1609
|
-
accepts: z.array(z.unknown()),
|
|
1610
|
-
error: z.string().optional()
|
|
1611
|
-
}).loose();
|
|
1612
|
-
var sublyPaymentPayloadSchema = z.object({
|
|
1613
|
-
x402Version: z.number().int(),
|
|
1614
|
-
scheme: z.literal(PAYMENT_SCHEME),
|
|
1615
|
-
network: z.string().min(1),
|
|
1616
|
-
payload: z.object({
|
|
1617
|
-
paymentId: z.string().min(1),
|
|
1618
|
-
requestBindingHash: z.string().min(1),
|
|
1619
|
-
preparedMessageHash: z.string().min(1),
|
|
1620
|
-
serializedTransaction: z.string().min(1).max(4096),
|
|
1621
|
-
agentSignature: z.string().min(1).max(128),
|
|
1622
|
-
temporarySettlementSignature: z.string().min(1).max(128)
|
|
1623
|
-
})
|
|
1624
|
-
}).loose();
|
|
1625
|
-
function decodeX402Header(headerValue) {
|
|
1626
|
-
if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
|
|
1627
|
-
throw new X402HeaderError(
|
|
1628
|
-
"header_too_large",
|
|
1629
|
-
`x402 header exceeds ${MAX_HEADER_JSON_BYTES} encoded bytes`
|
|
2375
|
+
if (ix.accounts.length !== 7) {
|
|
2376
|
+
reject(
|
|
2377
|
+
"farm_instruction_mismatch",
|
|
2378
|
+
"Farm withdrawal account list is not canonical"
|
|
1630
2379
|
);
|
|
1631
2380
|
}
|
|
1632
|
-
const
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
"
|
|
2381
|
+
const expectedSharesAta = deriveAssociatedTokenAddress({
|
|
2382
|
+
owner: intent.wallet,
|
|
2383
|
+
mint: intent.shareMint
|
|
2384
|
+
});
|
|
2385
|
+
if (ix.accounts[0] !== intent.wallet || ix.accounts[1] !== expectedUserState || ix.accounts[2] !== intent.farm || ix.accounts[3] !== expectedSharesAta || ix.accounts[6] !== SPL_TOKEN_PROGRAM_ID) {
|
|
2386
|
+
reject(
|
|
2387
|
+
"farm_instruction_mismatch",
|
|
2388
|
+
"Farm withdrawal must return the approved vault shares to the agent wallet"
|
|
1639
2389
|
);
|
|
1640
2390
|
}
|
|
1641
2391
|
}
|
|
1642
|
-
function
|
|
1643
|
-
|
|
1644
|
-
|
|
2392
|
+
function expectKvaultWithdraw(ixs, expectation) {
|
|
2393
|
+
const ix = ixs.shift();
|
|
2394
|
+
if (ix === void 0 || ix.programAddress !== KVAULT_PROGRAM_ID) {
|
|
2395
|
+
reject("withdraw_mismatch", "Expected KVault withdraw instruction");
|
|
1645
2396
|
}
|
|
1646
|
-
|
|
1647
|
-
typeof body2 === "string" ? Buffer.from(body2, "utf8") : Buffer.from(body2)
|
|
1648
|
-
);
|
|
2397
|
+
validateKvaultWithdrawInstruction(ix, expectation);
|
|
1649
2398
|
}
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
/** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
|
|
1659
|
-
asset: z2.string().min(1),
|
|
1660
|
-
/** Exact price in the asset's atomic units, as a decimal string. */
|
|
1661
|
-
amount: z2.string().regex(/^[1-9]\d*$/),
|
|
1662
|
-
/** Recipient wallet; the transfer destination ATA is derived from it. */
|
|
1663
|
-
payTo: z2.string().min(1),
|
|
1664
|
-
maxTimeoutSeconds: z2.number().int().positive().optional(),
|
|
1665
|
-
extra: z2.object({
|
|
1666
|
-
/** Facilitator address that pays the tx fee (gas sponsorship). */
|
|
1667
|
-
feePayer: z2.string().min(1).optional()
|
|
1668
|
-
}).loose().optional()
|
|
1669
|
-
}).loose();
|
|
1670
|
-
var standardPaymentRequiredSchema = z2.object({
|
|
1671
|
-
x402Version: z2.number().int(),
|
|
1672
|
-
accepts: z2.array(z2.unknown()),
|
|
1673
|
-
error: z2.string().optional(),
|
|
1674
|
-
resource: z2.object({ url: z2.string().optional() }).loose().optional()
|
|
1675
|
-
}).loose();
|
|
1676
|
-
var StandardX402ChallengeError = class extends Error {
|
|
1677
|
-
reason;
|
|
1678
|
-
constructor(reason, message) {
|
|
1679
|
-
super(message);
|
|
1680
|
-
this.name = "StandardX402ChallengeError";
|
|
1681
|
-
this.reason = reason;
|
|
2399
|
+
function validateKvaultWithdrawInstruction(ix, expectation) {
|
|
2400
|
+
const isWithdraw = bytesStartWith(ix.data, KVAULT_WITHDRAW_DISCRIMINATOR);
|
|
2401
|
+
const isWithdrawFromAvailable = bytesStartWith(
|
|
2402
|
+
ix.data,
|
|
2403
|
+
KVAULT_WITHDRAW_FROM_AVAILABLE_DISCRIMINATOR
|
|
2404
|
+
);
|
|
2405
|
+
if (!isWithdraw && !isWithdrawFromAvailable) {
|
|
2406
|
+
reject("withdraw_mismatch", "Unexpected KVault instruction");
|
|
1682
2407
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
2408
|
+
const sharesAmount = readU64LE(ix.data, 8);
|
|
2409
|
+
const fullExit = sharesAmount === U64_MAX;
|
|
2410
|
+
if (fullExit && !expectation.allowFullExit) {
|
|
2411
|
+
reject("withdraw_mismatch", "Full-exit share burn is not allowed for this intent");
|
|
2412
|
+
}
|
|
2413
|
+
if (!fullExit && sharesAmount > expectation.maxSharesToRedeemRaw) {
|
|
2414
|
+
reject(
|
|
2415
|
+
"shares_exceed_max",
|
|
2416
|
+
`Withdraw burns ${sharesAmount} shares which exceeds the approved maximum ${expectation.maxSharesToRedeemRaw}`
|
|
1690
2417
|
);
|
|
1691
2418
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
2419
|
+
if (ix.accounts[0] !== expectation.wallet) {
|
|
2420
|
+
reject("withdraw_mismatch", "Withdraw user is not the agent wallet");
|
|
2421
|
+
}
|
|
2422
|
+
if (ix.accounts[1] !== expectation.vault) {
|
|
2423
|
+
reject("withdraw_mismatch", "Withdraw vault mismatch");
|
|
2424
|
+
}
|
|
2425
|
+
if (ix.accounts[5] !== expectation.userTokenAccount) {
|
|
2426
|
+
reject(
|
|
2427
|
+
"withdraw_mismatch",
|
|
2428
|
+
"Withdraw token destination is not the approved account"
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2431
|
+
if (ix.accounts[6] !== expectation.asset) {
|
|
2432
|
+
reject("withdraw_mismatch", "Withdraw token mint mismatch");
|
|
2433
|
+
}
|
|
2434
|
+
const expectedSharesAta = deriveAssociatedTokenAddress({
|
|
2435
|
+
owner: expectation.wallet,
|
|
2436
|
+
mint: expectation.shareMint
|
|
1698
2437
|
});
|
|
1699
|
-
|
|
2438
|
+
if (ix.accounts[7] !== expectedSharesAta) {
|
|
2439
|
+
reject("withdraw_mismatch", "Withdraw share source must be the agent share ATA");
|
|
2440
|
+
}
|
|
2441
|
+
if (ix.accounts[8] !== expectation.shareMint) {
|
|
2442
|
+
reject("withdraw_mismatch", "Withdraw share mint mismatch");
|
|
2443
|
+
}
|
|
1700
2444
|
}
|
|
1701
|
-
function
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
2445
|
+
function expectTransferChecked(ixs, expectation) {
|
|
2446
|
+
const ix = ixs.shift();
|
|
2447
|
+
if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 12) {
|
|
2448
|
+
reject("transfer_mismatch", `Expected TransferChecked for ${expectation.label}`);
|
|
2449
|
+
}
|
|
2450
|
+
const amount = readU64LE(ix.data, 1);
|
|
2451
|
+
const decimals = ix.data[9];
|
|
2452
|
+
if (expectation.amount !== null && amount !== expectation.amount) {
|
|
2453
|
+
reject(
|
|
2454
|
+
"amount_mismatch",
|
|
2455
|
+
`${expectation.label} amount ${amount} does not match ${expectation.amount}`
|
|
1709
2456
|
);
|
|
1710
2457
|
}
|
|
1711
|
-
|
|
2458
|
+
if (decimals !== USDC_DECIMALS) {
|
|
2459
|
+
reject("transfer_mismatch", `${expectation.label} has unexpected decimals`);
|
|
2460
|
+
}
|
|
2461
|
+
if (ix.accounts[0] !== expectation.source) {
|
|
2462
|
+
reject("transfer_mismatch", `${expectation.label} source mismatch`);
|
|
2463
|
+
}
|
|
2464
|
+
if (ix.accounts[1] !== expectation.mint) {
|
|
2465
|
+
reject("transfer_mismatch", `${expectation.label} mint mismatch`);
|
|
2466
|
+
}
|
|
2467
|
+
if (ix.accounts[2] !== expectation.destination) {
|
|
2468
|
+
reject("transfer_mismatch", `${expectation.label} destination mismatch`);
|
|
2469
|
+
}
|
|
2470
|
+
if (ix.accounts[3] !== expectation.authority) {
|
|
2471
|
+
reject("transfer_mismatch", `${expectation.label} authority mismatch`);
|
|
2472
|
+
}
|
|
1712
2473
|
}
|
|
1713
|
-
function
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
(candidate) => candidate.network === network && candidate.asset === usdcMint
|
|
1718
|
-
);
|
|
1719
|
-
if (matchingRequirements.length === 0) {
|
|
1720
|
-
throw new StandardX402ChallengeError(
|
|
1721
|
-
"no_payable_requirement",
|
|
1722
|
-
`The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
|
|
1723
|
-
);
|
|
2474
|
+
function expectCloseAccount(ixs, expectation) {
|
|
2475
|
+
const ix = ixs.shift();
|
|
2476
|
+
if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 9) {
|
|
2477
|
+
reject("close_mismatch", "Expected CloseAccount instruction");
|
|
1724
2478
|
}
|
|
1725
|
-
|
|
1726
|
-
(
|
|
1727
|
-
) ?? null;
|
|
1728
|
-
if (requirement === null) {
|
|
1729
|
-
throw new StandardX402ChallengeError(
|
|
1730
|
-
"missing_svm_fee_payer",
|
|
1731
|
-
"The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
|
|
1732
|
-
);
|
|
2479
|
+
if (ix.accounts[0] !== expectation.account) {
|
|
2480
|
+
reject("close_mismatch", "CloseAccount target is not the temporary account");
|
|
1733
2481
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
2482
|
+
if (ix.accounts[1] !== expectation.destination) {
|
|
2483
|
+
reject("close_mismatch", "CloseAccount rent destination must be the sponsor");
|
|
2484
|
+
}
|
|
2485
|
+
if (ix.accounts[2] !== expectation.owner) {
|
|
2486
|
+
reject("close_mismatch", "CloseAccount authority must be the agent wallet");
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
function expectMemo(ixs, memo) {
|
|
2490
|
+
const ix = ixs.shift();
|
|
2491
|
+
if (ix === void 0 || ix.programAddress !== MEMO_PROGRAM_ID) {
|
|
2492
|
+
reject("memo_mismatch", "Expected Memo instruction");
|
|
2493
|
+
}
|
|
2494
|
+
if (Buffer.from(ix.data).toString("utf8") !== memo) {
|
|
2495
|
+
reject("memo_mismatch", "Memo content does not match the paymentId");
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
function expectAtaCreateForOwner(ix, owner) {
|
|
2499
|
+
if (ix.accounts[2] !== owner) {
|
|
2500
|
+
reject(
|
|
2501
|
+
"unexpected_instruction",
|
|
2502
|
+
"Associated token account creation for a foreign owner"
|
|
1739
2503
|
);
|
|
1740
2504
|
}
|
|
1741
|
-
return {
|
|
1742
|
-
requirement,
|
|
1743
|
-
amountRawUsdc: BigInt(requirement.amount),
|
|
1744
|
-
payTo: requirement.payTo,
|
|
1745
|
-
feePayer
|
|
1746
|
-
};
|
|
1747
2505
|
}
|
|
1748
|
-
function
|
|
1749
|
-
|
|
1750
|
-
|
|
2506
|
+
function bytesStartWith(data, prefix) {
|
|
2507
|
+
if (data.length < prefix.length) {
|
|
2508
|
+
return false;
|
|
2509
|
+
}
|
|
2510
|
+
return prefix.every((byte, index) => data[index] === byte);
|
|
1751
2511
|
}
|
|
1752
|
-
function
|
|
1753
|
-
|
|
2512
|
+
function readU64LE(data, offset) {
|
|
2513
|
+
if (data.length < offset + 8) {
|
|
2514
|
+
reject("invalid_transaction_encoding", "Instruction data too short for u64");
|
|
2515
|
+
}
|
|
2516
|
+
return Buffer.from(data.subarray(offset, offset + 8)).readBigUInt64LE(0);
|
|
1754
2517
|
}
|
|
1755
|
-
function
|
|
1756
|
-
if (
|
|
1757
|
-
|
|
2518
|
+
function readU32LE(data, offset) {
|
|
2519
|
+
if (data.length < offset + 4) {
|
|
2520
|
+
reject("invalid_transaction_encoding", "Instruction data too short for u32");
|
|
1758
2521
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
2522
|
+
return Buffer.from(data.subarray(offset, offset + 4)).readUInt32LE(0);
|
|
2523
|
+
}
|
|
2524
|
+
function readU128LE(data, offset) {
|
|
2525
|
+
if (data.length < offset + 16) {
|
|
2526
|
+
reject("invalid_transaction_encoding", "Instruction data too short for u128");
|
|
2527
|
+
}
|
|
2528
|
+
let value = 0n;
|
|
2529
|
+
for (let index = 0; index < 16; index += 1) {
|
|
2530
|
+
value |= BigInt(data[offset + index]) << BigInt(index * 8);
|
|
1763
2531
|
}
|
|
1764
2532
|
return value;
|
|
1765
2533
|
}
|
|
2534
|
+
function readShortVec(bytes, startOffset) {
|
|
2535
|
+
let value = 0;
|
|
2536
|
+
let shift = 0;
|
|
2537
|
+
let offset = startOffset;
|
|
2538
|
+
while (offset < bytes.length) {
|
|
2539
|
+
const byte = bytes[offset];
|
|
2540
|
+
value |= (byte & 127) << shift;
|
|
2541
|
+
offset += 1;
|
|
2542
|
+
if ((byte & 128) === 0) {
|
|
2543
|
+
return { value, nextOffset: offset };
|
|
2544
|
+
}
|
|
2545
|
+
shift += 7;
|
|
2546
|
+
if (shift > 28) {
|
|
2547
|
+
return null;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
return null;
|
|
2551
|
+
}
|
|
1766
2552
|
|
|
1767
|
-
// ../../src/client/
|
|
1768
|
-
var
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
this.
|
|
2553
|
+
// ../../src/client/agent-wallet-signer.ts
|
|
2554
|
+
var IntentValidatingAgentWalletSigner = class {
|
|
2555
|
+
vault;
|
|
2556
|
+
validationMode = "structured_intent_transaction";
|
|
2557
|
+
validationPolicy;
|
|
2558
|
+
constructor(validationPolicy) {
|
|
2559
|
+
this.vault = Object.freeze({ ...validationPolicy?.vault ?? SUBLY_VAULT });
|
|
2560
|
+
this.validationPolicy = { ...validationPolicy, vault: this.vault };
|
|
1774
2561
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
};
|
|
1778
|
-
|
|
1779
|
-
realizer;
|
|
1780
|
-
x402Fetch;
|
|
1781
|
-
probeFetch;
|
|
1782
|
-
defaultMaxAmountRawUsdc;
|
|
1783
|
-
network;
|
|
1784
|
-
usdcMint;
|
|
1785
|
-
stateStore;
|
|
1786
|
-
pending = /* @__PURE__ */ new Map();
|
|
1787
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
1788
|
-
nowMs;
|
|
1789
|
-
constructor(config) {
|
|
1790
|
-
this.realizer = config.realizer;
|
|
1791
|
-
this.x402Fetch = config.x402Fetch;
|
|
1792
|
-
this.probeFetch = config.probeFetch ?? fetch;
|
|
1793
|
-
this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
|
|
1794
|
-
this.network = config.network ?? SOLANA_MAINNET_NETWORK;
|
|
1795
|
-
this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
|
|
1796
|
-
this.stateStore = config.stateStore ?? null;
|
|
1797
|
-
this.nowMs = config.nowMs ?? (() => Date.now());
|
|
1798
|
-
if (this.stateStore !== null) {
|
|
1799
|
-
for (const record of this.stateStore.load()) {
|
|
1800
|
-
this.pending.set(record.key, record);
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
2562
|
+
async signPayment(params) {
|
|
2563
|
+
this.assertIntentWallet(params.intent.wallet);
|
|
2564
|
+
validatePaymentIntentTransaction({ ...params, ...this.policySpread() });
|
|
2565
|
+
return this.sign(params.serializedTransaction);
|
|
1803
2566
|
}
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
url: input.url,
|
|
1809
|
-
method: method2,
|
|
1810
|
-
requestBodyHash
|
|
1811
|
-
});
|
|
1812
|
-
const existingFlow = this.inFlight.get(pendingKey);
|
|
1813
|
-
if (existingFlow !== void 0) {
|
|
1814
|
-
return existingFlow;
|
|
1815
|
-
}
|
|
1816
|
-
const flow = this.run(input, {
|
|
1817
|
-
method: method2,
|
|
1818
|
-
requestBodyHash,
|
|
1819
|
-
pendingKey
|
|
1820
|
-
}).finally(() => {
|
|
1821
|
-
this.inFlight.delete(pendingKey);
|
|
1822
|
-
});
|
|
1823
|
-
this.inFlight.set(pendingKey, flow);
|
|
1824
|
-
return flow;
|
|
2567
|
+
async signDeposit(params) {
|
|
2568
|
+
this.assertIntentWallet(params.intent.wallet);
|
|
2569
|
+
validateDepositIntentTransaction({ ...params, ...this.policySpread() });
|
|
2570
|
+
return this.sign(params.serializedTransaction);
|
|
1825
2571
|
}
|
|
1826
|
-
async
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
this
|
|
1839
|
-
} catch (error) {
|
|
1840
|
-
throw new StandardX402PayError(
|
|
1841
|
-
"state_persist_failed",
|
|
1842
|
-
"could not clear the previous pending x402 marker before forcing a new payment",
|
|
1843
|
-
error
|
|
1844
|
-
);
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
const init = {
|
|
1848
|
-
method: method2,
|
|
1849
|
-
...input.body === void 0 ? {} : { body: input.body },
|
|
1850
|
-
...input.headers === void 0 ? {} : { headers: input.headers }
|
|
1851
|
-
};
|
|
1852
|
-
const probe = await this.probeFetch(input.url, init);
|
|
1853
|
-
if (probe.status !== 402) {
|
|
1854
|
-
return { paid: false, status: probe.status, body: await probe.text() };
|
|
1855
|
-
}
|
|
1856
|
-
const selected = await this.selectRequirement(probe);
|
|
1857
|
-
const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
|
|
1858
|
-
if (selected.amountRawUsdc > cap) {
|
|
1859
|
-
throw new StandardX402PayError(
|
|
1860
|
-
"amount_exceeds_client_cap",
|
|
1861
|
-
`the challenge demands ${selected.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
|
|
1862
|
-
{ amountRawUsdc: selected.amountRawUsdc.toString(), payTo: selected.payTo }
|
|
1863
|
-
);
|
|
1864
|
-
}
|
|
1865
|
-
let realized;
|
|
1866
|
-
try {
|
|
1867
|
-
realized = await this.realizer.ensureUsdcAvailable({
|
|
1868
|
-
amountRawUsdc: selected.amountRawUsdc,
|
|
1869
|
-
payment: {
|
|
1870
|
-
payTo: selected.payTo,
|
|
1871
|
-
amountRawUsdc: selected.amountRawUsdc.toString(),
|
|
1872
|
-
resourceUrlHash: sha256HexOf(input.url),
|
|
1873
|
-
method: method2
|
|
1874
|
-
},
|
|
1875
|
-
...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
|
|
1876
|
-
});
|
|
1877
|
-
} catch (error) {
|
|
1878
|
-
if (error.code === "approval_required") {
|
|
1879
|
-
throw new StandardX402PayError(
|
|
1880
|
-
"approval_required",
|
|
1881
|
-
"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",
|
|
1882
|
-
error.detail ?? null
|
|
1883
|
-
);
|
|
1884
|
-
}
|
|
1885
|
-
throw new StandardX402PayError(
|
|
1886
|
-
"realize_failed",
|
|
1887
|
-
`could not realize yield to cover ${selected.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
|
|
1888
|
-
error
|
|
1889
|
-
);
|
|
1890
|
-
}
|
|
1891
|
-
const pendingRecord = {
|
|
1892
|
-
key: pendingKey,
|
|
1893
|
-
url: input.url,
|
|
1894
|
-
method: method2,
|
|
1895
|
-
requestBodyHash,
|
|
1896
|
-
amountRawUsdc: selected.amountRawUsdc.toString(),
|
|
1897
|
-
payTo: selected.payTo,
|
|
1898
|
-
feePayer: selected.feePayer,
|
|
1899
|
-
realizedRawUsdc: realized.realizedRawUsdc.toString(),
|
|
1900
|
-
realizeTxSignature: realized.txSignature,
|
|
1901
|
-
status: "realized",
|
|
1902
|
-
createdAtMs: this.nowMs(),
|
|
1903
|
-
updatedAtMs: this.nowMs()
|
|
1904
|
-
};
|
|
1905
|
-
try {
|
|
1906
|
-
this.track(pendingRecord);
|
|
1907
|
-
} catch (error) {
|
|
1908
|
-
throw new StandardX402PayError(
|
|
1909
|
-
"state_persist_failed",
|
|
1910
|
-
"could not persist the pending x402 marker; refusing to attempt the external payment because a restart would not be double-payment safe",
|
|
1911
|
-
{ error, pendingPayment: pendingRecord }
|
|
1912
|
-
);
|
|
1913
|
-
}
|
|
1914
|
-
let response;
|
|
1915
|
-
try {
|
|
1916
|
-
response = await this.x402Fetch(input.url, init, selected);
|
|
1917
|
-
} catch (error) {
|
|
1918
|
-
const persistError = this.tryMarkUnknown(pendingKey, {
|
|
1919
|
-
message: error instanceof Error ? error.message : String(error)
|
|
1920
|
-
});
|
|
1921
|
-
throw new StandardX402PayError(
|
|
1922
|
-
"payment_outcome_unknown",
|
|
1923
|
-
`the x402 payment attempt failed after yield was realized; verify whether it settled before paying again: ${error instanceof Error ? error.message : String(error)}`,
|
|
1924
|
-
{ error, persistError }
|
|
1925
|
-
);
|
|
1926
|
-
}
|
|
1927
|
-
const bodyText = await response.text();
|
|
1928
|
-
if (response.status !== 200) {
|
|
1929
|
-
const persistError = this.tryMarkUnknown(pendingKey, {
|
|
1930
|
-
status: response.status,
|
|
1931
|
-
body: bodyText
|
|
1932
|
-
});
|
|
1933
|
-
throw new StandardX402PayError(
|
|
1934
|
-
"payment_outcome_unknown",
|
|
1935
|
-
`the x402 payment attempt returned ${response.status} after yield was realized; verify whether it settled before paying again`,
|
|
1936
|
-
{ status: response.status, body: bodyText, persistError }
|
|
2572
|
+
async signWithdrawal(params) {
|
|
2573
|
+
this.assertIntentWallet(params.intent.wallet);
|
|
2574
|
+
validateWithdrawalIntentTransaction({ ...params, ...this.policySpread() });
|
|
2575
|
+
return this.sign(params.serializedTransaction);
|
|
2576
|
+
}
|
|
2577
|
+
policySpread() {
|
|
2578
|
+
return this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy };
|
|
2579
|
+
}
|
|
2580
|
+
assertIntentWallet(wallet) {
|
|
2581
|
+
if (wallet !== this.walletAddress) {
|
|
2582
|
+
throw new IntentValidationError(
|
|
2583
|
+
"wallet_mismatch",
|
|
2584
|
+
"Intent wallet does not match this signer's wallet"
|
|
1937
2585
|
);
|
|
1938
2586
|
}
|
|
1939
|
-
this.clearDelivered(pendingKey);
|
|
1940
|
-
const paymentTxSignature = extractSettledPaymentTxSignature(response);
|
|
1941
|
-
if (paymentTxSignature !== null && typeof realized.withdrawalId === "string" && this.realizer.reportPayment !== void 0) {
|
|
1942
|
-
try {
|
|
1943
|
-
await this.realizer.reportPayment({
|
|
1944
|
-
withdrawalId: realized.withdrawalId,
|
|
1945
|
-
paymentTxSignature
|
|
1946
|
-
});
|
|
1947
|
-
} catch (error) {
|
|
1948
|
-
console.error(
|
|
1949
|
-
`[subly-x402] payment report-back failed (audit only, payment ok): ${error instanceof Error ? error.message : String(error)}`
|
|
1950
|
-
);
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
return {
|
|
1954
|
-
paid: true,
|
|
1955
|
-
status: response.status,
|
|
1956
|
-
body: bodyText,
|
|
1957
|
-
payment: {
|
|
1958
|
-
amountRawUsdc: selected.amountRawUsdc.toString(),
|
|
1959
|
-
payTo: selected.payTo,
|
|
1960
|
-
feePayer: selected.feePayer,
|
|
1961
|
-
realizedRawUsdc: realized.realizedRawUsdc.toString(),
|
|
1962
|
-
realizeTxSignature: realized.txSignature,
|
|
1963
|
-
paymentTxSignature
|
|
1964
|
-
}
|
|
1965
|
-
};
|
|
1966
2587
|
}
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2588
|
+
};
|
|
2589
|
+
var LocalKeypairAgentWalletSigner = class extends IntentValidatingAgentWalletSigner {
|
|
2590
|
+
provider = "local-keypair";
|
|
2591
|
+
keyPairSigner;
|
|
2592
|
+
constructor(keyPairSigner, validationPolicy) {
|
|
2593
|
+
super(validationPolicy);
|
|
2594
|
+
this.keyPairSigner = keyPairSigner;
|
|
2595
|
+
}
|
|
2596
|
+
get walletAddress() {
|
|
2597
|
+
return this.keyPairSigner.address;
|
|
2598
|
+
}
|
|
2599
|
+
async signApiMessage(message) {
|
|
2600
|
+
const signature = await signBytes(
|
|
2601
|
+
this.keyPairSigner.keyPair.privateKey,
|
|
2602
|
+
message
|
|
2603
|
+
);
|
|
2604
|
+
return bs587.encode(signature);
|
|
2605
|
+
}
|
|
2606
|
+
async sign(serializedTransaction) {
|
|
2607
|
+
const { serializedBase64, transaction } = await addSignaturesToSerializedTransaction({
|
|
2608
|
+
serializedBase64: serializedTransaction,
|
|
2609
|
+
signers: [this.keyPairSigner.keyPair]
|
|
2610
|
+
});
|
|
2611
|
+
const agentSignature = signatureBase58ForSigner(
|
|
2612
|
+
transaction,
|
|
2613
|
+
this.keyPairSigner.address
|
|
2614
|
+
);
|
|
2615
|
+
if (agentSignature === null) {
|
|
2616
|
+
throw new IntentValidationError(
|
|
2617
|
+
"signing_failed",
|
|
2618
|
+
"Agent signature was not produced"
|
|
1984
2619
|
);
|
|
1985
2620
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2621
|
+
return { serializedTransaction: serializedBase64, agentSignature };
|
|
2622
|
+
}
|
|
2623
|
+
};
|
|
2624
|
+
var RemoteAgentWalletSigner = class extends IntentValidatingAgentWalletSigner {
|
|
2625
|
+
transport;
|
|
2626
|
+
publicKey;
|
|
2627
|
+
constructor(transport, validationPolicy) {
|
|
2628
|
+
super(validationPolicy);
|
|
2629
|
+
this.transport = transport;
|
|
2630
|
+
this.publicKey = ed25519PublicKeyBytes(
|
|
2631
|
+
transport.provider,
|
|
2632
|
+
transport.walletAddress
|
|
2633
|
+
);
|
|
2634
|
+
}
|
|
2635
|
+
get walletAddress() {
|
|
2636
|
+
return this.transport.walletAddress;
|
|
2637
|
+
}
|
|
2638
|
+
get provider() {
|
|
2639
|
+
return this.transport.provider;
|
|
2640
|
+
}
|
|
2641
|
+
async signApiMessage(message) {
|
|
2642
|
+
const signature = await this.transport.signMessage(message);
|
|
2643
|
+
if (!nacl3.sign.detached.verify(message, signature, this.publicKey)) {
|
|
2644
|
+
throw new RemoteSigningError(
|
|
2645
|
+
this.transport.provider,
|
|
2646
|
+
"message signature did not verify for the agent wallet"
|
|
1996
2647
|
);
|
|
1997
2648
|
}
|
|
2649
|
+
return bs587.encode(signature);
|
|
1998
2650
|
}
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
} catch (error) {
|
|
2005
|
-
if (previous === void 0) {
|
|
2006
|
-
this.pending.delete(record.key);
|
|
2007
|
-
} else {
|
|
2008
|
-
this.pending.set(record.key, previous);
|
|
2009
|
-
}
|
|
2010
|
-
throw error;
|
|
2011
|
-
}
|
|
2651
|
+
sign(serializedTransaction) {
|
|
2652
|
+
return externallySignedAgentTransaction({
|
|
2653
|
+
transport: this.transport,
|
|
2654
|
+
serializedTransaction
|
|
2655
|
+
});
|
|
2012
2656
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
throw error;
|
|
2030
|
-
}
|
|
2657
|
+
};
|
|
2658
|
+
|
|
2659
|
+
// ../../src/client/signer-transports/circle.ts
|
|
2660
|
+
import {
|
|
2661
|
+
constants,
|
|
2662
|
+
createPublicKey,
|
|
2663
|
+
publicEncrypt
|
|
2664
|
+
} from "node:crypto";
|
|
2665
|
+
var PROVIDER = "circle";
|
|
2666
|
+
var DEFAULT_BASE_URL = "https://api.circle.com";
|
|
2667
|
+
async function createCircleSignerTransport(config) {
|
|
2668
|
+
if (!/^[0-9a-fA-F]{64}$/.test(config.entitySecret)) {
|
|
2669
|
+
throw new RemoteSigningError(
|
|
2670
|
+
PROVIDER,
|
|
2671
|
+
"entity secret must be 32 bytes of hex (64 hex chars)"
|
|
2672
|
+
);
|
|
2031
2673
|
}
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2674
|
+
const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
2675
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
2676
|
+
const request = async (method2, path, body2) => {
|
|
2677
|
+
const json = await providerJsonRequest({
|
|
2678
|
+
provider: PROVIDER,
|
|
2679
|
+
fetchImpl,
|
|
2680
|
+
baseUrl,
|
|
2681
|
+
path,
|
|
2682
|
+
method: method2,
|
|
2683
|
+
headers: { authorization: `Bearer ${config.apiKey}` },
|
|
2684
|
+
body: body2
|
|
2685
|
+
});
|
|
2686
|
+
const data = json?.data;
|
|
2687
|
+
if (data === void 0) {
|
|
2688
|
+
throw new RemoteSigningError(
|
|
2689
|
+
PROVIDER,
|
|
2690
|
+
`${method2} ${path} returned no data envelope`,
|
|
2691
|
+
json
|
|
2692
|
+
);
|
|
2038
2693
|
}
|
|
2694
|
+
return data;
|
|
2695
|
+
};
|
|
2696
|
+
const walletData = await request("GET", `/v1/w3s/wallets/${config.walletId}`);
|
|
2697
|
+
const wallet = walletData.wallet;
|
|
2698
|
+
if (wallet?.address === void 0) {
|
|
2699
|
+
throw new RemoteSigningError(
|
|
2700
|
+
PROVIDER,
|
|
2701
|
+
`wallet ${config.walletId} has no address`,
|
|
2702
|
+
walletData
|
|
2703
|
+
);
|
|
2039
2704
|
}
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2705
|
+
if (wallet.blockchain !== "SOL") {
|
|
2706
|
+
throw new RemoteSigningError(
|
|
2707
|
+
PROVIDER,
|
|
2708
|
+
`wallet ${config.walletId} is on ${String(
|
|
2709
|
+
wallet.blockchain
|
|
2710
|
+
)}, expected SOL (Solana mainnet)`
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2713
|
+
const walletAddress = wallet.address;
|
|
2714
|
+
let entityPublicKey = null;
|
|
2715
|
+
const entitySecretCiphertext = async () => {
|
|
2716
|
+
if (entityPublicKey === null) {
|
|
2717
|
+
const data = await request("GET", "/v1/w3s/config/entity/publicKey");
|
|
2718
|
+
const publicKey2 = data.publicKey;
|
|
2719
|
+
if (typeof publicKey2 !== "string") {
|
|
2720
|
+
throw new RemoteSigningError(
|
|
2721
|
+
PROVIDER,
|
|
2722
|
+
"entity public key response has no publicKey",
|
|
2723
|
+
data
|
|
2724
|
+
);
|
|
2049
2725
|
}
|
|
2050
|
-
|
|
2726
|
+
entityPublicKey = createPublicKey(publicKey2);
|
|
2051
2727
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2728
|
+
return publicEncrypt(
|
|
2729
|
+
{
|
|
2730
|
+
key: entityPublicKey,
|
|
2731
|
+
padding: constants.RSA_PKCS1_OAEP_PADDING,
|
|
2732
|
+
oaepHash: "sha256"
|
|
2733
|
+
},
|
|
2734
|
+
Buffer.from(config.entitySecret, "hex")
|
|
2735
|
+
).toString("base64");
|
|
2736
|
+
};
|
|
2737
|
+
return {
|
|
2738
|
+
provider: PROVIDER,
|
|
2739
|
+
walletAddress,
|
|
2740
|
+
async signMessage(message) {
|
|
2741
|
+
const data = await request("POST", "/v1/w3s/developer/sign/message", {
|
|
2742
|
+
walletId: config.walletId,
|
|
2743
|
+
message: `0x${Buffer.from(message).toString("hex")}`,
|
|
2744
|
+
encodedByHex: true,
|
|
2745
|
+
entitySecretCiphertext: await entitySecretCiphertext()
|
|
2746
|
+
});
|
|
2747
|
+
const signature = data.signature;
|
|
2748
|
+
if (typeof signature !== "string") {
|
|
2749
|
+
throw new RemoteSigningError(
|
|
2750
|
+
PROVIDER,
|
|
2751
|
+
"sign/message returned no signature",
|
|
2752
|
+
data
|
|
2753
|
+
);
|
|
2061
2754
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2755
|
+
return verifiedEd25519Signature({
|
|
2756
|
+
provider: PROVIDER,
|
|
2757
|
+
encodedSignature: signature,
|
|
2758
|
+
message,
|
|
2759
|
+
walletAddress
|
|
2760
|
+
});
|
|
2761
|
+
},
|
|
2762
|
+
async signTransaction(serializedTransactionBase64) {
|
|
2763
|
+
const data = await request(
|
|
2764
|
+
"POST",
|
|
2765
|
+
"/v1/w3s/developer/sign/transaction",
|
|
2766
|
+
{
|
|
2767
|
+
walletId: config.walletId,
|
|
2768
|
+
rawTransaction: serializedTransactionBase64,
|
|
2769
|
+
entitySecretCiphertext: await entitySecretCiphertext()
|
|
2770
|
+
}
|
|
2064
2771
|
);
|
|
2772
|
+
const signedTransaction = data.signedTransaction;
|
|
2773
|
+
if (typeof signedTransaction !== "string") {
|
|
2774
|
+
throw new RemoteSigningError(
|
|
2775
|
+
PROVIDER,
|
|
2776
|
+
"sign/transaction returned no signedTransaction",
|
|
2777
|
+
data
|
|
2778
|
+
);
|
|
2779
|
+
}
|
|
2780
|
+
return signedTransaction;
|
|
2065
2781
|
}
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
// ../../src/client/signer-transports/privy.ts
|
|
2786
|
+
import { createPrivateKey, createSign } from "node:crypto";
|
|
2787
|
+
var PROVIDER2 = "privy";
|
|
2788
|
+
var DEFAULT_BASE_URL2 = "https://api.privy.io";
|
|
2789
|
+
function canonicalJson(value) {
|
|
2790
|
+
if (Array.isArray(value)) {
|
|
2791
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
2066
2792
|
}
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
}
|
|
2071
|
-
this.stateStore.save([...this.pending.values()]);
|
|
2793
|
+
if (value !== null && typeof value === "object") {
|
|
2794
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2795
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
|
|
2072
2796
|
}
|
|
2073
|
-
|
|
2074
|
-
function pendingPaymentKey(input) {
|
|
2075
|
-
return `${input.method}:${input.url}:${input.requestBodyHash}`;
|
|
2797
|
+
return JSON.stringify(value);
|
|
2076
2798
|
}
|
|
2077
|
-
function
|
|
2078
|
-
const
|
|
2079
|
-
if (header === null || header.length === 0) {
|
|
2080
|
-
return null;
|
|
2081
|
-
}
|
|
2799
|
+
function parseAuthorizationKey(base64Pkcs8) {
|
|
2800
|
+
const stripped = base64Pkcs8.replace(/^wallet-auth:/, "").trim();
|
|
2082
2801
|
try {
|
|
2083
|
-
|
|
2084
|
-
Buffer.from(
|
|
2802
|
+
return createPrivateKey({
|
|
2803
|
+
key: Buffer.from(stripped, "base64"),
|
|
2804
|
+
format: "der",
|
|
2805
|
+
type: "pkcs8"
|
|
2806
|
+
});
|
|
2807
|
+
} catch (error) {
|
|
2808
|
+
throw new RemoteSigningError(
|
|
2809
|
+
PROVIDER2,
|
|
2810
|
+
"authorization key is not a base64 PKCS#8 P-256 private key",
|
|
2811
|
+
error
|
|
2085
2812
|
);
|
|
2086
|
-
|
|
2087
|
-
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
function authorizationSignature(params) {
|
|
2816
|
+
const payload = {
|
|
2817
|
+
version: 1,
|
|
2818
|
+
method: params.method,
|
|
2819
|
+
url: params.url,
|
|
2820
|
+
body: params.body,
|
|
2821
|
+
headers: { "privy-app-id": params.appId }
|
|
2822
|
+
};
|
|
2823
|
+
const signer2 = createSign("sha256");
|
|
2824
|
+
signer2.update(canonicalJson(payload));
|
|
2825
|
+
return signer2.sign(params.key).toString("base64");
|
|
2826
|
+
}
|
|
2827
|
+
async function createPrivySignerTransport(config) {
|
|
2828
|
+
const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/$/, "");
|
|
2829
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
2830
|
+
const authorizationKey = config.authorizationPrivateKey === void 0 ? null : parseAuthorizationKey(config.authorizationPrivateKey);
|
|
2831
|
+
const baseHeaders = {
|
|
2832
|
+
authorization: `Basic ${Buffer.from(
|
|
2833
|
+
`${config.appId}:${config.appSecret}`
|
|
2834
|
+
).toString("base64")}`,
|
|
2835
|
+
"privy-app-id": config.appId
|
|
2836
|
+
};
|
|
2837
|
+
const request = async (method2, path, body2) => {
|
|
2838
|
+
const headers = authorizationKey !== null && method2 !== "GET" && body2 !== void 0 ? {
|
|
2839
|
+
...baseHeaders,
|
|
2840
|
+
"privy-authorization-signature": authorizationSignature({
|
|
2841
|
+
key: authorizationKey,
|
|
2842
|
+
appId: config.appId,
|
|
2843
|
+
method: method2,
|
|
2844
|
+
url: `${baseUrl}${path}`,
|
|
2845
|
+
body: body2
|
|
2846
|
+
})
|
|
2847
|
+
} : baseHeaders;
|
|
2848
|
+
const json = await providerJsonRequest({
|
|
2849
|
+
provider: PROVIDER2,
|
|
2850
|
+
fetchImpl,
|
|
2851
|
+
baseUrl,
|
|
2852
|
+
path,
|
|
2853
|
+
method: method2,
|
|
2854
|
+
headers,
|
|
2855
|
+
body: body2
|
|
2856
|
+
});
|
|
2857
|
+
if (json === null || typeof json !== "object") {
|
|
2858
|
+
throw new RemoteSigningError(
|
|
2859
|
+
PROVIDER2,
|
|
2860
|
+
`${method2} ${path} returned a non-JSON body`
|
|
2861
|
+
);
|
|
2088
2862
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2863
|
+
return json;
|
|
2864
|
+
};
|
|
2865
|
+
const rpc2 = async (body2) => {
|
|
2866
|
+
const response = await request(
|
|
2867
|
+
"POST",
|
|
2868
|
+
`/v1/wallets/${config.walletId}/rpc`,
|
|
2869
|
+
body2
|
|
2870
|
+
);
|
|
2871
|
+
const data = response.data;
|
|
2872
|
+
if (data === null || typeof data !== "object") {
|
|
2873
|
+
throw new RemoteSigningError(PROVIDER2, "rpc returned no data", response);
|
|
2091
2874
|
}
|
|
2092
|
-
return
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2875
|
+
return data;
|
|
2876
|
+
};
|
|
2877
|
+
const wallet = await request("GET", `/v1/wallets/${config.walletId}`);
|
|
2878
|
+
const walletAddress = wallet.address;
|
|
2879
|
+
if (typeof walletAddress !== "string") {
|
|
2880
|
+
throw new RemoteSigningError(
|
|
2881
|
+
PROVIDER2,
|
|
2882
|
+
`wallet ${config.walletId} has no address`,
|
|
2883
|
+
wallet
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
if (wallet.chain_type !== "solana") {
|
|
2887
|
+
throw new RemoteSigningError(
|
|
2888
|
+
PROVIDER2,
|
|
2889
|
+
`wallet ${config.walletId} is ${String(
|
|
2890
|
+
wallet.chain_type
|
|
2891
|
+
)}, expected solana`
|
|
2892
|
+
);
|
|
2095
2893
|
}
|
|
2894
|
+
return {
|
|
2895
|
+
provider: PROVIDER2,
|
|
2896
|
+
walletAddress,
|
|
2897
|
+
async signMessage(message) {
|
|
2898
|
+
const data = await rpc2({
|
|
2899
|
+
chain_type: "solana",
|
|
2900
|
+
method: "signMessage",
|
|
2901
|
+
params: {
|
|
2902
|
+
message: Buffer.from(message).toString("base64"),
|
|
2903
|
+
encoding: "base64"
|
|
2904
|
+
}
|
|
2905
|
+
});
|
|
2906
|
+
const signature = data.signature;
|
|
2907
|
+
if (typeof signature !== "string") {
|
|
2908
|
+
throw new RemoteSigningError(
|
|
2909
|
+
PROVIDER2,
|
|
2910
|
+
"signMessage returned no signature",
|
|
2911
|
+
data
|
|
2912
|
+
);
|
|
2913
|
+
}
|
|
2914
|
+
return verifiedEd25519Signature({
|
|
2915
|
+
provider: PROVIDER2,
|
|
2916
|
+
encodedSignature: signature,
|
|
2917
|
+
message,
|
|
2918
|
+
walletAddress
|
|
2919
|
+
});
|
|
2920
|
+
},
|
|
2921
|
+
async signTransaction(serializedTransactionBase64) {
|
|
2922
|
+
const data = await rpc2({
|
|
2923
|
+
chain_type: "solana",
|
|
2924
|
+
method: "signTransaction",
|
|
2925
|
+
params: {
|
|
2926
|
+
transaction: serializedTransactionBase64,
|
|
2927
|
+
encoding: "base64"
|
|
2928
|
+
}
|
|
2929
|
+
});
|
|
2930
|
+
const signedTransaction = data.signed_transaction;
|
|
2931
|
+
if (typeof signedTransaction !== "string") {
|
|
2932
|
+
throw new RemoteSigningError(
|
|
2933
|
+
PROVIDER2,
|
|
2934
|
+
"signTransaction returned no signed_transaction",
|
|
2935
|
+
data
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
return signedTransaction;
|
|
2939
|
+
}
|
|
2940
|
+
};
|
|
2096
2941
|
}
|
|
2097
2942
|
|
|
2098
|
-
// ../../src/client/
|
|
2099
|
-
function
|
|
2100
|
-
const
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2943
|
+
// ../../src/client/signer-env.ts
|
|
2944
|
+
async function agentWalletSignerFromEnv(env = process.env) {
|
|
2945
|
+
const nonEmpty = (value) => {
|
|
2946
|
+
const trimmed = value?.trim();
|
|
2947
|
+
return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
|
|
2948
|
+
};
|
|
2949
|
+
const provider = nonEmpty(env.SUBLY_SIGNER_PROVIDER)?.toLowerCase() ?? "local";
|
|
2950
|
+
const pickVar = (name) => nonEmpty(env[`SUBLY_${name}`]) ?? nonEmpty(env[name]);
|
|
2951
|
+
const requireVar = (name) => {
|
|
2952
|
+
const value = pickVar(name);
|
|
2953
|
+
if (value === void 0) {
|
|
2954
|
+
throw new Error(
|
|
2955
|
+
`${name} (or SUBLY_${name}) is required for SUBLY_SIGNER_PROVIDER=${provider}`
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
return value;
|
|
2959
|
+
};
|
|
2960
|
+
if (provider === "local") {
|
|
2961
|
+
const localSecretKey = loadSecretKeyBytes({
|
|
2962
|
+
base58Secret: env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
2963
|
+
jsonFilePath: env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
2964
|
+
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
2965
|
+
});
|
|
2966
|
+
return {
|
|
2967
|
+
provider,
|
|
2968
|
+
signer: new LocalKeypairAgentWalletSigner(
|
|
2969
|
+
await createKeyPairSignerFromBytes2(localSecretKey)
|
|
2970
|
+
),
|
|
2971
|
+
localSecretKey
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
if (provider === "circle") {
|
|
2975
|
+
const transport = await createCircleSignerTransport({
|
|
2976
|
+
apiKey: requireVar("CIRCLE_API_KEY"),
|
|
2977
|
+
entitySecret: requireVar("CIRCLE_ENTITY_SECRET"),
|
|
2978
|
+
walletId: requireVar("CIRCLE_WALLET_ID"),
|
|
2979
|
+
baseUrl: pickVar("CIRCLE_BASE_URL")
|
|
2980
|
+
});
|
|
2981
|
+
return { provider, signer: new RemoteAgentWalletSigner(transport), transport };
|
|
2982
|
+
}
|
|
2983
|
+
if (provider === "privy") {
|
|
2984
|
+
const transport = await createPrivySignerTransport({
|
|
2985
|
+
appId: requireVar("PRIVY_APP_ID"),
|
|
2986
|
+
appSecret: requireVar("PRIVY_APP_SECRET"),
|
|
2987
|
+
walletId: requireVar("PRIVY_WALLET_ID"),
|
|
2988
|
+
authorizationPrivateKey: pickVar("PRIVY_AUTHORIZATION_KEY"),
|
|
2989
|
+
baseUrl: pickVar("PRIVY_BASE_URL")
|
|
2990
|
+
});
|
|
2991
|
+
return { provider, signer: new RemoteAgentWalletSigner(transport), transport };
|
|
2992
|
+
}
|
|
2993
|
+
throw new Error(
|
|
2994
|
+
`unknown SUBLY_SIGNER_PROVIDER "${provider}" (expected local, circle, or privy)`
|
|
2995
|
+
);
|
|
2111
2996
|
}
|
|
2112
2997
|
|
|
2113
2998
|
// ../../src/client/standard-x402-state-store.ts
|
|
2114
|
-
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2999
|
+
import { closeSync, mkdirSync, openSync, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2115
3000
|
import { basename, dirname, join } from "node:path";
|
|
2116
3001
|
function fileStandardX402StateStore(path) {
|
|
2117
3002
|
return {
|
|
3003
|
+
async withExclusiveLock(operation) {
|
|
3004
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
3005
|
+
const lockPath = `${path}.lock`;
|
|
3006
|
+
let fd;
|
|
3007
|
+
try {
|
|
3008
|
+
fd = openSync(lockPath, "wx", 384);
|
|
3009
|
+
} catch (error) {
|
|
3010
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
|
|
3011
|
+
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.`);
|
|
3012
|
+
}
|
|
3013
|
+
throw error;
|
|
3014
|
+
}
|
|
3015
|
+
try {
|
|
3016
|
+
writeFileSync(fd, JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
3017
|
+
return await operation();
|
|
3018
|
+
} finally {
|
|
3019
|
+
closeSync(fd);
|
|
3020
|
+
unlinkSync(lockPath);
|
|
3021
|
+
}
|
|
3022
|
+
},
|
|
2118
3023
|
load() {
|
|
2119
3024
|
let text;
|
|
2120
3025
|
try {
|
|
2121
|
-
text =
|
|
3026
|
+
text = readFileSync3(path, "utf8");
|
|
2122
3027
|
} catch (error) {
|
|
2123
3028
|
if (isMissingFileError(error)) {
|
|
2124
3029
|
return [];
|
|
@@ -2140,12 +3045,12 @@ function fileStandardX402StateStore(path) {
|
|
|
2140
3045
|
},
|
|
2141
3046
|
save(records) {
|
|
2142
3047
|
const directory = dirname(path);
|
|
2143
|
-
mkdirSync(directory, { recursive: true });
|
|
3048
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
2144
3049
|
const tempPath = join(
|
|
2145
3050
|
directory,
|
|
2146
3051
|
`.${basename(path)}.${process.pid}.${Date.now()}.tmp`
|
|
2147
3052
|
);
|
|
2148
|
-
writeFileSync(tempPath, JSON.stringify(records, null, 2));
|
|
3053
|
+
writeFileSync(tempPath, JSON.stringify(records, null, 2), { mode: 384, flag: "wx" });
|
|
2149
3054
|
renameSync(tempPath, path);
|
|
2150
3055
|
}
|
|
2151
3056
|
};
|
|
@@ -2161,63 +3066,55 @@ function isPendingPaymentRecord(value) {
|
|
|
2161
3066
|
return typeof record.key === "string" && typeof record.url === "string" && typeof record.method === "string" && typeof record.requestBodyHash === "string" && typeof record.amountRawUsdc === "string" && typeof record.payTo === "string" && (record.feePayer === null || typeof record.feePayer === "string") && typeof record.realizedRawUsdc === "string" && (record.realizeTxSignature === null || typeof record.realizeTxSignature === "string") && (record.status === "realized" || record.status === "external_outcome_unknown") && typeof record.createdAtMs === "number" && typeof record.updatedAtMs === "number";
|
|
2162
3067
|
}
|
|
2163
3068
|
|
|
2164
|
-
// ../../src/solana/
|
|
2165
|
-
import {
|
|
2166
|
-
|
|
3069
|
+
// ../../src/solana/rpc.ts
|
|
3070
|
+
import { createSolanaRpc } from "@solana/kit";
|
|
3071
|
+
function createRpc(url2) {
|
|
3072
|
+
return createSolanaRpc(url2);
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
// src/svm-signer.ts
|
|
2167
3076
|
import {
|
|
2168
|
-
|
|
3077
|
+
address as address2,
|
|
3078
|
+
createKeyPairSignerFromBytes as createKeyPairSignerFromBytes3,
|
|
3079
|
+
getBase64EncodedWireTransaction as getBase64EncodedWireTransaction2
|
|
2169
3080
|
} from "@solana/kit";
|
|
2170
|
-
async function
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
const bytes = bs586.decode(base58Secret);
|
|
2174
|
-
if (bytes.length !== 64) {
|
|
2175
|
-
throw new Error(`${label} base58 secret must decode to 64 bytes`);
|
|
2176
|
-
}
|
|
2177
|
-
return createKeyPairSignerFromBytes(bytes);
|
|
2178
|
-
}
|
|
2179
|
-
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
2180
|
-
const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
|
|
2181
|
-
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
2182
|
-
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
2183
|
-
}
|
|
2184
|
-
return createKeyPairSignerFromBytes(Uint8Array.from(raw));
|
|
3081
|
+
async function svmTransactionSignerFromBundle(bundle2) {
|
|
3082
|
+
if (bundle2.provider === "local") {
|
|
3083
|
+
return createKeyPairSignerFromBytes3(bundle2.localSecretKey);
|
|
2185
3084
|
}
|
|
2186
|
-
|
|
3085
|
+
return remoteSvmTransactionSigner(bundle2.transport);
|
|
2187
3086
|
}
|
|
2188
|
-
function
|
|
2189
|
-
const
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
3087
|
+
function remoteSvmTransactionSigner(transport) {
|
|
3088
|
+
const signerAddress = address2(transport.walletAddress);
|
|
3089
|
+
const publicKey2 = ed25519PublicKeyBytes(
|
|
3090
|
+
transport.provider,
|
|
3091
|
+
transport.walletAddress
|
|
3092
|
+
);
|
|
3093
|
+
return {
|
|
3094
|
+
address: signerAddress,
|
|
3095
|
+
async signTransactions(transactions) {
|
|
3096
|
+
const dictionaries = [];
|
|
3097
|
+
for (const transaction of transactions) {
|
|
3098
|
+
const signature = await requestVerifiedTransactionSignature({
|
|
3099
|
+
transport,
|
|
3100
|
+
serializedTransactionBase64: getBase64EncodedWireTransaction2(transaction),
|
|
3101
|
+
messageBytes: transaction.messageBytes,
|
|
3102
|
+
publicKey: publicKey2
|
|
3103
|
+
});
|
|
3104
|
+
dictionaries.push(
|
|
3105
|
+
Object.freeze({ [signerAddress]: signature })
|
|
3106
|
+
);
|
|
3107
|
+
}
|
|
3108
|
+
return dictionaries;
|
|
2201
3109
|
}
|
|
2202
|
-
|
|
2203
|
-
}
|
|
2204
|
-
throw new Error(`${label} keypair is not configured`);
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
// ../../src/solana/rpc.ts
|
|
2208
|
-
import { createSolanaRpc } from "@solana/kit";
|
|
2209
|
-
function createRpc(url2) {
|
|
2210
|
-
return createSolanaRpc(url2);
|
|
3110
|
+
};
|
|
2211
3111
|
}
|
|
2212
3112
|
|
|
2213
3113
|
// src/svm-x402-fetch.ts
|
|
2214
|
-
import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
|
|
2215
3114
|
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
|
|
2216
3115
|
import { ExactSvmScheme, toClientSvmSigner } from "@x402/svm";
|
|
2217
3116
|
async function createSvmX402Fetch(params) {
|
|
2218
|
-
const signer2 = toClientSvmSigner(
|
|
2219
|
-
await createKeyPairSignerFromBytes2(params.agentSecretKey)
|
|
2220
|
-
);
|
|
3117
|
+
const signer2 = toClientSvmSigner(params.signer);
|
|
2221
3118
|
return (url2, init, expected) => {
|
|
2222
3119
|
const wrapped = wrapFetchWithPaymentFromConfig(fetch, {
|
|
2223
3120
|
schemes: [
|
|
@@ -2269,23 +3166,17 @@ var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITA
|
|
|
2269
3166
|
var rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
|
|
2270
3167
|
var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ? 10000n : BigInt(process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC);
|
|
2271
3168
|
var pendingStatePath = process.env.SUBLY_MCP_STATE_PATH ?? join2(homedir(), ".subly", "standard-x402-pending.json");
|
|
2272
|
-
var
|
|
2273
|
-
|
|
2274
|
-
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
2275
|
-
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
2276
|
-
});
|
|
2277
|
-
var agentSecretKey = loadSecretKeyBytes({
|
|
2278
|
-
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
2279
|
-
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
2280
|
-
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
2281
|
-
});
|
|
2282
|
-
var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
|
|
3169
|
+
var bundle = await agentWalletSignerFromEnv();
|
|
3170
|
+
var signer = bundle.signer;
|
|
2283
3171
|
var rpc = createRpc(rpcUrl);
|
|
2284
3172
|
var payer = createRelayerX402Payer({
|
|
2285
3173
|
relayerBaseUrl,
|
|
2286
3174
|
signer,
|
|
2287
3175
|
rpc,
|
|
2288
|
-
x402Fetch: await createSvmX402Fetch({
|
|
3176
|
+
x402Fetch: await createSvmX402Fetch({
|
|
3177
|
+
signer: await svmTransactionSignerFromBundle(bundle),
|
|
3178
|
+
rpcUrl
|
|
3179
|
+
}),
|
|
2289
3180
|
defaultMaxAmountRawUsdc,
|
|
2290
3181
|
stateStore: fileStandardX402StateStore(pendingStatePath)
|
|
2291
3182
|
});
|