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