@subly_fi/pay 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -152
- package/dist/budget.js +2267 -0
- package/dist/cli.js +37 -20
- package/dist/deposit.js +243 -58
- package/dist/doctor.js +188 -0
- package/dist/mcp-server.js +1537 -1227
- package/dist/pay.js +437 -225
- package/dist/setup-link.js +238 -55
- package/dist/vaults.js +129 -0
- package/dist/withdraw.js +238 -55
- package/package.json +7 -5
package/dist/budget.js
ADDED
|
@@ -0,0 +1,2267 @@
|
|
|
1
|
+
// ../../src/client/signer-env.ts
|
|
2
|
+
import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
|
|
3
|
+
|
|
4
|
+
// ../../src/solana/keys.ts
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import bs58 from "bs58";
|
|
7
|
+
import {
|
|
8
|
+
createKeyPairSignerFromBytes
|
|
9
|
+
} from "@solana/kit";
|
|
10
|
+
function loadSecretKeyBytes(params) {
|
|
11
|
+
const { base58Secret, jsonFilePath, label } = params;
|
|
12
|
+
if (base58Secret !== void 0 && base58Secret.length > 0) {
|
|
13
|
+
const bytes = bs58.decode(base58Secret);
|
|
14
|
+
if (bytes.length !== 64) {
|
|
15
|
+
throw new Error(`${label} base58 secret must decode to 64 bytes`);
|
|
16
|
+
}
|
|
17
|
+
return bytes;
|
|
18
|
+
}
|
|
19
|
+
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
20
|
+
const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
|
|
21
|
+
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
22
|
+
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
23
|
+
}
|
|
24
|
+
return Uint8Array.from(raw);
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`${label} keypair is not configured`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ../../src/config/vault-catalog.ts
|
|
30
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
|
|
33
|
+
// ../../src/lib/solana-address.ts
|
|
34
|
+
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
35
|
+
var BASE58_LOOKUP = new Map(
|
|
36
|
+
[...BASE58_ALPHABET].map((character, index) => [character, BigInt(index)])
|
|
37
|
+
);
|
|
38
|
+
function assertSolanaAddress(value, fieldName) {
|
|
39
|
+
if (value.length < 32 || value.length > 44) {
|
|
40
|
+
throw new Error(`${fieldName} must be a valid Solana public key`);
|
|
41
|
+
}
|
|
42
|
+
if (decodeBase58(value).length !== 32) {
|
|
43
|
+
throw new Error(`${fieldName} must be a valid Solana public key`);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
function decodeBase58(value) {
|
|
48
|
+
if (value.length === 0) {
|
|
49
|
+
return new Uint8Array();
|
|
50
|
+
}
|
|
51
|
+
let decoded = 0n;
|
|
52
|
+
for (const character of value) {
|
|
53
|
+
const digit = BASE58_LOOKUP.get(character);
|
|
54
|
+
if (digit === void 0) {
|
|
55
|
+
return new Uint8Array();
|
|
56
|
+
}
|
|
57
|
+
decoded = decoded * 58n + digit;
|
|
58
|
+
}
|
|
59
|
+
const bytes = [];
|
|
60
|
+
while (decoded > 0n) {
|
|
61
|
+
bytes.push(Number(decoded & 0xffn));
|
|
62
|
+
decoded >>= 8n;
|
|
63
|
+
}
|
|
64
|
+
for (const character of value) {
|
|
65
|
+
if (character !== "1") {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
bytes.push(0);
|
|
69
|
+
}
|
|
70
|
+
return Uint8Array.from(bytes.reverse());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ../../src/config/vault.ts
|
|
74
|
+
var MAINNET_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
75
|
+
var KAMINO_VAULT_PROGRAM_ID = "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd";
|
|
76
|
+
var NO_VAULT_FARM = "11111111111111111111111111111111";
|
|
77
|
+
var DEFAULT_VAULT_CONFIG = Object.freeze({
|
|
78
|
+
address: "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr",
|
|
79
|
+
programId: KAMINO_VAULT_PROGRAM_ID,
|
|
80
|
+
usdcMint: MAINNET_USDC_MINT,
|
|
81
|
+
shareMint: "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a",
|
|
82
|
+
farm: "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
|
|
83
|
+
});
|
|
84
|
+
function vaultConfigFromEnv(env = process.env) {
|
|
85
|
+
const value = (name) => env[name]?.trim() || void 0;
|
|
86
|
+
const vaultAddress = value("SUBLY_VAULT_ADDRESS") ?? DEFAULT_VAULT_CONFIG.address;
|
|
87
|
+
const customVault = vaultAddress !== DEFAULT_VAULT_CONFIG.address;
|
|
88
|
+
const anchor = (name, fallback) => {
|
|
89
|
+
const configured = value(name);
|
|
90
|
+
if (customVault && configured === void 0) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`${name} is required for a custom vault. Generate its settings with npm run configure:vault -- <vault-address>; use ${NO_VAULT_FARM} for a vault without a farm.`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return assertSolanaAddress(configured ?? fallback, name);
|
|
96
|
+
};
|
|
97
|
+
const usdcMint = value("SUBLY_VAULT_USDC_MINT") ?? MAINNET_USDC_MINT;
|
|
98
|
+
if (usdcMint !== MAINNET_USDC_MINT) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"SUBLY_VAULT_USDC_MINT must be mainnet USDC; other deposit assets are not supported"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return Object.freeze({
|
|
104
|
+
address: assertSolanaAddress(vaultAddress, "SUBLY_VAULT_ADDRESS"),
|
|
105
|
+
programId: KAMINO_VAULT_PROGRAM_ID,
|
|
106
|
+
usdcMint,
|
|
107
|
+
shareMint: anchor("SUBLY_VAULT_SHARE_MINT", DEFAULT_VAULT_CONFIG.shareMint),
|
|
108
|
+
farm: anchor("SUBLY_VAULT_FARM", DEFAULT_VAULT_CONFIG.farm)
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ../../src/config/vault-catalog.ts
|
|
113
|
+
var publicKey = z.string().refine((value) => {
|
|
114
|
+
try {
|
|
115
|
+
assertSolanaAddress(value, "vault catalog address");
|
|
116
|
+
return true;
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}, "Invalid Solana public key");
|
|
121
|
+
var catalogSchema = z.object({
|
|
122
|
+
version: z.literal(1),
|
|
123
|
+
defaultVault: publicKey,
|
|
124
|
+
vaults: z.array(z.object({
|
|
125
|
+
address: publicKey,
|
|
126
|
+
programId: z.literal(KAMINO_VAULT_PROGRAM_ID),
|
|
127
|
+
usdcMint: z.literal(MAINNET_USDC_MINT),
|
|
128
|
+
shareMint: publicKey,
|
|
129
|
+
farm: publicKey,
|
|
130
|
+
name: z.string().min(1).max(128).optional(),
|
|
131
|
+
depositsEnabled: z.boolean().optional(),
|
|
132
|
+
extraLookupTables: z.array(publicKey).max(16).optional()
|
|
133
|
+
}).strict()).min(1).max(100)
|
|
134
|
+
}).strict();
|
|
135
|
+
function parseVaultCatalog(value) {
|
|
136
|
+
const catalog = catalogSchema.parse(value);
|
|
137
|
+
const addresses = new Set(catalog.vaults.map((vault) => vault.address));
|
|
138
|
+
if (addresses.size !== catalog.vaults.length) throw new Error("Duplicate vault in catalog");
|
|
139
|
+
if (!addresses.has(catalog.defaultVault)) throw new Error("defaultVault must be in the vault catalog");
|
|
140
|
+
return { ...catalog, vaults: catalog.vaults.map((vault) => Object.freeze(vault)) };
|
|
141
|
+
}
|
|
142
|
+
function vaultCatalogFromEnv(env = process.env) {
|
|
143
|
+
const path = env.SUBLY_VAULTS_FILE?.trim();
|
|
144
|
+
if (!path) {
|
|
145
|
+
const vault = vaultConfigFromEnv(env);
|
|
146
|
+
return { version: 1, defaultVault: vault.address, vaults: [vault] };
|
|
147
|
+
}
|
|
148
|
+
const catalog = parseVaultCatalog(JSON.parse(readFileSync2(path, "utf8")));
|
|
149
|
+
const selected = env.SUBLY_VAULT_ADDRESS?.trim() || catalog.defaultVault;
|
|
150
|
+
if (!catalog.vaults.some((vault) => vault.address === selected)) {
|
|
151
|
+
throw new Error("SUBLY_VAULT_ADDRESS must be in SUBLY_VAULTS_FILE");
|
|
152
|
+
}
|
|
153
|
+
return { ...catalog, defaultVault: selected };
|
|
154
|
+
}
|
|
155
|
+
function defaultCatalogVault(catalog) {
|
|
156
|
+
return catalog.vaults.find((vault) => vault.address === catalog.defaultVault);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ../../src/config/constants.ts
|
|
160
|
+
var PAYMENT_SCHEME = "subly-yield-exact";
|
|
161
|
+
var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
162
|
+
var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
163
|
+
var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
164
|
+
var SUBLY_VAULT = defaultCatalogVault(vaultCatalogFromEnv());
|
|
165
|
+
var USDC_DECIMALS = 6;
|
|
166
|
+
|
|
167
|
+
// ../../src/client/agent-wallet-signer.ts
|
|
168
|
+
import { signBytes } from "@solana/kit";
|
|
169
|
+
import bs586 from "bs58";
|
|
170
|
+
import nacl2 from "tweetnacl";
|
|
171
|
+
|
|
172
|
+
// ../../src/solana/tx.ts
|
|
173
|
+
import bs582 from "bs58";
|
|
174
|
+
import {
|
|
175
|
+
appendTransactionMessageInstructions,
|
|
176
|
+
compileTransaction,
|
|
177
|
+
compressTransactionMessageUsingAddressLookupTables,
|
|
178
|
+
createTransactionMessage,
|
|
179
|
+
getBase64EncodedWireTransaction,
|
|
180
|
+
getTransactionDecoder,
|
|
181
|
+
partiallySignTransaction,
|
|
182
|
+
pipe,
|
|
183
|
+
setTransactionMessageFeePayer,
|
|
184
|
+
setTransactionMessageLifetimeUsingBlockhash
|
|
185
|
+
} from "@solana/kit";
|
|
186
|
+
|
|
187
|
+
// ../../src/lib/hash.ts
|
|
188
|
+
import { createHash } from "node:crypto";
|
|
189
|
+
function sha256TaggedHex(data) {
|
|
190
|
+
return `sha256-${createHash("sha256").update(data).digest("hex")}`;
|
|
191
|
+
}
|
|
192
|
+
function stableStringify(value) {
|
|
193
|
+
if (value === null) {
|
|
194
|
+
return "null";
|
|
195
|
+
}
|
|
196
|
+
if (typeof value === "bigint") {
|
|
197
|
+
return JSON.stringify(value.toString());
|
|
198
|
+
}
|
|
199
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
200
|
+
return JSON.stringify(value);
|
|
201
|
+
}
|
|
202
|
+
if (Array.isArray(value)) {
|
|
203
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
204
|
+
}
|
|
205
|
+
const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
|
|
206
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
207
|
+
}
|
|
208
|
+
function hashStableJson(value) {
|
|
209
|
+
return sha256TaggedHex(stableStringify(value));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ../../src/solana/tx.ts
|
|
213
|
+
function decodeSerializedTransaction(serializedBase64) {
|
|
214
|
+
return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
|
|
215
|
+
}
|
|
216
|
+
function attachExternalSignatureToTransaction(params) {
|
|
217
|
+
if (!(params.signer in params.transaction.signatures)) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`transaction does not expect a signature from ${params.signer}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const transaction = Object.freeze({
|
|
223
|
+
...params.transaction,
|
|
224
|
+
signatures: Object.freeze({
|
|
225
|
+
...params.transaction.signatures,
|
|
226
|
+
[params.signer]: params.signature
|
|
227
|
+
})
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
serializedBase64: getBase64EncodedWireTransaction(transaction),
|
|
231
|
+
transaction
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async function addSignaturesToSerializedTransaction(params) {
|
|
235
|
+
const decoded = decodeSerializedTransaction(params.serializedBase64);
|
|
236
|
+
const signed = await partiallySignTransaction(params.signers, decoded);
|
|
237
|
+
return {
|
|
238
|
+
serializedBase64: getBase64EncodedWireTransaction(signed),
|
|
239
|
+
transaction: signed
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function signatureBase58ForSigner(transaction, signer2) {
|
|
243
|
+
const signature = transaction.signatures[signer2];
|
|
244
|
+
if (signature === null || signature === void 0) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
return bs582.encode(signature);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ../../src/client/remote-signer-transport.ts
|
|
251
|
+
import bs583 from "bs58";
|
|
252
|
+
import nacl from "tweetnacl";
|
|
253
|
+
var RemoteSigningError = class extends Error {
|
|
254
|
+
constructor(provider, message, detail = null) {
|
|
255
|
+
super(`[${provider}] ${message}`);
|
|
256
|
+
this.provider = provider;
|
|
257
|
+
this.detail = detail;
|
|
258
|
+
this.name = "RemoteSigningError";
|
|
259
|
+
}
|
|
260
|
+
provider;
|
|
261
|
+
detail;
|
|
262
|
+
};
|
|
263
|
+
function ed25519PublicKeyBytes(provider, walletAddress) {
|
|
264
|
+
let bytes;
|
|
265
|
+
try {
|
|
266
|
+
bytes = bs583.decode(walletAddress);
|
|
267
|
+
} catch {
|
|
268
|
+
throw new RemoteSigningError(
|
|
269
|
+
provider,
|
|
270
|
+
`wallet address ${walletAddress} is not base58`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (bytes.length !== 32) {
|
|
274
|
+
throw new RemoteSigningError(
|
|
275
|
+
provider,
|
|
276
|
+
`wallet address ${walletAddress} is not a 32-byte ed25519 key`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return bytes;
|
|
280
|
+
}
|
|
281
|
+
function verifiedEd25519Signature(params) {
|
|
282
|
+
const publicKey2 = ed25519PublicKeyBytes(params.provider, params.walletAddress);
|
|
283
|
+
const encoded = params.encodedSignature.trim();
|
|
284
|
+
for (const candidate of decodeSignatureCandidates(encoded)) {
|
|
285
|
+
if (nacl.sign.detached.verify(params.message, candidate, publicKey2)) {
|
|
286
|
+
return candidate;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
throw new RemoteSigningError(
|
|
290
|
+
params.provider,
|
|
291
|
+
`signature did not verify for wallet ${params.walletAddress}`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
function decodeSignatureCandidates(encoded) {
|
|
295
|
+
const candidates = [];
|
|
296
|
+
const hex = encoded.startsWith("0x") ? encoded.slice(2) : encoded;
|
|
297
|
+
if (/^[0-9a-fA-F]{128}$/.test(hex)) {
|
|
298
|
+
candidates.push(Uint8Array.from(Buffer.from(hex, "hex")));
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
const fromBase58 = bs583.decode(encoded);
|
|
302
|
+
if (fromBase58.length === 64) {
|
|
303
|
+
candidates.push(fromBase58);
|
|
304
|
+
}
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
if (/^[A-Za-z0-9+/=_-]+$/.test(encoded)) {
|
|
308
|
+
const fromBase64 = Uint8Array.from(
|
|
309
|
+
Buffer.from(encoded.replace(/-/g, "+").replace(/_/g, "/"), "base64")
|
|
310
|
+
);
|
|
311
|
+
if (fromBase64.length === 64) {
|
|
312
|
+
candidates.push(fromBase64);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return candidates;
|
|
316
|
+
}
|
|
317
|
+
async function requestVerifiedTransactionSignature(params) {
|
|
318
|
+
const { transport } = params;
|
|
319
|
+
const signedBase64 = await transport.signTransaction(
|
|
320
|
+
params.serializedTransactionBase64
|
|
321
|
+
);
|
|
322
|
+
let returned;
|
|
323
|
+
try {
|
|
324
|
+
returned = decodeSerializedTransaction(signedBase64);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
throw new RemoteSigningError(
|
|
327
|
+
transport.provider,
|
|
328
|
+
"provider returned an undecodable signed transaction",
|
|
329
|
+
error
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
const signature = returned.signatures[transport.walletAddress] ?? null;
|
|
333
|
+
if (signature === null) {
|
|
334
|
+
throw new RemoteSigningError(
|
|
335
|
+
transport.provider,
|
|
336
|
+
`signed transaction is missing the signature for ${transport.walletAddress}`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
const publicKey2 = params.publicKey ?? ed25519PublicKeyBytes(transport.provider, transport.walletAddress);
|
|
340
|
+
if (!nacl.sign.detached.verify(params.messageBytes, signature, publicKey2)) {
|
|
341
|
+
throw new RemoteSigningError(
|
|
342
|
+
transport.provider,
|
|
343
|
+
"returned signature does not verify over the requested transaction"
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return signature;
|
|
347
|
+
}
|
|
348
|
+
async function externallySignedAgentTransaction(params) {
|
|
349
|
+
const original = decodeSerializedTransaction(params.serializedTransaction);
|
|
350
|
+
const signature = await requestVerifiedTransactionSignature({
|
|
351
|
+
transport: params.transport,
|
|
352
|
+
serializedTransactionBase64: params.serializedTransaction,
|
|
353
|
+
messageBytes: original.messageBytes
|
|
354
|
+
});
|
|
355
|
+
const attached = attachExternalSignatureToTransaction({
|
|
356
|
+
transaction: original,
|
|
357
|
+
signer: params.transport.walletAddress,
|
|
358
|
+
signature
|
|
359
|
+
});
|
|
360
|
+
return {
|
|
361
|
+
serializedTransaction: attached.serializedBase64,
|
|
362
|
+
agentSignature: bs583.encode(signature)
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
async function providerJsonRequest(params) {
|
|
366
|
+
const response = await params.fetchImpl(`${params.baseUrl}${params.path}`, {
|
|
367
|
+
method: params.method,
|
|
368
|
+
headers: { ...params.headers, "content-type": "application/json" },
|
|
369
|
+
...params.body === void 0 ? {} : { body: JSON.stringify(params.body) }
|
|
370
|
+
});
|
|
371
|
+
let json = null;
|
|
372
|
+
try {
|
|
373
|
+
json = await response.json();
|
|
374
|
+
} catch {
|
|
375
|
+
json = null;
|
|
376
|
+
}
|
|
377
|
+
if (!response.ok) {
|
|
378
|
+
throw new RemoteSigningError(
|
|
379
|
+
params.provider,
|
|
380
|
+
`${params.method} ${params.path} failed with ${response.status}`,
|
|
381
|
+
json
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
return json;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ../../src/client/transaction-intent-validator.ts
|
|
388
|
+
import bs585 from "bs58";
|
|
389
|
+
import { getCompiledTransactionMessageDecoder } from "@solana/kit";
|
|
390
|
+
|
|
391
|
+
// ../../src/domain/request-binding.ts
|
|
392
|
+
function computeRequestBindingHash(fields) {
|
|
393
|
+
return hashStableJson({
|
|
394
|
+
sellerRequestId: fields.sellerRequestId,
|
|
395
|
+
httpMethod: fields.httpMethod.toUpperCase(),
|
|
396
|
+
canonicalResourceUrl: fields.canonicalResourceUrl,
|
|
397
|
+
requestBodyHash: fields.requestBodyHash,
|
|
398
|
+
seller: fields.seller,
|
|
399
|
+
asset: fields.asset,
|
|
400
|
+
amountRawUsdc: fields.amountRawUsdc,
|
|
401
|
+
payTo: fields.payTo,
|
|
402
|
+
sellerUsdcAta: fields.sellerUsdcAta
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ../../src/lib/associated-token-account.ts
|
|
407
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
408
|
+
import bs584 from "bs58";
|
|
409
|
+
var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
|
|
410
|
+
var ED25519_P = (1n << 255n) - 19n;
|
|
411
|
+
var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
|
|
412
|
+
function deriveAssociatedTokenAddress(params) {
|
|
413
|
+
const owner = decodePublicKey(params.owner, "owner");
|
|
414
|
+
const mint = decodePublicKey(params.mint ?? SUBLY_VAULT.usdcMint, "mint");
|
|
415
|
+
const tokenProgramId = decodePublicKey(
|
|
416
|
+
params.tokenProgramId ?? SPL_TOKEN_PROGRAM_ID,
|
|
417
|
+
"tokenProgramId"
|
|
418
|
+
);
|
|
419
|
+
const associatedTokenProgramId = decodePublicKey(
|
|
420
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
421
|
+
"associatedTokenProgramId"
|
|
422
|
+
);
|
|
423
|
+
for (let bump = 255; bump >= 0; bump -= 1) {
|
|
424
|
+
const address2 = createProgramAddress(
|
|
425
|
+
[owner, tokenProgramId, mint, Uint8Array.of(bump)],
|
|
426
|
+
associatedTokenProgramId
|
|
427
|
+
);
|
|
428
|
+
if (address2 !== null) {
|
|
429
|
+
return bs584.encode(address2);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
throw new Error("Unable to derive associated token account address");
|
|
433
|
+
}
|
|
434
|
+
function createProgramAddress(seeds, programId) {
|
|
435
|
+
const hash = createHash2("sha256");
|
|
436
|
+
for (const seed of seeds) {
|
|
437
|
+
hash.update(seed);
|
|
438
|
+
}
|
|
439
|
+
hash.update(programId);
|
|
440
|
+
hash.update(PDA_MARKER);
|
|
441
|
+
const digest = hash.digest();
|
|
442
|
+
return isEd25519Point(digest) ? null : new Uint8Array(digest);
|
|
443
|
+
}
|
|
444
|
+
function decodePublicKey(value, fieldName) {
|
|
445
|
+
const decoded = bs584.decode(value);
|
|
446
|
+
if (decoded.length !== 32) {
|
|
447
|
+
throw new Error(`${fieldName} must be a 32-byte public key`);
|
|
448
|
+
}
|
|
449
|
+
return decoded;
|
|
450
|
+
}
|
|
451
|
+
function isEd25519Point(bytes) {
|
|
452
|
+
if (bytes.length !== 32) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
const yBytes = Uint8Array.from(bytes);
|
|
456
|
+
yBytes[31] = yBytes[31] & 127;
|
|
457
|
+
const y = littleEndianToBigInt(yBytes);
|
|
458
|
+
if (y >= ED25519_P) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
const ySquared = mod(y * y, ED25519_P);
|
|
462
|
+
const numerator = mod(ySquared - 1n, ED25519_P);
|
|
463
|
+
const denominator = mod(ED25519_D * ySquared + 1n, ED25519_P);
|
|
464
|
+
if (denominator === 0n) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
const xSquared = mod(
|
|
468
|
+
numerator * modPow(denominator, ED25519_P - 2n, ED25519_P),
|
|
469
|
+
ED25519_P
|
|
470
|
+
);
|
|
471
|
+
return xSquared === 0n || modPow(xSquared, (ED25519_P - 1n) / 2n, ED25519_P) === 1n;
|
|
472
|
+
}
|
|
473
|
+
function littleEndianToBigInt(bytes) {
|
|
474
|
+
let value = 0n;
|
|
475
|
+
for (let index = bytes.length - 1; index >= 0; index -= 1) {
|
|
476
|
+
value = (value << 8n) + BigInt(bytes[index]);
|
|
477
|
+
}
|
|
478
|
+
return value;
|
|
479
|
+
}
|
|
480
|
+
function mod(value, modulus) {
|
|
481
|
+
const result = value % modulus;
|
|
482
|
+
return result >= 0n ? result : result + modulus;
|
|
483
|
+
}
|
|
484
|
+
function modPow(base, exponent, modulus) {
|
|
485
|
+
let result = 1n;
|
|
486
|
+
let nextBase = mod(base, modulus);
|
|
487
|
+
let nextExponent = exponent;
|
|
488
|
+
while (nextExponent > 0n) {
|
|
489
|
+
if ((nextExponent & 1n) === 1n) {
|
|
490
|
+
result = mod(result * nextBase, modulus);
|
|
491
|
+
}
|
|
492
|
+
nextBase = mod(nextBase * nextBase, modulus);
|
|
493
|
+
nextExponent >>= 1n;
|
|
494
|
+
}
|
|
495
|
+
return result;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ../../src/client/transaction-intent-validator.ts
|
|
499
|
+
var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
|
|
500
|
+
var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
|
|
501
|
+
var ASSOCIATED_TOKEN_PROGRAM_ID2 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
502
|
+
var MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
|
|
503
|
+
var KVAULT_PROGRAM_ID = "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd";
|
|
504
|
+
var KAMINO_FARMS_PROGRAM_ID = "FarmsPZpWu9i7Kky8tPN37rs2TpmMrAZrC7S7vJa91Hr";
|
|
505
|
+
var KVAULT_WITHDRAW_DISCRIMINATOR = Uint8Array.from([
|
|
506
|
+
183,
|
|
507
|
+
18,
|
|
508
|
+
70,
|
|
509
|
+
156,
|
|
510
|
+
148,
|
|
511
|
+
109,
|
|
512
|
+
161,
|
|
513
|
+
34
|
|
514
|
+
]);
|
|
515
|
+
var KVAULT_WITHDRAW_FROM_AVAILABLE_DISCRIMINATOR = Uint8Array.from([
|
|
516
|
+
19,
|
|
517
|
+
131,
|
|
518
|
+
112,
|
|
519
|
+
155,
|
|
520
|
+
170,
|
|
521
|
+
220,
|
|
522
|
+
34,
|
|
523
|
+
57
|
|
524
|
+
]);
|
|
525
|
+
var KVAULT_DEPOSIT_DISCRIMINATOR = Uint8Array.from([
|
|
526
|
+
242,
|
|
527
|
+
35,
|
|
528
|
+
198,
|
|
529
|
+
137,
|
|
530
|
+
82,
|
|
531
|
+
225,
|
|
532
|
+
242,
|
|
533
|
+
182
|
|
534
|
+
]);
|
|
535
|
+
var U64_MAX = 18446744073709551615n;
|
|
536
|
+
var MAX_TEMP_ACCOUNT_LAMPORTS = 10000000n;
|
|
537
|
+
var DEFAULT_MAX_COMPUTE_UNIT_LIMIT = 14e5;
|
|
538
|
+
var DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS = 100000n;
|
|
539
|
+
var IntentValidationError = class extends Error {
|
|
540
|
+
reason;
|
|
541
|
+
constructor(reason, message) {
|
|
542
|
+
super(message);
|
|
543
|
+
this.name = "IntentValidationError";
|
|
544
|
+
this.reason = reason;
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
function reject(reason, message) {
|
|
548
|
+
throw new IntentValidationError(reason, message);
|
|
549
|
+
}
|
|
550
|
+
function decodeIntentTransaction(params) {
|
|
551
|
+
const wire = Buffer.from(params.serializedTransaction, "base64");
|
|
552
|
+
const signatureCount = readShortVec(wire, 0);
|
|
553
|
+
if (signatureCount === null) {
|
|
554
|
+
reject("invalid_transaction_encoding", "Cannot parse signature count");
|
|
555
|
+
}
|
|
556
|
+
const messageOffset = signatureCount.nextOffset + signatureCount.value * 64;
|
|
557
|
+
if (messageOffset >= wire.length) {
|
|
558
|
+
reject("invalid_transaction_encoding", "Transaction has no message bytes");
|
|
559
|
+
}
|
|
560
|
+
const messageBytes = wire.subarray(messageOffset);
|
|
561
|
+
const compiled = getCompiledTransactionMessageDecoder().decode(messageBytes);
|
|
562
|
+
if (compiled.version !== 0) {
|
|
563
|
+
reject("unsupported_transaction_version", "Only v0 transactions are supported");
|
|
564
|
+
}
|
|
565
|
+
const staticAccounts = compiled.staticAccounts.map(String);
|
|
566
|
+
const loadedWritable = [];
|
|
567
|
+
const loadedReadonly = [];
|
|
568
|
+
const lookups = compiled.addressTableLookups ?? [];
|
|
569
|
+
for (const rawLookup of lookups) {
|
|
570
|
+
const lookup = rawLookup;
|
|
571
|
+
const table = params.lookupTables?.[String(lookup.lookupTableAddress)];
|
|
572
|
+
if (table === void 0) {
|
|
573
|
+
reject(
|
|
574
|
+
"lookup_table_unresolved",
|
|
575
|
+
`Transaction references unknown lookup table ${lookup.lookupTableAddress}`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
const writableIndexes = lookup.writableIndexes ?? lookup.writableIndices ?? [];
|
|
579
|
+
const readonlyIndexes = lookup.readonlyIndexes ?? lookup.readableIndices ?? [];
|
|
580
|
+
for (const index of writableIndexes) {
|
|
581
|
+
const resolved = table[index];
|
|
582
|
+
if (resolved === void 0) {
|
|
583
|
+
reject("lookup_table_unresolved", "Lookup table index out of range");
|
|
584
|
+
}
|
|
585
|
+
loadedWritable.push(String(resolved));
|
|
586
|
+
}
|
|
587
|
+
for (const index of readonlyIndexes) {
|
|
588
|
+
const resolved = table[index];
|
|
589
|
+
if (resolved === void 0) {
|
|
590
|
+
reject("lookup_table_unresolved", "Lookup table index out of range");
|
|
591
|
+
}
|
|
592
|
+
loadedReadonly.push(String(resolved));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const orderedAccounts = [...staticAccounts, ...loadedWritable, ...loadedReadonly];
|
|
596
|
+
const instructions = compiled.instructions.map(
|
|
597
|
+
(instruction) => {
|
|
598
|
+
const programAddress = orderedAccounts[instruction.programAddressIndex];
|
|
599
|
+
if (programAddress === void 0) {
|
|
600
|
+
reject("invalid_transaction_encoding", "Program index out of range");
|
|
601
|
+
}
|
|
602
|
+
const accounts = (instruction.accountIndices ?? []).map((index) => {
|
|
603
|
+
const account = orderedAccounts[index];
|
|
604
|
+
if (account === void 0) {
|
|
605
|
+
reject("invalid_transaction_encoding", "Account index out of range");
|
|
606
|
+
}
|
|
607
|
+
return account;
|
|
608
|
+
});
|
|
609
|
+
return {
|
|
610
|
+
programAddress,
|
|
611
|
+
accounts,
|
|
612
|
+
data: instruction.data === void 0 ? new Uint8Array() : Uint8Array.from(instruction.data)
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
);
|
|
616
|
+
const feePayer = staticAccounts[0];
|
|
617
|
+
if (feePayer === void 0) {
|
|
618
|
+
reject("invalid_transaction_encoding", "Transaction has no fee payer");
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
feePayer,
|
|
622
|
+
requiredSigners: staticAccounts.slice(0, compiled.header.numSignerAccounts),
|
|
623
|
+
instructions,
|
|
624
|
+
messageHash: sha256TaggedHex(Buffer.from(messageBytes))
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function validatePaymentIntentTransaction(params) {
|
|
628
|
+
const { intent } = params;
|
|
629
|
+
const now = params.nowMs ?? Date.now();
|
|
630
|
+
const policy = resolveIntentValidationPolicy(params.policy);
|
|
631
|
+
if (new Date(intent.expiresAt).getTime() <= now) {
|
|
632
|
+
reject("expired", "Payment intent has expired");
|
|
633
|
+
}
|
|
634
|
+
if (intent.scheme !== PAYMENT_SCHEME) {
|
|
635
|
+
reject("scheme_mismatch", `scheme must be ${PAYMENT_SCHEME}`);
|
|
636
|
+
}
|
|
637
|
+
if (intent.network !== SOLANA_MAINNET_NETWORK) {
|
|
638
|
+
reject("network_mismatch", "Unsupported network");
|
|
639
|
+
}
|
|
640
|
+
if (intent.vault !== policy.vault.address) {
|
|
641
|
+
reject("vault_mismatch", "Unsupported vault");
|
|
642
|
+
}
|
|
643
|
+
if (intent.shareMint !== policy.vault.shareMint) {
|
|
644
|
+
reject("share_mint_mismatch", "Unsupported share mint");
|
|
645
|
+
}
|
|
646
|
+
if (intent.farm !== policy.vault.farm) {
|
|
647
|
+
reject("farm_mismatch", "Unsupported Kamino farm");
|
|
648
|
+
}
|
|
649
|
+
if (intent.asset !== policy.vault.usdcMint) {
|
|
650
|
+
reject("asset_mismatch", "Only USDC payments are supported");
|
|
651
|
+
}
|
|
652
|
+
if (intent.memo !== intent.paymentId) {
|
|
653
|
+
reject("memo_mismatch", "Memo must equal the paymentId");
|
|
654
|
+
}
|
|
655
|
+
const expectedBinding = computeRequestBindingHash({
|
|
656
|
+
sellerRequestId: intent.sellerRequestId,
|
|
657
|
+
httpMethod: intent.httpMethod,
|
|
658
|
+
canonicalResourceUrl: intent.canonicalResourceUrl,
|
|
659
|
+
requestBodyHash: intent.requestBodyHash,
|
|
660
|
+
seller: intent.seller,
|
|
661
|
+
asset: intent.asset,
|
|
662
|
+
amountRawUsdc: intent.amountRawUsdc,
|
|
663
|
+
payTo: intent.payTo,
|
|
664
|
+
sellerUsdcAta: intent.sellerUsdcAta
|
|
665
|
+
});
|
|
666
|
+
if (expectedBinding !== intent.requestBindingHash) {
|
|
667
|
+
reject(
|
|
668
|
+
"request_binding_mismatch",
|
|
669
|
+
"requestBindingHash does not match the request fields"
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
const expectedSellerAta = deriveAssociatedTokenAddress({
|
|
673
|
+
owner: intent.payTo,
|
|
674
|
+
mint: intent.asset
|
|
675
|
+
});
|
|
676
|
+
if (expectedSellerAta !== intent.sellerUsdcAta) {
|
|
677
|
+
reject(
|
|
678
|
+
"seller_ata_mismatch",
|
|
679
|
+
"sellerUsdcAta must be the associated USDC account for payTo"
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
const expectedDustAta = deriveAssociatedTokenAddress({
|
|
683
|
+
owner: intent.wallet,
|
|
684
|
+
mint: intent.asset
|
|
685
|
+
});
|
|
686
|
+
if (expectedDustAta !== intent.dustRecipientUsdcAta) {
|
|
687
|
+
reject(
|
|
688
|
+
"dust_recipient_mismatch",
|
|
689
|
+
"dustRecipientUsdcAta must be the agent wallet's USDC ATA"
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
const decoded = decodeIntentTransaction({
|
|
693
|
+
serializedTransaction: params.serializedTransaction,
|
|
694
|
+
...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
|
|
695
|
+
});
|
|
696
|
+
if (decoded.messageHash !== intent.preparedMessageHash) {
|
|
697
|
+
reject("message_hash_mismatch", "Prepared message hash mismatch");
|
|
698
|
+
}
|
|
699
|
+
if (decoded.feePayer !== intent.feePayer) {
|
|
700
|
+
reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
|
|
701
|
+
}
|
|
702
|
+
const expectedSigners = /* @__PURE__ */ new Set([
|
|
703
|
+
intent.feePayer,
|
|
704
|
+
intent.wallet,
|
|
705
|
+
intent.temporarySettlementTokenAccount
|
|
706
|
+
]);
|
|
707
|
+
if (decoded.requiredSigners.length !== expectedSigners.size || !decoded.requiredSigners.every((signer2) => expectedSigners.has(signer2))) {
|
|
708
|
+
reject(
|
|
709
|
+
"unexpected_signers",
|
|
710
|
+
"Transaction signers must be exactly the sponsor, the agent wallet, and the temporary settlement account"
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
const ixs = [...decoded.instructions];
|
|
714
|
+
expectComputeBudgetPair(ixs, policy);
|
|
715
|
+
expectCreateTemporaryAccount(ixs, intent, policy);
|
|
716
|
+
expectInitializeTemporaryAccount(ixs, intent);
|
|
717
|
+
consumeFarmInstructions(ixs, intent);
|
|
718
|
+
expectKvaultWithdraw(ixs, {
|
|
719
|
+
wallet: intent.wallet,
|
|
720
|
+
vault: intent.vault,
|
|
721
|
+
shareMint: intent.shareMint,
|
|
722
|
+
asset: intent.asset,
|
|
723
|
+
userTokenAccount: intent.temporarySettlementTokenAccount,
|
|
724
|
+
maxSharesToRedeemRaw: BigInt(intent.maxSharesToRedeemRaw),
|
|
725
|
+
allowFullExit: false
|
|
726
|
+
});
|
|
727
|
+
expectTransferChecked(ixs, {
|
|
728
|
+
source: intent.temporarySettlementTokenAccount,
|
|
729
|
+
mint: intent.asset,
|
|
730
|
+
destination: intent.sellerUsdcAta,
|
|
731
|
+
authority: intent.wallet,
|
|
732
|
+
amount: BigInt(intent.amountRawUsdc),
|
|
733
|
+
label: "seller transfer"
|
|
734
|
+
});
|
|
735
|
+
if (ixs[0] !== void 0 && ixs[0].programAddress === SPL_TOKEN_PROGRAM_ID && ixs[0].data[0] === 12) {
|
|
736
|
+
expectTransferChecked(ixs, {
|
|
737
|
+
source: intent.temporarySettlementTokenAccount,
|
|
738
|
+
mint: intent.asset,
|
|
739
|
+
destination: intent.dustRecipientUsdcAta,
|
|
740
|
+
authority: intent.wallet,
|
|
741
|
+
amount: null,
|
|
742
|
+
label: "dust sweep"
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
expectCloseAccount(ixs, {
|
|
746
|
+
account: intent.temporarySettlementTokenAccount,
|
|
747
|
+
destination: intent.feePayer,
|
|
748
|
+
owner: intent.wallet
|
|
749
|
+
});
|
|
750
|
+
expectMemo(ixs, intent.memo);
|
|
751
|
+
if (ixs.length > 0) {
|
|
752
|
+
reject(
|
|
753
|
+
"unexpected_instruction",
|
|
754
|
+
`Transaction contains ${ixs.length} unexpected trailing instruction(s)`
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function validateDepositIntentTransaction(params) {
|
|
759
|
+
const { intent } = params;
|
|
760
|
+
const now = params.nowMs ?? Date.now();
|
|
761
|
+
const policy = resolveIntentValidationPolicy(params.policy);
|
|
762
|
+
if (new Date(intent.expiresAt).getTime() <= now) {
|
|
763
|
+
reject("expired", "Deposit intent has expired");
|
|
764
|
+
}
|
|
765
|
+
assertVaultIntentTargets(intent, policy.vault);
|
|
766
|
+
const decoded = decodeIntentTransaction({
|
|
767
|
+
serializedTransaction: params.serializedTransaction,
|
|
768
|
+
...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
|
|
769
|
+
});
|
|
770
|
+
if (decoded.messageHash !== intent.preparedMessageHash) {
|
|
771
|
+
reject("message_hash_mismatch", "Prepared message hash mismatch");
|
|
772
|
+
}
|
|
773
|
+
if (decoded.feePayer !== intent.feePayer) {
|
|
774
|
+
reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
|
|
775
|
+
}
|
|
776
|
+
let sawDeposit = false;
|
|
777
|
+
for (const ix of decoded.instructions) {
|
|
778
|
+
switch (ix.programAddress) {
|
|
779
|
+
case COMPUTE_BUDGET_PROGRAM_ID:
|
|
780
|
+
validateComputeBudgetInstruction(ix, policy);
|
|
781
|
+
break;
|
|
782
|
+
case ASSOCIATED_TOKEN_PROGRAM_ID2:
|
|
783
|
+
expectAtaCreateForOwner(ix, intent.wallet);
|
|
784
|
+
break;
|
|
785
|
+
case MEMO_PROGRAM_ID:
|
|
786
|
+
break;
|
|
787
|
+
case KVAULT_PROGRAM_ID: {
|
|
788
|
+
if (sawDeposit) {
|
|
789
|
+
reject("duplicate_deposit", "A deposit intent authorizes exactly one KVault deposit");
|
|
790
|
+
}
|
|
791
|
+
if (!bytesStartWith(ix.data, KVAULT_DEPOSIT_DISCRIMINATOR)) {
|
|
792
|
+
reject("unexpected_instruction", "Unexpected KVault instruction in deposit");
|
|
793
|
+
}
|
|
794
|
+
const maxAmount = readU64LE(ix.data, 8);
|
|
795
|
+
if (maxAmount !== BigInt(intent.amountRawUsdc)) {
|
|
796
|
+
reject("amount_mismatch", "Deposit amount does not match the intent");
|
|
797
|
+
}
|
|
798
|
+
if (ix.accounts[0] !== intent.wallet) {
|
|
799
|
+
reject("wallet_mismatch", "Deposit user is not the agent wallet");
|
|
800
|
+
}
|
|
801
|
+
if (ix.accounts[1] !== intent.vault) {
|
|
802
|
+
reject("vault_mismatch", "Deposit vault mismatch");
|
|
803
|
+
}
|
|
804
|
+
if (ix.accounts[3] !== intent.asset) {
|
|
805
|
+
reject("asset_mismatch", "Deposit token mint mismatch");
|
|
806
|
+
}
|
|
807
|
+
if (ix.accounts[5] !== intent.shareMint) {
|
|
808
|
+
reject("share_mint_mismatch", "Deposit share mint mismatch");
|
|
809
|
+
}
|
|
810
|
+
const expectedSourceAta = deriveAssociatedTokenAddress({
|
|
811
|
+
owner: intent.wallet,
|
|
812
|
+
mint: intent.asset
|
|
813
|
+
});
|
|
814
|
+
if (ix.accounts[6] !== expectedSourceAta) {
|
|
815
|
+
reject(
|
|
816
|
+
"source_ata_mismatch",
|
|
817
|
+
"Deposit source must be the agent wallet's USDC ATA"
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
sawDeposit = true;
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
default:
|
|
824
|
+
reject(
|
|
825
|
+
"unexpected_instruction",
|
|
826
|
+
`Unexpected program ${ix.programAddress} in deposit transaction`
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (!sawDeposit) {
|
|
831
|
+
reject("missing_instruction", "Deposit transaction has no KVault deposit");
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function validateWithdrawalIntentTransaction(params) {
|
|
835
|
+
const { intent } = params;
|
|
836
|
+
const now = params.nowMs ?? Date.now();
|
|
837
|
+
const policy = resolveIntentValidationPolicy(params.policy);
|
|
838
|
+
if (new Date(intent.expiresAt).getTime() <= now) {
|
|
839
|
+
reject("expired", "Withdrawal intent has expired");
|
|
840
|
+
}
|
|
841
|
+
assertVaultIntentTargets(intent, policy.vault);
|
|
842
|
+
const expectedDestination = deriveAssociatedTokenAddress({
|
|
843
|
+
owner: intent.wallet,
|
|
844
|
+
mint: intent.asset
|
|
845
|
+
});
|
|
846
|
+
if (expectedDestination !== intent.destinationUsdcAta) {
|
|
847
|
+
reject(
|
|
848
|
+
"destination_mismatch",
|
|
849
|
+
"Withdrawal destination must be the agent wallet's USDC ATA"
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
const decoded = decodeIntentTransaction({
|
|
853
|
+
serializedTransaction: params.serializedTransaction,
|
|
854
|
+
...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
|
|
855
|
+
});
|
|
856
|
+
if (decoded.messageHash !== intent.preparedMessageHash) {
|
|
857
|
+
reject("message_hash_mismatch", "Prepared message hash mismatch");
|
|
858
|
+
}
|
|
859
|
+
if (decoded.feePayer !== intent.feePayer) {
|
|
860
|
+
reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
|
|
861
|
+
}
|
|
862
|
+
let sawWithdraw = false;
|
|
863
|
+
let farmUserState = null;
|
|
864
|
+
let farmInstructionCount = 0;
|
|
865
|
+
for (const ix of decoded.instructions) {
|
|
866
|
+
switch (ix.programAddress) {
|
|
867
|
+
case COMPUTE_BUDGET_PROGRAM_ID:
|
|
868
|
+
validateComputeBudgetInstruction(ix, policy);
|
|
869
|
+
break;
|
|
870
|
+
case MEMO_PROGRAM_ID:
|
|
871
|
+
break;
|
|
872
|
+
case KAMINO_FARMS_PROGRAM_ID:
|
|
873
|
+
farmInstructionCount += 1;
|
|
874
|
+
if (farmInstructionCount === 1) {
|
|
875
|
+
farmUserState = validateFarmUnstakeInstruction(ix, intent);
|
|
876
|
+
} else if (farmInstructionCount === 2) {
|
|
877
|
+
validateFarmWithdrawInstruction(ix, intent, farmUserState);
|
|
878
|
+
} else {
|
|
879
|
+
reject(
|
|
880
|
+
"farm_instruction_mismatch",
|
|
881
|
+
"Withdrawal may contain only one farm unstake and one farm withdrawal"
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
break;
|
|
885
|
+
case ASSOCIATED_TOKEN_PROGRAM_ID2:
|
|
886
|
+
expectAtaCreateForOwner(ix, intent.wallet);
|
|
887
|
+
break;
|
|
888
|
+
case SPL_TOKEN_PROGRAM_ID: {
|
|
889
|
+
if (ix.data[0] !== 9) {
|
|
890
|
+
reject(
|
|
891
|
+
"unexpected_instruction",
|
|
892
|
+
"Only CloseAccount token instructions are allowed in withdrawals"
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
if (ix.accounts[1] !== intent.wallet || ix.accounts[2] !== intent.wallet) {
|
|
896
|
+
reject(
|
|
897
|
+
"unexpected_instruction",
|
|
898
|
+
"Withdrawal CloseAccount must pay out to the agent wallet"
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
case KVAULT_PROGRAM_ID: {
|
|
904
|
+
if (sawWithdraw) {
|
|
905
|
+
reject(
|
|
906
|
+
"withdraw_mismatch",
|
|
907
|
+
"Withdrawal may contain only one KVault withdraw instruction"
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
validateKvaultWithdrawInstruction(ix, {
|
|
911
|
+
wallet: intent.wallet,
|
|
912
|
+
vault: intent.vault,
|
|
913
|
+
shareMint: intent.shareMint,
|
|
914
|
+
asset: intent.asset,
|
|
915
|
+
userTokenAccount: intent.destinationUsdcAta,
|
|
916
|
+
maxSharesToRedeemRaw: BigInt(intent.maxSharesToRedeemRaw),
|
|
917
|
+
allowFullExit: intent.allowFullExit
|
|
918
|
+
});
|
|
919
|
+
sawWithdraw = true;
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
default:
|
|
923
|
+
reject(
|
|
924
|
+
"unexpected_instruction",
|
|
925
|
+
`Unexpected program ${ix.programAddress} in withdrawal transaction`
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (!sawWithdraw) {
|
|
930
|
+
reject("missing_instruction", "Withdrawal transaction has no KVault withdraw");
|
|
931
|
+
}
|
|
932
|
+
if (farmInstructionCount === 1) {
|
|
933
|
+
reject(
|
|
934
|
+
"farm_instruction_mismatch",
|
|
935
|
+
"A farm unstake must be followed by a farm withdrawal"
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function assertVaultIntentTargets(intent, vault) {
|
|
940
|
+
if (intent.vault !== vault.address) {
|
|
941
|
+
reject("vault_mismatch", "Unsupported vault");
|
|
942
|
+
}
|
|
943
|
+
if (intent.shareMint !== vault.shareMint) {
|
|
944
|
+
reject("share_mint_mismatch", "Unsupported share mint");
|
|
945
|
+
}
|
|
946
|
+
if (intent.farm !== vault.farm) {
|
|
947
|
+
reject("farm_mismatch", "Unsupported Kamino farm");
|
|
948
|
+
}
|
|
949
|
+
if (intent.asset !== vault.usdcMint) {
|
|
950
|
+
reject("asset_mismatch", "Only USDC is supported");
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function resolveIntentValidationPolicy(policy) {
|
|
954
|
+
const resolved = {
|
|
955
|
+
vault: policy?.vault ?? SUBLY_VAULT,
|
|
956
|
+
maxComputeUnitLimit: policy?.maxComputeUnitLimit ?? DEFAULT_MAX_COMPUTE_UNIT_LIMIT,
|
|
957
|
+
maxComputeUnitPriceMicroLamports: policy?.maxComputeUnitPriceMicroLamports ?? DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
|
|
958
|
+
maxTemporaryAccountLamports: policy?.maxTemporaryAccountLamports ?? MAX_TEMP_ACCOUNT_LAMPORTS
|
|
959
|
+
};
|
|
960
|
+
if (!Number.isSafeInteger(resolved.maxComputeUnitLimit) || resolved.maxComputeUnitLimit <= 0) {
|
|
961
|
+
reject("invalid_policy", "maxComputeUnitLimit must be a positive safe integer");
|
|
962
|
+
}
|
|
963
|
+
if (resolved.maxComputeUnitPriceMicroLamports < 0n) {
|
|
964
|
+
reject(
|
|
965
|
+
"invalid_policy",
|
|
966
|
+
"maxComputeUnitPriceMicroLamports must be non-negative"
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
if (resolved.maxTemporaryAccountLamports <= 0n) {
|
|
970
|
+
reject("invalid_policy", "maxTemporaryAccountLamports must be positive");
|
|
971
|
+
}
|
|
972
|
+
return resolved;
|
|
973
|
+
}
|
|
974
|
+
function expectComputeBudgetPair(ixs, policy) {
|
|
975
|
+
for (const discriminator of [2, 3]) {
|
|
976
|
+
const ix = ixs.shift();
|
|
977
|
+
if (ix === void 0 || ix.programAddress !== COMPUTE_BUDGET_PROGRAM_ID || ix.data[0] !== discriminator) {
|
|
978
|
+
reject(
|
|
979
|
+
"compute_budget_mismatch",
|
|
980
|
+
"Transaction must start with ComputeBudget limit and price instructions"
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
validateComputeBudgetInstruction(ix, policy);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
function validateComputeBudgetInstruction(ix, policy) {
|
|
987
|
+
switch (ix.data[0]) {
|
|
988
|
+
case 2: {
|
|
989
|
+
const units = readU32LE(ix.data, 1);
|
|
990
|
+
if (units <= 0 || units > policy.maxComputeUnitLimit) {
|
|
991
|
+
reject(
|
|
992
|
+
"compute_budget_mismatch",
|
|
993
|
+
`Compute unit limit ${units} exceeds policy maximum ${policy.maxComputeUnitLimit}`
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
case 3: {
|
|
999
|
+
const microLamports = readU64LE(ix.data, 1);
|
|
1000
|
+
if (microLamports > policy.maxComputeUnitPriceMicroLamports) {
|
|
1001
|
+
reject(
|
|
1002
|
+
"compute_budget_mismatch",
|
|
1003
|
+
`Compute unit price ${microLamports} exceeds policy maximum ${policy.maxComputeUnitPriceMicroLamports}`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
default:
|
|
1009
|
+
reject("compute_budget_mismatch", "Unexpected ComputeBudget instruction");
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
function expectCreateTemporaryAccount(ixs, intent, policy) {
|
|
1013
|
+
const ix = ixs.shift();
|
|
1014
|
+
if (ix === void 0 || ix.programAddress !== SYSTEM_PROGRAM_ID) {
|
|
1015
|
+
reject("temp_account_mismatch", "Expected System createAccount instruction");
|
|
1016
|
+
}
|
|
1017
|
+
if (ix.data.length < 52 || readU32LE(ix.data, 0) !== 0) {
|
|
1018
|
+
reject("temp_account_mismatch", "Expected createAccount discriminator");
|
|
1019
|
+
}
|
|
1020
|
+
const lamports = readU64LE(ix.data, 4);
|
|
1021
|
+
const space = readU64LE(ix.data, 12);
|
|
1022
|
+
const owner = bs585.encode(ix.data.subarray(20, 52));
|
|
1023
|
+
if (space !== 165n) {
|
|
1024
|
+
reject("temp_account_mismatch", "Temporary account space must be 165 bytes");
|
|
1025
|
+
}
|
|
1026
|
+
if (owner !== SPL_TOKEN_PROGRAM_ID) {
|
|
1027
|
+
reject("temp_account_mismatch", "Temporary account owner must be the token program");
|
|
1028
|
+
}
|
|
1029
|
+
if (lamports > policy.maxTemporaryAccountLamports) {
|
|
1030
|
+
reject("temp_account_mismatch", "Temporary account rent exceeds the cap");
|
|
1031
|
+
}
|
|
1032
|
+
if (ix.accounts[0] !== intent.feePayer) {
|
|
1033
|
+
reject("temp_account_mismatch", "Temporary account must be funded by the sponsor");
|
|
1034
|
+
}
|
|
1035
|
+
if (ix.accounts[1] !== intent.temporarySettlementTokenAccount) {
|
|
1036
|
+
reject("temp_account_mismatch", "createAccount target is not the temporary account");
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function expectInitializeTemporaryAccount(ixs, intent) {
|
|
1040
|
+
const ix = ixs.shift();
|
|
1041
|
+
if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 18) {
|
|
1042
|
+
reject("temp_account_mismatch", "Expected InitializeAccount3 instruction");
|
|
1043
|
+
}
|
|
1044
|
+
const owner = bs585.encode(ix.data.subarray(1, 33));
|
|
1045
|
+
if (owner !== intent.wallet) {
|
|
1046
|
+
reject(
|
|
1047
|
+
"temp_account_mismatch",
|
|
1048
|
+
"Temporary account token authority must be the agent wallet"
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
if (ix.accounts[0] !== intent.temporarySettlementTokenAccount) {
|
|
1052
|
+
reject("temp_account_mismatch", "InitializeAccount3 target mismatch");
|
|
1053
|
+
}
|
|
1054
|
+
if (ix.accounts[1] !== intent.asset) {
|
|
1055
|
+
reject("temp_account_mismatch", "Temporary account mint must be USDC");
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function consumeFarmInstructions(ixs, intent) {
|
|
1059
|
+
if (ixs[0]?.programAddress !== KAMINO_FARMS_PROGRAM_ID) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
const unstake = ixs.shift();
|
|
1063
|
+
const userState = validateFarmUnstakeInstruction(unstake, intent);
|
|
1064
|
+
if (ixs[0]?.programAddress !== KAMINO_FARMS_PROGRAM_ID) {
|
|
1065
|
+
reject(
|
|
1066
|
+
"farm_instruction_mismatch",
|
|
1067
|
+
"A farm unstake must be followed by a farm withdrawal"
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
const withdraw = ixs.shift();
|
|
1071
|
+
validateFarmWithdrawInstruction(withdraw, intent, userState);
|
|
1072
|
+
if (ixs[0]?.programAddress === KAMINO_FARMS_PROGRAM_ID) {
|
|
1073
|
+
reject(
|
|
1074
|
+
"farm_instruction_mismatch",
|
|
1075
|
+
"Payment may contain only one farm unstake and one farm withdrawal"
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
var KAMINO_FARMS_UNSTAKE_DISCRIMINATOR = Uint8Array.from([
|
|
1080
|
+
90,
|
|
1081
|
+
95,
|
|
1082
|
+
107,
|
|
1083
|
+
42,
|
|
1084
|
+
205,
|
|
1085
|
+
124,
|
|
1086
|
+
50,
|
|
1087
|
+
225
|
|
1088
|
+
]);
|
|
1089
|
+
var KAMINO_FARMS_WITHDRAW_UNSTAKED_DISCRIMINATOR = Uint8Array.from([
|
|
1090
|
+
36,
|
|
1091
|
+
102,
|
|
1092
|
+
187,
|
|
1093
|
+
49,
|
|
1094
|
+
220,
|
|
1095
|
+
36,
|
|
1096
|
+
132,
|
|
1097
|
+
67
|
|
1098
|
+
]);
|
|
1099
|
+
function validateFarmUnstakeInstruction(ix, intent) {
|
|
1100
|
+
if (ix.programAddress !== KAMINO_FARMS_PROGRAM_ID || !bytesStartWith(ix.data, KAMINO_FARMS_UNSTAKE_DISCRIMINATOR) || ix.data.length !== 24 || readU128LE(ix.data, 8) <= 0n) {
|
|
1101
|
+
reject(
|
|
1102
|
+
"farm_instruction_mismatch",
|
|
1103
|
+
"Expected a non-zero Kamino farm unstake instruction"
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
if (ix.accounts.length !== 4) {
|
|
1107
|
+
reject(
|
|
1108
|
+
"farm_instruction_mismatch",
|
|
1109
|
+
"Farm unstake account list is not canonical"
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
if (ix.accounts[0] !== intent.wallet) {
|
|
1113
|
+
reject("farm_instruction_mismatch", "Farm unstake owner must be the agent wallet");
|
|
1114
|
+
}
|
|
1115
|
+
if (ix.accounts[2] !== intent.farm) {
|
|
1116
|
+
reject("farm_instruction_mismatch", "Farm unstake target is not the approved farm");
|
|
1117
|
+
}
|
|
1118
|
+
return ix.accounts[1];
|
|
1119
|
+
}
|
|
1120
|
+
function validateFarmWithdrawInstruction(ix, intent, expectedUserState) {
|
|
1121
|
+
if (ix.programAddress !== KAMINO_FARMS_PROGRAM_ID || !bytesStartWith(ix.data, KAMINO_FARMS_WITHDRAW_UNSTAKED_DISCRIMINATOR) || ix.data.length !== 8) {
|
|
1122
|
+
reject(
|
|
1123
|
+
"farm_instruction_mismatch",
|
|
1124
|
+
"Expected a canonical Kamino farm withdrawal instruction"
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
if (ix.accounts.length !== 7) {
|
|
1128
|
+
reject(
|
|
1129
|
+
"farm_instruction_mismatch",
|
|
1130
|
+
"Farm withdrawal account list is not canonical"
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
const expectedSharesAta = deriveAssociatedTokenAddress({
|
|
1134
|
+
owner: intent.wallet,
|
|
1135
|
+
mint: intent.shareMint
|
|
1136
|
+
});
|
|
1137
|
+
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) {
|
|
1138
|
+
reject(
|
|
1139
|
+
"farm_instruction_mismatch",
|
|
1140
|
+
"Farm withdrawal must return the approved vault shares to the agent wallet"
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function expectKvaultWithdraw(ixs, expectation) {
|
|
1145
|
+
const ix = ixs.shift();
|
|
1146
|
+
if (ix === void 0 || ix.programAddress !== KVAULT_PROGRAM_ID) {
|
|
1147
|
+
reject("withdraw_mismatch", "Expected KVault withdraw instruction");
|
|
1148
|
+
}
|
|
1149
|
+
validateKvaultWithdrawInstruction(ix, expectation);
|
|
1150
|
+
}
|
|
1151
|
+
function validateKvaultWithdrawInstruction(ix, expectation) {
|
|
1152
|
+
const isWithdraw = bytesStartWith(ix.data, KVAULT_WITHDRAW_DISCRIMINATOR);
|
|
1153
|
+
const isWithdrawFromAvailable = bytesStartWith(
|
|
1154
|
+
ix.data,
|
|
1155
|
+
KVAULT_WITHDRAW_FROM_AVAILABLE_DISCRIMINATOR
|
|
1156
|
+
);
|
|
1157
|
+
if (!isWithdraw && !isWithdrawFromAvailable) {
|
|
1158
|
+
reject("withdraw_mismatch", "Unexpected KVault instruction");
|
|
1159
|
+
}
|
|
1160
|
+
const sharesAmount = readU64LE(ix.data, 8);
|
|
1161
|
+
const fullExit = sharesAmount === U64_MAX;
|
|
1162
|
+
if (fullExit && !expectation.allowFullExit) {
|
|
1163
|
+
reject("withdraw_mismatch", "Full-exit share burn is not allowed for this intent");
|
|
1164
|
+
}
|
|
1165
|
+
if (!fullExit && sharesAmount > expectation.maxSharesToRedeemRaw) {
|
|
1166
|
+
reject(
|
|
1167
|
+
"shares_exceed_max",
|
|
1168
|
+
`Withdraw burns ${sharesAmount} shares which exceeds the approved maximum ${expectation.maxSharesToRedeemRaw}`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
if (ix.accounts[0] !== expectation.wallet) {
|
|
1172
|
+
reject("withdraw_mismatch", "Withdraw user is not the agent wallet");
|
|
1173
|
+
}
|
|
1174
|
+
if (ix.accounts[1] !== expectation.vault) {
|
|
1175
|
+
reject("withdraw_mismatch", "Withdraw vault mismatch");
|
|
1176
|
+
}
|
|
1177
|
+
if (ix.accounts[5] !== expectation.userTokenAccount) {
|
|
1178
|
+
reject(
|
|
1179
|
+
"withdraw_mismatch",
|
|
1180
|
+
"Withdraw token destination is not the approved account"
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
if (ix.accounts[6] !== expectation.asset) {
|
|
1184
|
+
reject("withdraw_mismatch", "Withdraw token mint mismatch");
|
|
1185
|
+
}
|
|
1186
|
+
const expectedSharesAta = deriveAssociatedTokenAddress({
|
|
1187
|
+
owner: expectation.wallet,
|
|
1188
|
+
mint: expectation.shareMint
|
|
1189
|
+
});
|
|
1190
|
+
if (ix.accounts[7] !== expectedSharesAta) {
|
|
1191
|
+
reject("withdraw_mismatch", "Withdraw share source must be the agent share ATA");
|
|
1192
|
+
}
|
|
1193
|
+
if (ix.accounts[8] !== expectation.shareMint) {
|
|
1194
|
+
reject("withdraw_mismatch", "Withdraw share mint mismatch");
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function expectTransferChecked(ixs, expectation) {
|
|
1198
|
+
const ix = ixs.shift();
|
|
1199
|
+
if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 12) {
|
|
1200
|
+
reject("transfer_mismatch", `Expected TransferChecked for ${expectation.label}`);
|
|
1201
|
+
}
|
|
1202
|
+
const amount = readU64LE(ix.data, 1);
|
|
1203
|
+
const decimals = ix.data[9];
|
|
1204
|
+
if (expectation.amount !== null && amount !== expectation.amount) {
|
|
1205
|
+
reject(
|
|
1206
|
+
"amount_mismatch",
|
|
1207
|
+
`${expectation.label} amount ${amount} does not match ${expectation.amount}`
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
if (decimals !== USDC_DECIMALS) {
|
|
1211
|
+
reject("transfer_mismatch", `${expectation.label} has unexpected decimals`);
|
|
1212
|
+
}
|
|
1213
|
+
if (ix.accounts[0] !== expectation.source) {
|
|
1214
|
+
reject("transfer_mismatch", `${expectation.label} source mismatch`);
|
|
1215
|
+
}
|
|
1216
|
+
if (ix.accounts[1] !== expectation.mint) {
|
|
1217
|
+
reject("transfer_mismatch", `${expectation.label} mint mismatch`);
|
|
1218
|
+
}
|
|
1219
|
+
if (ix.accounts[2] !== expectation.destination) {
|
|
1220
|
+
reject("transfer_mismatch", `${expectation.label} destination mismatch`);
|
|
1221
|
+
}
|
|
1222
|
+
if (ix.accounts[3] !== expectation.authority) {
|
|
1223
|
+
reject("transfer_mismatch", `${expectation.label} authority mismatch`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
function expectCloseAccount(ixs, expectation) {
|
|
1227
|
+
const ix = ixs.shift();
|
|
1228
|
+
if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 9) {
|
|
1229
|
+
reject("close_mismatch", "Expected CloseAccount instruction");
|
|
1230
|
+
}
|
|
1231
|
+
if (ix.accounts[0] !== expectation.account) {
|
|
1232
|
+
reject("close_mismatch", "CloseAccount target is not the temporary account");
|
|
1233
|
+
}
|
|
1234
|
+
if (ix.accounts[1] !== expectation.destination) {
|
|
1235
|
+
reject("close_mismatch", "CloseAccount rent destination must be the sponsor");
|
|
1236
|
+
}
|
|
1237
|
+
if (ix.accounts[2] !== expectation.owner) {
|
|
1238
|
+
reject("close_mismatch", "CloseAccount authority must be the agent wallet");
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
function expectMemo(ixs, memo) {
|
|
1242
|
+
const ix = ixs.shift();
|
|
1243
|
+
if (ix === void 0 || ix.programAddress !== MEMO_PROGRAM_ID) {
|
|
1244
|
+
reject("memo_mismatch", "Expected Memo instruction");
|
|
1245
|
+
}
|
|
1246
|
+
if (Buffer.from(ix.data).toString("utf8") !== memo) {
|
|
1247
|
+
reject("memo_mismatch", "Memo content does not match the paymentId");
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
function expectAtaCreateForOwner(ix, owner) {
|
|
1251
|
+
if (ix.accounts[2] !== owner) {
|
|
1252
|
+
reject(
|
|
1253
|
+
"unexpected_instruction",
|
|
1254
|
+
"Associated token account creation for a foreign owner"
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
function bytesStartWith(data, prefix) {
|
|
1259
|
+
if (data.length < prefix.length) {
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
return prefix.every((byte, index) => data[index] === byte);
|
|
1263
|
+
}
|
|
1264
|
+
function readU64LE(data, offset) {
|
|
1265
|
+
if (data.length < offset + 8) {
|
|
1266
|
+
reject("invalid_transaction_encoding", "Instruction data too short for u64");
|
|
1267
|
+
}
|
|
1268
|
+
return Buffer.from(data.subarray(offset, offset + 8)).readBigUInt64LE(0);
|
|
1269
|
+
}
|
|
1270
|
+
function readU32LE(data, offset) {
|
|
1271
|
+
if (data.length < offset + 4) {
|
|
1272
|
+
reject("invalid_transaction_encoding", "Instruction data too short for u32");
|
|
1273
|
+
}
|
|
1274
|
+
return Buffer.from(data.subarray(offset, offset + 4)).readUInt32LE(0);
|
|
1275
|
+
}
|
|
1276
|
+
function readU128LE(data, offset) {
|
|
1277
|
+
if (data.length < offset + 16) {
|
|
1278
|
+
reject("invalid_transaction_encoding", "Instruction data too short for u128");
|
|
1279
|
+
}
|
|
1280
|
+
let value = 0n;
|
|
1281
|
+
for (let index = 0; index < 16; index += 1) {
|
|
1282
|
+
value |= BigInt(data[offset + index]) << BigInt(index * 8);
|
|
1283
|
+
}
|
|
1284
|
+
return value;
|
|
1285
|
+
}
|
|
1286
|
+
function readShortVec(bytes, startOffset) {
|
|
1287
|
+
let value = 0;
|
|
1288
|
+
let shift = 0;
|
|
1289
|
+
let offset = startOffset;
|
|
1290
|
+
while (offset < bytes.length) {
|
|
1291
|
+
const byte = bytes[offset];
|
|
1292
|
+
value |= (byte & 127) << shift;
|
|
1293
|
+
offset += 1;
|
|
1294
|
+
if ((byte & 128) === 0) {
|
|
1295
|
+
return { value, nextOffset: offset };
|
|
1296
|
+
}
|
|
1297
|
+
shift += 7;
|
|
1298
|
+
if (shift > 28) {
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return null;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// ../../src/client/agent-wallet-signer.ts
|
|
1306
|
+
var IntentValidatingAgentWalletSigner = class {
|
|
1307
|
+
vault;
|
|
1308
|
+
validationMode = "structured_intent_transaction";
|
|
1309
|
+
validationPolicy;
|
|
1310
|
+
constructor(validationPolicy) {
|
|
1311
|
+
this.vault = Object.freeze({ ...validationPolicy?.vault ?? SUBLY_VAULT });
|
|
1312
|
+
this.validationPolicy = { ...validationPolicy, vault: this.vault };
|
|
1313
|
+
}
|
|
1314
|
+
async signPayment(params) {
|
|
1315
|
+
this.assertIntentWallet(params.intent.wallet);
|
|
1316
|
+
validatePaymentIntentTransaction({ ...params, ...this.policySpread() });
|
|
1317
|
+
return this.sign(params.serializedTransaction);
|
|
1318
|
+
}
|
|
1319
|
+
async signDeposit(params) {
|
|
1320
|
+
this.assertIntentWallet(params.intent.wallet);
|
|
1321
|
+
validateDepositIntentTransaction({ ...params, ...this.policySpread() });
|
|
1322
|
+
return this.sign(params.serializedTransaction);
|
|
1323
|
+
}
|
|
1324
|
+
async signWithdrawal(params) {
|
|
1325
|
+
this.assertIntentWallet(params.intent.wallet);
|
|
1326
|
+
validateWithdrawalIntentTransaction({ ...params, ...this.policySpread() });
|
|
1327
|
+
return this.sign(params.serializedTransaction);
|
|
1328
|
+
}
|
|
1329
|
+
policySpread() {
|
|
1330
|
+
return this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy };
|
|
1331
|
+
}
|
|
1332
|
+
assertIntentWallet(wallet) {
|
|
1333
|
+
if (wallet !== this.walletAddress) {
|
|
1334
|
+
throw new IntentValidationError(
|
|
1335
|
+
"wallet_mismatch",
|
|
1336
|
+
"Intent wallet does not match this signer's wallet"
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
var LocalKeypairAgentWalletSigner = class extends IntentValidatingAgentWalletSigner {
|
|
1342
|
+
provider = "local-keypair";
|
|
1343
|
+
keyPairSigner;
|
|
1344
|
+
constructor(keyPairSigner, validationPolicy) {
|
|
1345
|
+
super(validationPolicy);
|
|
1346
|
+
this.keyPairSigner = keyPairSigner;
|
|
1347
|
+
}
|
|
1348
|
+
get walletAddress() {
|
|
1349
|
+
return this.keyPairSigner.address;
|
|
1350
|
+
}
|
|
1351
|
+
async signApiMessage(message) {
|
|
1352
|
+
const signature = await signBytes(
|
|
1353
|
+
this.keyPairSigner.keyPair.privateKey,
|
|
1354
|
+
message
|
|
1355
|
+
);
|
|
1356
|
+
return bs586.encode(signature);
|
|
1357
|
+
}
|
|
1358
|
+
async sign(serializedTransaction) {
|
|
1359
|
+
const { serializedBase64, transaction } = await addSignaturesToSerializedTransaction({
|
|
1360
|
+
serializedBase64: serializedTransaction,
|
|
1361
|
+
signers: [this.keyPairSigner.keyPair]
|
|
1362
|
+
});
|
|
1363
|
+
const agentSignature = signatureBase58ForSigner(
|
|
1364
|
+
transaction,
|
|
1365
|
+
this.keyPairSigner.address
|
|
1366
|
+
);
|
|
1367
|
+
if (agentSignature === null) {
|
|
1368
|
+
throw new IntentValidationError(
|
|
1369
|
+
"signing_failed",
|
|
1370
|
+
"Agent signature was not produced"
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
return { serializedTransaction: serializedBase64, agentSignature };
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
var RemoteAgentWalletSigner = class extends IntentValidatingAgentWalletSigner {
|
|
1377
|
+
transport;
|
|
1378
|
+
publicKey;
|
|
1379
|
+
constructor(transport, validationPolicy) {
|
|
1380
|
+
super(validationPolicy);
|
|
1381
|
+
this.transport = transport;
|
|
1382
|
+
this.publicKey = ed25519PublicKeyBytes(
|
|
1383
|
+
transport.provider,
|
|
1384
|
+
transport.walletAddress
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
get walletAddress() {
|
|
1388
|
+
return this.transport.walletAddress;
|
|
1389
|
+
}
|
|
1390
|
+
get provider() {
|
|
1391
|
+
return this.transport.provider;
|
|
1392
|
+
}
|
|
1393
|
+
async signApiMessage(message) {
|
|
1394
|
+
const signature = await this.transport.signMessage(message);
|
|
1395
|
+
if (!nacl2.sign.detached.verify(message, signature, this.publicKey)) {
|
|
1396
|
+
throw new RemoteSigningError(
|
|
1397
|
+
this.transport.provider,
|
|
1398
|
+
"message signature did not verify for the agent wallet"
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
return bs586.encode(signature);
|
|
1402
|
+
}
|
|
1403
|
+
sign(serializedTransaction) {
|
|
1404
|
+
return externallySignedAgentTransaction({
|
|
1405
|
+
transport: this.transport,
|
|
1406
|
+
serializedTransaction
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
// ../../src/client/signer-transports/circle.ts
|
|
1412
|
+
import {
|
|
1413
|
+
constants,
|
|
1414
|
+
createPublicKey,
|
|
1415
|
+
publicEncrypt
|
|
1416
|
+
} from "node:crypto";
|
|
1417
|
+
var PROVIDER = "circle";
|
|
1418
|
+
var DEFAULT_BASE_URL = "https://api.circle.com";
|
|
1419
|
+
async function createCircleSignerTransport(config) {
|
|
1420
|
+
if (!/^[0-9a-fA-F]{64}$/.test(config.entitySecret)) {
|
|
1421
|
+
throw new RemoteSigningError(
|
|
1422
|
+
PROVIDER,
|
|
1423
|
+
"entity secret must be 32 bytes of hex (64 hex chars)"
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
1427
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
1428
|
+
const request = async (method, path, body) => {
|
|
1429
|
+
const json = await providerJsonRequest({
|
|
1430
|
+
provider: PROVIDER,
|
|
1431
|
+
fetchImpl,
|
|
1432
|
+
baseUrl,
|
|
1433
|
+
path,
|
|
1434
|
+
method,
|
|
1435
|
+
headers: { authorization: `Bearer ${config.apiKey}` },
|
|
1436
|
+
body
|
|
1437
|
+
});
|
|
1438
|
+
const data = json?.data;
|
|
1439
|
+
if (data === void 0) {
|
|
1440
|
+
throw new RemoteSigningError(
|
|
1441
|
+
PROVIDER,
|
|
1442
|
+
`${method} ${path} returned no data envelope`,
|
|
1443
|
+
json
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
return data;
|
|
1447
|
+
};
|
|
1448
|
+
const walletData = await request("GET", `/v1/w3s/wallets/${config.walletId}`);
|
|
1449
|
+
const wallet = walletData.wallet;
|
|
1450
|
+
if (wallet?.address === void 0) {
|
|
1451
|
+
throw new RemoteSigningError(
|
|
1452
|
+
PROVIDER,
|
|
1453
|
+
`wallet ${config.walletId} has no address`,
|
|
1454
|
+
walletData
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
if (wallet.blockchain !== "SOL") {
|
|
1458
|
+
throw new RemoteSigningError(
|
|
1459
|
+
PROVIDER,
|
|
1460
|
+
`wallet ${config.walletId} is on ${String(
|
|
1461
|
+
wallet.blockchain
|
|
1462
|
+
)}, expected SOL (Solana mainnet)`
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
const walletAddress = wallet.address;
|
|
1466
|
+
let entityPublicKey = null;
|
|
1467
|
+
const entitySecretCiphertext = async () => {
|
|
1468
|
+
if (entityPublicKey === null) {
|
|
1469
|
+
const data = await request("GET", "/v1/w3s/config/entity/publicKey");
|
|
1470
|
+
const publicKey2 = data.publicKey;
|
|
1471
|
+
if (typeof publicKey2 !== "string") {
|
|
1472
|
+
throw new RemoteSigningError(
|
|
1473
|
+
PROVIDER,
|
|
1474
|
+
"entity public key response has no publicKey",
|
|
1475
|
+
data
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
entityPublicKey = createPublicKey(publicKey2);
|
|
1479
|
+
}
|
|
1480
|
+
return publicEncrypt(
|
|
1481
|
+
{
|
|
1482
|
+
key: entityPublicKey,
|
|
1483
|
+
padding: constants.RSA_PKCS1_OAEP_PADDING,
|
|
1484
|
+
oaepHash: "sha256"
|
|
1485
|
+
},
|
|
1486
|
+
Buffer.from(config.entitySecret, "hex")
|
|
1487
|
+
).toString("base64");
|
|
1488
|
+
};
|
|
1489
|
+
return {
|
|
1490
|
+
provider: PROVIDER,
|
|
1491
|
+
walletAddress,
|
|
1492
|
+
async signMessage(message) {
|
|
1493
|
+
const data = await request("POST", "/v1/w3s/developer/sign/message", {
|
|
1494
|
+
walletId: config.walletId,
|
|
1495
|
+
message: `0x${Buffer.from(message).toString("hex")}`,
|
|
1496
|
+
encodedByHex: true,
|
|
1497
|
+
entitySecretCiphertext: await entitySecretCiphertext()
|
|
1498
|
+
});
|
|
1499
|
+
const signature = data.signature;
|
|
1500
|
+
if (typeof signature !== "string") {
|
|
1501
|
+
throw new RemoteSigningError(
|
|
1502
|
+
PROVIDER,
|
|
1503
|
+
"sign/message returned no signature",
|
|
1504
|
+
data
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
return verifiedEd25519Signature({
|
|
1508
|
+
provider: PROVIDER,
|
|
1509
|
+
encodedSignature: signature,
|
|
1510
|
+
message,
|
|
1511
|
+
walletAddress
|
|
1512
|
+
});
|
|
1513
|
+
},
|
|
1514
|
+
async signTransaction(serializedTransactionBase64) {
|
|
1515
|
+
const data = await request(
|
|
1516
|
+
"POST",
|
|
1517
|
+
"/v1/w3s/developer/sign/transaction",
|
|
1518
|
+
{
|
|
1519
|
+
walletId: config.walletId,
|
|
1520
|
+
rawTransaction: serializedTransactionBase64,
|
|
1521
|
+
entitySecretCiphertext: await entitySecretCiphertext()
|
|
1522
|
+
}
|
|
1523
|
+
);
|
|
1524
|
+
const signedTransaction = data.signedTransaction;
|
|
1525
|
+
if (typeof signedTransaction !== "string") {
|
|
1526
|
+
throw new RemoteSigningError(
|
|
1527
|
+
PROVIDER,
|
|
1528
|
+
"sign/transaction returned no signedTransaction",
|
|
1529
|
+
data
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
return signedTransaction;
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// ../../src/client/signer-transports/privy.ts
|
|
1538
|
+
import { createPrivateKey, createSign } from "node:crypto";
|
|
1539
|
+
var PROVIDER2 = "privy";
|
|
1540
|
+
var DEFAULT_BASE_URL2 = "https://api.privy.io";
|
|
1541
|
+
function canonicalJson(value) {
|
|
1542
|
+
if (Array.isArray(value)) {
|
|
1543
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
1544
|
+
}
|
|
1545
|
+
if (value !== null && typeof value === "object") {
|
|
1546
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1547
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
|
|
1548
|
+
}
|
|
1549
|
+
return JSON.stringify(value);
|
|
1550
|
+
}
|
|
1551
|
+
function parseAuthorizationKey(base64Pkcs8) {
|
|
1552
|
+
const stripped = base64Pkcs8.replace(/^wallet-auth:/, "").trim();
|
|
1553
|
+
try {
|
|
1554
|
+
return createPrivateKey({
|
|
1555
|
+
key: Buffer.from(stripped, "base64"),
|
|
1556
|
+
format: "der",
|
|
1557
|
+
type: "pkcs8"
|
|
1558
|
+
});
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
throw new RemoteSigningError(
|
|
1561
|
+
PROVIDER2,
|
|
1562
|
+
"authorization key is not a base64 PKCS#8 P-256 private key",
|
|
1563
|
+
error
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
function authorizationSignature(params) {
|
|
1568
|
+
const payload = {
|
|
1569
|
+
version: 1,
|
|
1570
|
+
method: params.method,
|
|
1571
|
+
url: params.url,
|
|
1572
|
+
body: params.body,
|
|
1573
|
+
headers: { "privy-app-id": params.appId }
|
|
1574
|
+
};
|
|
1575
|
+
const signer2 = createSign("sha256");
|
|
1576
|
+
signer2.update(canonicalJson(payload));
|
|
1577
|
+
return signer2.sign(params.key).toString("base64");
|
|
1578
|
+
}
|
|
1579
|
+
async function createPrivySignerTransport(config) {
|
|
1580
|
+
const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/$/, "");
|
|
1581
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
1582
|
+
const authorizationKey = config.authorizationPrivateKey === void 0 ? null : parseAuthorizationKey(config.authorizationPrivateKey);
|
|
1583
|
+
const baseHeaders = {
|
|
1584
|
+
authorization: `Basic ${Buffer.from(
|
|
1585
|
+
`${config.appId}:${config.appSecret}`
|
|
1586
|
+
).toString("base64")}`,
|
|
1587
|
+
"privy-app-id": config.appId
|
|
1588
|
+
};
|
|
1589
|
+
const request = async (method, path, body) => {
|
|
1590
|
+
const headers = authorizationKey !== null && method !== "GET" && body !== void 0 ? {
|
|
1591
|
+
...baseHeaders,
|
|
1592
|
+
"privy-authorization-signature": authorizationSignature({
|
|
1593
|
+
key: authorizationKey,
|
|
1594
|
+
appId: config.appId,
|
|
1595
|
+
method,
|
|
1596
|
+
url: `${baseUrl}${path}`,
|
|
1597
|
+
body
|
|
1598
|
+
})
|
|
1599
|
+
} : baseHeaders;
|
|
1600
|
+
const json = await providerJsonRequest({
|
|
1601
|
+
provider: PROVIDER2,
|
|
1602
|
+
fetchImpl,
|
|
1603
|
+
baseUrl,
|
|
1604
|
+
path,
|
|
1605
|
+
method,
|
|
1606
|
+
headers,
|
|
1607
|
+
body
|
|
1608
|
+
});
|
|
1609
|
+
if (json === null || typeof json !== "object") {
|
|
1610
|
+
throw new RemoteSigningError(
|
|
1611
|
+
PROVIDER2,
|
|
1612
|
+
`${method} ${path} returned a non-JSON body`
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
return json;
|
|
1616
|
+
};
|
|
1617
|
+
const rpc = async (body) => {
|
|
1618
|
+
const response = await request(
|
|
1619
|
+
"POST",
|
|
1620
|
+
`/v1/wallets/${config.walletId}/rpc`,
|
|
1621
|
+
body
|
|
1622
|
+
);
|
|
1623
|
+
const data = response.data;
|
|
1624
|
+
if (data === null || typeof data !== "object") {
|
|
1625
|
+
throw new RemoteSigningError(PROVIDER2, "rpc returned no data", response);
|
|
1626
|
+
}
|
|
1627
|
+
return data;
|
|
1628
|
+
};
|
|
1629
|
+
const wallet = await request("GET", `/v1/wallets/${config.walletId}`);
|
|
1630
|
+
const walletAddress = wallet.address;
|
|
1631
|
+
if (typeof walletAddress !== "string") {
|
|
1632
|
+
throw new RemoteSigningError(
|
|
1633
|
+
PROVIDER2,
|
|
1634
|
+
`wallet ${config.walletId} has no address`,
|
|
1635
|
+
wallet
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
if (wallet.chain_type !== "solana") {
|
|
1639
|
+
throw new RemoteSigningError(
|
|
1640
|
+
PROVIDER2,
|
|
1641
|
+
`wallet ${config.walletId} is ${String(
|
|
1642
|
+
wallet.chain_type
|
|
1643
|
+
)}, expected solana`
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
return {
|
|
1647
|
+
provider: PROVIDER2,
|
|
1648
|
+
walletAddress,
|
|
1649
|
+
async signMessage(message) {
|
|
1650
|
+
const data = await rpc({
|
|
1651
|
+
chain_type: "solana",
|
|
1652
|
+
method: "signMessage",
|
|
1653
|
+
params: {
|
|
1654
|
+
message: Buffer.from(message).toString("base64"),
|
|
1655
|
+
encoding: "base64"
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
const signature = data.signature;
|
|
1659
|
+
if (typeof signature !== "string") {
|
|
1660
|
+
throw new RemoteSigningError(
|
|
1661
|
+
PROVIDER2,
|
|
1662
|
+
"signMessage returned no signature",
|
|
1663
|
+
data
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
return verifiedEd25519Signature({
|
|
1667
|
+
provider: PROVIDER2,
|
|
1668
|
+
encodedSignature: signature,
|
|
1669
|
+
message,
|
|
1670
|
+
walletAddress
|
|
1671
|
+
});
|
|
1672
|
+
},
|
|
1673
|
+
async signTransaction(serializedTransactionBase64) {
|
|
1674
|
+
const data = await rpc({
|
|
1675
|
+
chain_type: "solana",
|
|
1676
|
+
method: "signTransaction",
|
|
1677
|
+
params: {
|
|
1678
|
+
transaction: serializedTransactionBase64,
|
|
1679
|
+
encoding: "base64"
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
const signedTransaction = data.signed_transaction;
|
|
1683
|
+
if (typeof signedTransaction !== "string") {
|
|
1684
|
+
throw new RemoteSigningError(
|
|
1685
|
+
PROVIDER2,
|
|
1686
|
+
"signTransaction returned no signed_transaction",
|
|
1687
|
+
data
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
return signedTransaction;
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// ../../src/client/signer-env.ts
|
|
1696
|
+
async function agentWalletSignerFromEnv(env = process.env) {
|
|
1697
|
+
const nonEmpty = (value) => {
|
|
1698
|
+
const trimmed = value?.trim();
|
|
1699
|
+
return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
|
|
1700
|
+
};
|
|
1701
|
+
const provider = nonEmpty(env.SUBLY_SIGNER_PROVIDER)?.toLowerCase() ?? "local";
|
|
1702
|
+
const pickVar = (name) => nonEmpty(env[`SUBLY_${name}`]) ?? nonEmpty(env[name]);
|
|
1703
|
+
const requireVar = (name) => {
|
|
1704
|
+
const value = pickVar(name);
|
|
1705
|
+
if (value === void 0) {
|
|
1706
|
+
throw new Error(
|
|
1707
|
+
`${name} (or SUBLY_${name}) is required for SUBLY_SIGNER_PROVIDER=${provider}`
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
return value;
|
|
1711
|
+
};
|
|
1712
|
+
if (provider === "local") {
|
|
1713
|
+
const localSecretKey = loadSecretKeyBytes({
|
|
1714
|
+
base58Secret: env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
1715
|
+
jsonFilePath: env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
1716
|
+
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
1717
|
+
});
|
|
1718
|
+
return {
|
|
1719
|
+
provider,
|
|
1720
|
+
signer: new LocalKeypairAgentWalletSigner(
|
|
1721
|
+
await createKeyPairSignerFromBytes2(localSecretKey)
|
|
1722
|
+
),
|
|
1723
|
+
localSecretKey
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
if (provider === "circle") {
|
|
1727
|
+
const transport = await createCircleSignerTransport({
|
|
1728
|
+
apiKey: requireVar("CIRCLE_API_KEY"),
|
|
1729
|
+
entitySecret: requireVar("CIRCLE_ENTITY_SECRET"),
|
|
1730
|
+
walletId: requireVar("CIRCLE_WALLET_ID"),
|
|
1731
|
+
baseUrl: pickVar("CIRCLE_BASE_URL")
|
|
1732
|
+
});
|
|
1733
|
+
return { provider, signer: new RemoteAgentWalletSigner(transport), transport };
|
|
1734
|
+
}
|
|
1735
|
+
if (provider === "privy") {
|
|
1736
|
+
const transport = await createPrivySignerTransport({
|
|
1737
|
+
appId: requireVar("PRIVY_APP_ID"),
|
|
1738
|
+
appSecret: requireVar("PRIVY_APP_SECRET"),
|
|
1739
|
+
walletId: requireVar("PRIVY_WALLET_ID"),
|
|
1740
|
+
authorizationPrivateKey: pickVar("PRIVY_AUTHORIZATION_KEY"),
|
|
1741
|
+
baseUrl: pickVar("PRIVY_BASE_URL")
|
|
1742
|
+
});
|
|
1743
|
+
return { provider, signer: new RemoteAgentWalletSigner(transport), transport };
|
|
1744
|
+
}
|
|
1745
|
+
throw new Error(
|
|
1746
|
+
`unknown SUBLY_SIGNER_PROVIDER "${provider}" (expected local, circle, or privy)`
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// ../../src/client/withdrawal-preview.ts
|
|
1751
|
+
var ROUNDING_RAW_USDC = 10n;
|
|
1752
|
+
async function assertWithdrawalPreview(input) {
|
|
1753
|
+
const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
|
|
1754
|
+
const simulation = await input.rpc.simulateTransaction(
|
|
1755
|
+
input.serializedTransaction,
|
|
1756
|
+
{
|
|
1757
|
+
encoding: "base64",
|
|
1758
|
+
commitment: "confirmed",
|
|
1759
|
+
sigVerify: false,
|
|
1760
|
+
replaceRecentBlockhash: false,
|
|
1761
|
+
innerInstructions: true
|
|
1762
|
+
}
|
|
1763
|
+
).send({ abortSignal: AbortSignal.timeout(15e3) });
|
|
1764
|
+
if (simulation.value.err !== null) {
|
|
1765
|
+
throw new Error("Withdrawal preview failed on the client RPC; no transaction was signed. Check liquidity, RPC and blockhash, then prepare again.");
|
|
1766
|
+
}
|
|
1767
|
+
let received = 0n;
|
|
1768
|
+
for (const group of simulation.value.innerInstructions ?? []) {
|
|
1769
|
+
for (const instruction of group.instructions) {
|
|
1770
|
+
if (!("parsed" in instruction) || instruction.programId !== SPL_TOKEN_PROGRAM_ID) continue;
|
|
1771
|
+
const parsed = instruction.parsed;
|
|
1772
|
+
if (parsed.type !== "transfer" && parsed.type !== "transferChecked") continue;
|
|
1773
|
+
const info = parsed.info;
|
|
1774
|
+
if (!info || info.destination !== destination && info.source !== destination) continue;
|
|
1775
|
+
const raw = parsed.type === "transferChecked" ? info.tokenAmount?.amount : info.amount;
|
|
1776
|
+
if (typeof raw !== "string" || !/^\d+$/.test(raw)) {
|
|
1777
|
+
throw new Error("Withdrawal preview returned an invalid token amount");
|
|
1778
|
+
}
|
|
1779
|
+
const amount = BigInt(raw);
|
|
1780
|
+
if (info.destination === destination) received += amount;
|
|
1781
|
+
if (info.source === destination) received -= amount;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (received <= 0n || received > input.amountRawUsdc + ROUNDING_RAW_USDC || received < input.amountRawUsdc - ROUNDING_RAW_USDC) {
|
|
1785
|
+
throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// ../../src/client/lookup-tables.ts
|
|
1790
|
+
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
|
|
1791
|
+
import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
|
|
1792
|
+
function lookupTableAddressesForTransaction(serializedTransaction) {
|
|
1793
|
+
const wire = Buffer.from(serializedTransaction, "base64");
|
|
1794
|
+
let offset = 0;
|
|
1795
|
+
let signatureCount = 0;
|
|
1796
|
+
let shift = 0;
|
|
1797
|
+
while (offset < wire.length) {
|
|
1798
|
+
const byte = wire[offset];
|
|
1799
|
+
signatureCount |= (byte & 127) << shift;
|
|
1800
|
+
offset += 1;
|
|
1801
|
+
if ((byte & 128) === 0) {
|
|
1802
|
+
break;
|
|
1803
|
+
}
|
|
1804
|
+
shift += 7;
|
|
1805
|
+
}
|
|
1806
|
+
const messageBytes = wire.subarray(offset + signatureCount * 64);
|
|
1807
|
+
const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
|
|
1808
|
+
const lookups = compiled.addressTableLookups ?? [];
|
|
1809
|
+
return lookups.map((lookup) => String(lookup.lookupTableAddress));
|
|
1810
|
+
}
|
|
1811
|
+
async function fetchLookupTablesForTransaction(rpc, serializedTransaction) {
|
|
1812
|
+
const addresses = lookupTableAddressesForTransaction(serializedTransaction);
|
|
1813
|
+
if (addresses.length === 0) {
|
|
1814
|
+
return {};
|
|
1815
|
+
}
|
|
1816
|
+
const tables = await fetchAllMaybeAddressLookupTable(
|
|
1817
|
+
rpc,
|
|
1818
|
+
addresses.map((value) => address(value))
|
|
1819
|
+
);
|
|
1820
|
+
const result = {};
|
|
1821
|
+
for (const table of tables) {
|
|
1822
|
+
if (table.exists) {
|
|
1823
|
+
result[table.address] = table.data.addresses.map(String);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
return result;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// ../../src/api/wallet-auth.ts
|
|
1830
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1831
|
+
import bs587 from "bs58";
|
|
1832
|
+
import nacl3 from "tweetnacl";
|
|
1833
|
+
var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
|
|
1834
|
+
var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
|
|
1835
|
+
var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
|
|
1836
|
+
function sha256Hex(data) {
|
|
1837
|
+
return createHash3("sha256").update(data, "utf8").digest("hex");
|
|
1838
|
+
}
|
|
1839
|
+
function walletAuthMessage(params) {
|
|
1840
|
+
return new TextEncoder().encode(
|
|
1841
|
+
`subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
|
|
1842
|
+
params.rawBody
|
|
1843
|
+
)}:${params.signedAtMs}`
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// ../../src/client/wallet-auth-headers.ts
|
|
1848
|
+
async function walletAuthHeaders(params) {
|
|
1849
|
+
const signedAtMs = String(Date.now());
|
|
1850
|
+
const message = walletAuthMessage({
|
|
1851
|
+
method: params.method,
|
|
1852
|
+
path: (() => {
|
|
1853
|
+
const url = new URL(params.url);
|
|
1854
|
+
return url.pathname + url.search;
|
|
1855
|
+
})(),
|
|
1856
|
+
rawBody: params.body ?? "",
|
|
1857
|
+
signedAtMs
|
|
1858
|
+
});
|
|
1859
|
+
return {
|
|
1860
|
+
[WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
|
|
1861
|
+
[WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
|
|
1862
|
+
[WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// ../../src/client/vault-flows.ts
|
|
1867
|
+
var VaultFlowClientError = class extends Error {
|
|
1868
|
+
constructor(step, message, detail = null, code = null, errorDetails = null) {
|
|
1869
|
+
super(message);
|
|
1870
|
+
this.step = step;
|
|
1871
|
+
this.detail = detail;
|
|
1872
|
+
this.code = code;
|
|
1873
|
+
this.errorDetails = errorDetails;
|
|
1874
|
+
this.name = "VaultFlowClientError";
|
|
1875
|
+
}
|
|
1876
|
+
step;
|
|
1877
|
+
detail;
|
|
1878
|
+
code;
|
|
1879
|
+
errorDetails;
|
|
1880
|
+
};
|
|
1881
|
+
var VaultFlowClient = class {
|
|
1882
|
+
vault;
|
|
1883
|
+
rpc;
|
|
1884
|
+
baseUrl;
|
|
1885
|
+
signer;
|
|
1886
|
+
fetchImpl;
|
|
1887
|
+
lookupTablesFor;
|
|
1888
|
+
pollTimeoutMs;
|
|
1889
|
+
pollIntervalMs;
|
|
1890
|
+
constructor(config) {
|
|
1891
|
+
this.rpc = config.rpc;
|
|
1892
|
+
this.vault = config.vault ?? config.signer.vault ?? SUBLY_VAULT;
|
|
1893
|
+
if (config.signer.vault && config.signer.vault.address !== this.vault.address) {
|
|
1894
|
+
throw new Error("Vault flow client and signer must select the same vault");
|
|
1895
|
+
}
|
|
1896
|
+
this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
|
|
1897
|
+
this.signer = config.signer;
|
|
1898
|
+
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1899
|
+
this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
|
|
1900
|
+
this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
|
|
1901
|
+
this.pollIntervalMs = config.pollIntervalMs ?? 2500;
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Moves USDC from the agent wallet into the vault (fee sponsored). Under
|
|
1905
|
+
* depositPolicy "owner_approval_required" the relayer refuses to prepare
|
|
1906
|
+
* without an owner approval; when the caller passes none, an already
|
|
1907
|
+
* APPROVED deposit approval for this exact amount (e.g. the mandate's
|
|
1908
|
+
* initialDeposit — "one Face ID covers mandate + first deposit") is looked
|
|
1909
|
+
* up and used automatically before surfacing deposit_approval_required.
|
|
1910
|
+
*/
|
|
1911
|
+
async deposit(input) {
|
|
1912
|
+
let approvalId = input.approvalId;
|
|
1913
|
+
let prepared;
|
|
1914
|
+
try {
|
|
1915
|
+
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
1916
|
+
wallet: this.signer.walletAddress,
|
|
1917
|
+
vault: this.vault.address,
|
|
1918
|
+
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1919
|
+
...approvalId === void 0 ? {} : { approvalId }
|
|
1920
|
+
});
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId !== void 0) {
|
|
1923
|
+
throw error;
|
|
1924
|
+
}
|
|
1925
|
+
approvalId = await this.findApprovedDepositApproval(input.amountRawUsdc);
|
|
1926
|
+
if (approvalId === void 0) {
|
|
1927
|
+
throw error;
|
|
1928
|
+
}
|
|
1929
|
+
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
1930
|
+
wallet: this.signer.walletAddress,
|
|
1931
|
+
vault: this.vault.address,
|
|
1932
|
+
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1933
|
+
approvalId
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.signingIntent.amountRawUsdc !== input.amountRawUsdc.toString()) {
|
|
1937
|
+
throw new VaultFlowClientError("prepare", "Prepared deposit differs from the requested wallet, vault or amount");
|
|
1938
|
+
}
|
|
1939
|
+
const signed = await this.signer.signDeposit({
|
|
1940
|
+
intent: prepared.signingIntent,
|
|
1941
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
1942
|
+
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
1943
|
+
});
|
|
1944
|
+
let outcome = await this.postJson("submit", "/v1/deposits/submit", {
|
|
1945
|
+
depositId: prepared.depositId,
|
|
1946
|
+
serializedTransaction: signed.serializedTransaction,
|
|
1947
|
+
agentSignature: signed.agentSignature
|
|
1948
|
+
});
|
|
1949
|
+
if (outcome.status === "submitted") {
|
|
1950
|
+
outcome = await this.pollUntilTerminal(
|
|
1951
|
+
`/v1/deposits/${prepared.depositId}`,
|
|
1952
|
+
outcome
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
return {
|
|
1956
|
+
depositId: prepared.depositId,
|
|
1957
|
+
status: outcome.status,
|
|
1958
|
+
txSignature: outcome.txSignature ?? null,
|
|
1959
|
+
actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
|
|
1960
|
+
sharesMintedRaw: outcome.sharesMintedRaw ?? null,
|
|
1961
|
+
errorCode: outcome.errorCode ?? null
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Moves USDC from the vault back to the agent wallet's USDC ATA (fee
|
|
1966
|
+
* sponsored). A plain withdrawal is the exit path and MAY spend principal;
|
|
1967
|
+
* with purpose "yield_realize" the relayer refuses anything beyond the
|
|
1968
|
+
* spendable yield (the payment path, via RelayerYieldRealizer).
|
|
1969
|
+
*/
|
|
1970
|
+
async withdraw(input) {
|
|
1971
|
+
const prepared = await this.postJson(
|
|
1972
|
+
"prepare",
|
|
1973
|
+
"/v1/withdrawals/prepare",
|
|
1974
|
+
{
|
|
1975
|
+
wallet: this.signer.walletAddress,
|
|
1976
|
+
vault: this.vault.address,
|
|
1977
|
+
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1978
|
+
...input.purpose === void 0 ? {} : { purpose: input.purpose },
|
|
1979
|
+
...input.payment === void 0 ? {} : { payment: input.payment },
|
|
1980
|
+
...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
|
|
1981
|
+
}
|
|
1982
|
+
);
|
|
1983
|
+
if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.requestedWithdrawRawUsdc !== input.amountRawUsdc.toString() || prepared.purpose !== (input.purpose ?? "normal") || input.purpose === "yield_realize" && prepared.signingIntent.allowFullExit) {
|
|
1984
|
+
throw new VaultFlowClientError("prepare", "Prepared withdrawal differs from the requested operation");
|
|
1985
|
+
}
|
|
1986
|
+
await assertWithdrawalPreview({
|
|
1987
|
+
rpc: this.rpc,
|
|
1988
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
1989
|
+
wallet: this.signer.walletAddress,
|
|
1990
|
+
vault: this.vault,
|
|
1991
|
+
amountRawUsdc: input.amountRawUsdc
|
|
1992
|
+
});
|
|
1993
|
+
const signed = await this.signer.signWithdrawal({
|
|
1994
|
+
intent: prepared.signingIntent,
|
|
1995
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
1996
|
+
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
1997
|
+
});
|
|
1998
|
+
let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
|
|
1999
|
+
withdrawalId: prepared.withdrawalId,
|
|
2000
|
+
serializedTransaction: signed.serializedTransaction,
|
|
2001
|
+
agentSignature: signed.agentSignature
|
|
2002
|
+
});
|
|
2003
|
+
if (outcome.status === "submitted") {
|
|
2004
|
+
outcome = await this.pollUntilTerminal(
|
|
2005
|
+
`/v1/withdrawals/${prepared.withdrawalId}`,
|
|
2006
|
+
outcome
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
return {
|
|
2010
|
+
withdrawalId: prepared.withdrawalId,
|
|
2011
|
+
status: outcome.status,
|
|
2012
|
+
txSignature: outcome.txSignature ?? null,
|
|
2013
|
+
destinationUsdcAta: prepared.destinationUsdcAta,
|
|
2014
|
+
actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
|
|
2015
|
+
actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
|
|
2016
|
+
errorCode: outcome.errorCode ?? null
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Reads the yield budget. Syncs the relayer's ledger from chain first (so
|
|
2021
|
+
* yield accrued since the last sync shows up); the sync is best-effort and
|
|
2022
|
+
* on failure the last-synced view is returned.
|
|
2023
|
+
*/
|
|
2024
|
+
async getBudget(options = {}) {
|
|
2025
|
+
if (options.refreshFromChain !== false) {
|
|
2026
|
+
try {
|
|
2027
|
+
await this.postJson(
|
|
2028
|
+
"sync",
|
|
2029
|
+
`/v1/wallets/${this.signer.walletAddress}/sync`,
|
|
2030
|
+
{ source: "chain", vault: this.vault.address }
|
|
2031
|
+
);
|
|
2032
|
+
} catch {
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget?vault=${this.vault.address}`;
|
|
2036
|
+
const response = await this.fetchImpl(url, {
|
|
2037
|
+
headers: await walletAuthHeaders({
|
|
2038
|
+
signer: this.signer,
|
|
2039
|
+
method: "GET",
|
|
2040
|
+
url
|
|
2041
|
+
})
|
|
2042
|
+
});
|
|
2043
|
+
const text = await response.text();
|
|
2044
|
+
if (response.status !== 200) {
|
|
2045
|
+
throw new VaultFlowClientError(
|
|
2046
|
+
"budget",
|
|
2047
|
+
`budget endpoint returned ${response.status}: ${text}`
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
let parsed;
|
|
2051
|
+
try {
|
|
2052
|
+
parsed = JSON.parse(text);
|
|
2053
|
+
} catch {
|
|
2054
|
+
throw new VaultFlowClientError(
|
|
2055
|
+
"budget",
|
|
2056
|
+
"budget endpoint returned 200 with a non-JSON body",
|
|
2057
|
+
text
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
const body = parsed;
|
|
2061
|
+
if (body.position?.vault !== void 0 && body.position.vault !== this.vault.address) {
|
|
2062
|
+
throw new VaultFlowClientError("budget", "Relayer returned the budget for a different vault");
|
|
2063
|
+
}
|
|
2064
|
+
return {
|
|
2065
|
+
wallet: this.signer.walletAddress,
|
|
2066
|
+
vault: this.vault.address,
|
|
2067
|
+
principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
|
|
2068
|
+
positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
|
|
2069
|
+
grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
|
|
2070
|
+
spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
/** Best-effort audit link: reports the x402 payment tx a realize funded. */
|
|
2074
|
+
async reportPayment(input) {
|
|
2075
|
+
await this.postJson("submit", "/v1/payments/report", {
|
|
2076
|
+
wallet: this.signer.walletAddress,
|
|
2077
|
+
withdrawalId: input.withdrawalId,
|
|
2078
|
+
paymentTxSignature: input.paymentTxSignature
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
/** Wallet's approvals as the relayer sees them (optionally by status). */
|
|
2082
|
+
async listApprovals(status) {
|
|
2083
|
+
const body = await this.getJson(
|
|
2084
|
+
`/v1/wallets/${this.signer.walletAddress}/approvals${`?vault=${this.vault.address}${status === void 0 ? "" : `&status=${encodeURIComponent(status)}`}`}`
|
|
2085
|
+
);
|
|
2086
|
+
return body.approvals ?? [];
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Creates the owner-onboarding setup link (wallet-auth pins the agreed
|
|
2090
|
+
* policy + initial deposit). Paste `setupUrl` into the chat verbatim.
|
|
2091
|
+
*/
|
|
2092
|
+
async createSetupSession(input) {
|
|
2093
|
+
const session = await this.postJson(
|
|
2094
|
+
"prepare",
|
|
2095
|
+
`/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
|
|
2096
|
+
{
|
|
2097
|
+
vault: this.vault.address,
|
|
2098
|
+
...input.policy === void 0 ? {} : { policy: input.policy },
|
|
2099
|
+
...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
|
|
2100
|
+
...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
|
|
2101
|
+
...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
|
|
2102
|
+
}
|
|
2103
|
+
);
|
|
2104
|
+
if (session.vault !== this.vault.address || session.wallet !== this.signer.walletAddress) {
|
|
2105
|
+
throw new VaultFlowClientError("prepare", "Relayer returned a setup session for a different wallet or vault");
|
|
2106
|
+
}
|
|
2107
|
+
return session;
|
|
2108
|
+
}
|
|
2109
|
+
/** Polls a setup session (public capability URL — no auth needed). */
|
|
2110
|
+
async getSetupSession(sessionId) {
|
|
2111
|
+
const url = `${this.baseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`;
|
|
2112
|
+
const response = await this.fetchImpl(url);
|
|
2113
|
+
const text = await response.text();
|
|
2114
|
+
if (response.status !== 200) {
|
|
2115
|
+
const parsed = parseRelayerError(text);
|
|
2116
|
+
throw new VaultFlowClientError(
|
|
2117
|
+
"read",
|
|
2118
|
+
parsed.message ?? `setup session read failed with ${response.status}`,
|
|
2119
|
+
text,
|
|
2120
|
+
parsed.code,
|
|
2121
|
+
parsed.details
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2124
|
+
return JSON.parse(text);
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Finds an APPROVED, unconsumed deposit approval bound to exactly this
|
|
2128
|
+
* amount — the shape the mandate's initialDeposit approval has.
|
|
2129
|
+
*/
|
|
2130
|
+
async findApprovedDepositApproval(amountRawUsdc) {
|
|
2131
|
+
try {
|
|
2132
|
+
const approvals = await this.listApprovals("approved");
|
|
2133
|
+
const match = approvals.find((approval) => {
|
|
2134
|
+
const binding = approval.binding;
|
|
2135
|
+
return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc.toString();
|
|
2136
|
+
});
|
|
2137
|
+
return match?.approvalId;
|
|
2138
|
+
} catch {
|
|
2139
|
+
return void 0;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Polls the reconciling GET endpoint until the intent leaves "submitted"
|
|
2144
|
+
* (each read looks the tx up on-chain) or the timeout elapses.
|
|
2145
|
+
*/
|
|
2146
|
+
async pollUntilTerminal(path, last) {
|
|
2147
|
+
const deadline = Date.now() + this.pollTimeoutMs;
|
|
2148
|
+
let latest = last;
|
|
2149
|
+
while (Date.now() < deadline) {
|
|
2150
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
2151
|
+
const url = `${this.baseUrl}${path}`;
|
|
2152
|
+
const response = await this.fetchImpl(url, {
|
|
2153
|
+
headers: await walletAuthHeaders({
|
|
2154
|
+
signer: this.signer,
|
|
2155
|
+
method: "GET",
|
|
2156
|
+
url
|
|
2157
|
+
})
|
|
2158
|
+
});
|
|
2159
|
+
if (response.status !== 200) {
|
|
2160
|
+
continue;
|
|
2161
|
+
}
|
|
2162
|
+
try {
|
|
2163
|
+
latest = await response.json();
|
|
2164
|
+
} catch {
|
|
2165
|
+
continue;
|
|
2166
|
+
}
|
|
2167
|
+
if (latest.status !== "submitted") {
|
|
2168
|
+
return latest;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
return latest;
|
|
2172
|
+
}
|
|
2173
|
+
async postJson(step, path, body) {
|
|
2174
|
+
const url = `${this.baseUrl}${path}`;
|
|
2175
|
+
const serialized = JSON.stringify(body);
|
|
2176
|
+
const response = await this.fetchImpl(url, {
|
|
2177
|
+
method: "POST",
|
|
2178
|
+
headers: {
|
|
2179
|
+
...await walletAuthHeaders({
|
|
2180
|
+
signer: this.signer,
|
|
2181
|
+
method: "POST",
|
|
2182
|
+
url,
|
|
2183
|
+
body: serialized
|
|
2184
|
+
}),
|
|
2185
|
+
"content-type": "application/json"
|
|
2186
|
+
},
|
|
2187
|
+
body: serialized
|
|
2188
|
+
});
|
|
2189
|
+
const text = await response.text();
|
|
2190
|
+
if (response.status !== 200) {
|
|
2191
|
+
const parsed = parseRelayerError(text);
|
|
2192
|
+
throw new VaultFlowClientError(
|
|
2193
|
+
step,
|
|
2194
|
+
parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
|
|
2195
|
+
text,
|
|
2196
|
+
parsed.code,
|
|
2197
|
+
parsed.details
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
try {
|
|
2201
|
+
return JSON.parse(text);
|
|
2202
|
+
} catch {
|
|
2203
|
+
throw new VaultFlowClientError(
|
|
2204
|
+
step,
|
|
2205
|
+
`${path} returned 200 with a non-JSON body`,
|
|
2206
|
+
text
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
async getJson(path) {
|
|
2211
|
+
const url = `${this.baseUrl}${path}`;
|
|
2212
|
+
const response = await this.fetchImpl(url, {
|
|
2213
|
+
headers: await walletAuthHeaders({
|
|
2214
|
+
signer: this.signer,
|
|
2215
|
+
method: "GET",
|
|
2216
|
+
url
|
|
2217
|
+
})
|
|
2218
|
+
});
|
|
2219
|
+
const text = await response.text();
|
|
2220
|
+
if (response.status !== 200) {
|
|
2221
|
+
const parsed = parseRelayerError(text);
|
|
2222
|
+
throw new VaultFlowClientError(
|
|
2223
|
+
"read",
|
|
2224
|
+
parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
|
|
2225
|
+
text,
|
|
2226
|
+
parsed.code,
|
|
2227
|
+
parsed.details
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
try {
|
|
2231
|
+
return JSON.parse(text);
|
|
2232
|
+
} catch {
|
|
2233
|
+
throw new VaultFlowClientError(
|
|
2234
|
+
"read",
|
|
2235
|
+
`${path} returned 200 with a non-JSON body`,
|
|
2236
|
+
text
|
|
2237
|
+
);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
function parseRelayerError(text) {
|
|
2242
|
+
try {
|
|
2243
|
+
const parsed = JSON.parse(text);
|
|
2244
|
+
return {
|
|
2245
|
+
code: typeof parsed.error?.code === "string" ? parsed.error.code : null,
|
|
2246
|
+
message: typeof parsed.error?.message === "string" ? parsed.error.message : null,
|
|
2247
|
+
details: parsed.error?.details ?? null
|
|
2248
|
+
};
|
|
2249
|
+
} catch {
|
|
2250
|
+
return { code: null, message: null, details: null };
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// ../../src/solana/rpc.ts
|
|
2255
|
+
import { createSolanaRpc } from "@solana/kit";
|
|
2256
|
+
function createRpc(url) {
|
|
2257
|
+
return createSolanaRpc(url);
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
// src/budget.ts
|
|
2261
|
+
var { signer } = await agentWalletSignerFromEnv();
|
|
2262
|
+
var client = new VaultFlowClient({
|
|
2263
|
+
relayerBaseUrl: process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com",
|
|
2264
|
+
signer,
|
|
2265
|
+
rpc: createRpc(process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com")
|
|
2266
|
+
});
|
|
2267
|
+
console.log(JSON.stringify(await client.getBudget(), null, 2));
|