@subly_fi/pay 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +98 -61
- package/dist/budget.js +2267 -0
- package/dist/cli.js +37 -20
- package/dist/deposit.js +1031 -183
- package/dist/doctor.js +188 -0
- package/dist/mcp-server.js +3404 -2415
- package/dist/pay.js +2764 -1873
- package/dist/setup-link.js +950 -104
- package/dist/vaults.js +129 -0
- package/dist/withdraw.js +950 -104
- package/package.json +25 -6
package/dist/withdraw.js
CHANGED
|
@@ -1,9 +1,176 @@
|
|
|
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
|
+
|
|
1
167
|
// ../../src/client/agent-wallet-signer.ts
|
|
2
168
|
import { signBytes } from "@solana/kit";
|
|
3
|
-
import
|
|
169
|
+
import bs586 from "bs58";
|
|
170
|
+
import nacl2 from "tweetnacl";
|
|
4
171
|
|
|
5
172
|
// ../../src/solana/tx.ts
|
|
6
|
-
import
|
|
173
|
+
import bs582 from "bs58";
|
|
7
174
|
import {
|
|
8
175
|
appendTransactionMessageInstructions,
|
|
9
176
|
compileTransaction,
|
|
@@ -46,6 +213,24 @@ function hashStableJson(value) {
|
|
|
46
213
|
function decodeSerializedTransaction(serializedBase64) {
|
|
47
214
|
return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
|
|
48
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
|
+
}
|
|
49
234
|
async function addSignaturesToSerializedTransaction(params) {
|
|
50
235
|
const decoded = decodeSerializedTransaction(params.serializedBase64);
|
|
51
236
|
const signed = await partiallySignTransaction(params.signers, decoded);
|
|
@@ -59,28 +244,149 @@ function signatureBase58ForSigner(transaction, signer2) {
|
|
|
59
244
|
if (signature === null || signature === void 0) {
|
|
60
245
|
return null;
|
|
61
246
|
}
|
|
62
|
-
return
|
|
247
|
+
return bs582.encode(signature);
|
|
63
248
|
}
|
|
64
249
|
|
|
65
|
-
// ../../src/client/
|
|
250
|
+
// ../../src/client/remote-signer-transport.ts
|
|
66
251
|
import bs583 from "bs58";
|
|
67
|
-
import
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
programId: "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd",
|
|
78
|
-
usdcMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
79
|
-
shareMint: "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a",
|
|
80
|
-
lookupTable: "7UbXhDnpK7WVnwsfivzQRENoqKqAULQ5s19gS1xJrQEo",
|
|
81
|
-
farm: "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
|
|
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;
|
|
82
262
|
};
|
|
83
|
-
|
|
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";
|
|
84
390
|
|
|
85
391
|
// ../../src/domain/request-binding.ts
|
|
86
392
|
function computeRequestBindingHash(fields) {
|
|
@@ -99,7 +405,7 @@ function computeRequestBindingHash(fields) {
|
|
|
99
405
|
|
|
100
406
|
// ../../src/lib/associated-token-account.ts
|
|
101
407
|
import { createHash as createHash2 } from "node:crypto";
|
|
102
|
-
import
|
|
408
|
+
import bs584 from "bs58";
|
|
103
409
|
var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
|
|
104
410
|
var ED25519_P = (1n << 255n) - 19n;
|
|
105
411
|
var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
|
|
@@ -120,7 +426,7 @@ function deriveAssociatedTokenAddress(params) {
|
|
|
120
426
|
associatedTokenProgramId
|
|
121
427
|
);
|
|
122
428
|
if (address2 !== null) {
|
|
123
|
-
return
|
|
429
|
+
return bs584.encode(address2);
|
|
124
430
|
}
|
|
125
431
|
}
|
|
126
432
|
throw new Error("Unable to derive associated token account address");
|
|
@@ -136,7 +442,7 @@ function createProgramAddress(seeds, programId) {
|
|
|
136
442
|
return isEd25519Point(digest) ? null : new Uint8Array(digest);
|
|
137
443
|
}
|
|
138
444
|
function decodePublicKey(value, fieldName) {
|
|
139
|
-
const decoded =
|
|
445
|
+
const decoded = bs584.decode(value);
|
|
140
446
|
if (decoded.length !== 32) {
|
|
141
447
|
throw new Error(`${fieldName} must be a 32-byte public key`);
|
|
142
448
|
}
|
|
@@ -331,13 +637,16 @@ function validatePaymentIntentTransaction(params) {
|
|
|
331
637
|
if (intent.network !== SOLANA_MAINNET_NETWORK) {
|
|
332
638
|
reject("network_mismatch", "Unsupported network");
|
|
333
639
|
}
|
|
334
|
-
if (intent.vault !==
|
|
640
|
+
if (intent.vault !== policy.vault.address) {
|
|
335
641
|
reject("vault_mismatch", "Unsupported vault");
|
|
336
642
|
}
|
|
337
|
-
if (intent.shareMint !==
|
|
643
|
+
if (intent.shareMint !== policy.vault.shareMint) {
|
|
338
644
|
reject("share_mint_mismatch", "Unsupported share mint");
|
|
339
645
|
}
|
|
340
|
-
if (intent.
|
|
646
|
+
if (intent.farm !== policy.vault.farm) {
|
|
647
|
+
reject("farm_mismatch", "Unsupported Kamino farm");
|
|
648
|
+
}
|
|
649
|
+
if (intent.asset !== policy.vault.usdcMint) {
|
|
341
650
|
reject("asset_mismatch", "Only USDC payments are supported");
|
|
342
651
|
}
|
|
343
652
|
if (intent.memo !== intent.paymentId) {
|
|
@@ -405,7 +714,7 @@ function validatePaymentIntentTransaction(params) {
|
|
|
405
714
|
expectComputeBudgetPair(ixs, policy);
|
|
406
715
|
expectCreateTemporaryAccount(ixs, intent, policy);
|
|
407
716
|
expectInitializeTemporaryAccount(ixs, intent);
|
|
408
|
-
consumeFarmInstructions(ixs, intent
|
|
717
|
+
consumeFarmInstructions(ixs, intent);
|
|
409
718
|
expectKvaultWithdraw(ixs, {
|
|
410
719
|
wallet: intent.wallet,
|
|
411
720
|
vault: intent.vault,
|
|
@@ -453,7 +762,7 @@ function validateDepositIntentTransaction(params) {
|
|
|
453
762
|
if (new Date(intent.expiresAt).getTime() <= now) {
|
|
454
763
|
reject("expired", "Deposit intent has expired");
|
|
455
764
|
}
|
|
456
|
-
assertVaultIntentTargets(intent);
|
|
765
|
+
assertVaultIntentTargets(intent, policy.vault);
|
|
457
766
|
const decoded = decodeIntentTransaction({
|
|
458
767
|
serializedTransaction: params.serializedTransaction,
|
|
459
768
|
...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
|
|
@@ -476,6 +785,9 @@ function validateDepositIntentTransaction(params) {
|
|
|
476
785
|
case MEMO_PROGRAM_ID:
|
|
477
786
|
break;
|
|
478
787
|
case KVAULT_PROGRAM_ID: {
|
|
788
|
+
if (sawDeposit) {
|
|
789
|
+
reject("duplicate_deposit", "A deposit intent authorizes exactly one KVault deposit");
|
|
790
|
+
}
|
|
479
791
|
if (!bytesStartWith(ix.data, KVAULT_DEPOSIT_DISCRIMINATOR)) {
|
|
480
792
|
reject("unexpected_instruction", "Unexpected KVault instruction in deposit");
|
|
481
793
|
}
|
|
@@ -526,7 +838,7 @@ function validateWithdrawalIntentTransaction(params) {
|
|
|
526
838
|
if (new Date(intent.expiresAt).getTime() <= now) {
|
|
527
839
|
reject("expired", "Withdrawal intent has expired");
|
|
528
840
|
}
|
|
529
|
-
assertVaultIntentTargets(intent);
|
|
841
|
+
assertVaultIntentTargets(intent, policy.vault);
|
|
530
842
|
const expectedDestination = deriveAssociatedTokenAddress({
|
|
531
843
|
owner: intent.wallet,
|
|
532
844
|
mint: intent.asset
|
|
@@ -548,13 +860,27 @@ function validateWithdrawalIntentTransaction(params) {
|
|
|
548
860
|
reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
|
|
549
861
|
}
|
|
550
862
|
let sawWithdraw = false;
|
|
863
|
+
let farmUserState = null;
|
|
864
|
+
let farmInstructionCount = 0;
|
|
551
865
|
for (const ix of decoded.instructions) {
|
|
552
866
|
switch (ix.programAddress) {
|
|
553
867
|
case COMPUTE_BUDGET_PROGRAM_ID:
|
|
554
868
|
validateComputeBudgetInstruction(ix, policy);
|
|
555
869
|
break;
|
|
556
870
|
case MEMO_PROGRAM_ID:
|
|
871
|
+
break;
|
|
557
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
|
+
}
|
|
558
884
|
break;
|
|
559
885
|
case ASSOCIATED_TOKEN_PROGRAM_ID2:
|
|
560
886
|
expectAtaCreateForOwner(ix, intent.wallet);
|
|
@@ -575,6 +901,12 @@ function validateWithdrawalIntentTransaction(params) {
|
|
|
575
901
|
break;
|
|
576
902
|
}
|
|
577
903
|
case KVAULT_PROGRAM_ID: {
|
|
904
|
+
if (sawWithdraw) {
|
|
905
|
+
reject(
|
|
906
|
+
"withdraw_mismatch",
|
|
907
|
+
"Withdrawal may contain only one KVault withdraw instruction"
|
|
908
|
+
);
|
|
909
|
+
}
|
|
578
910
|
validateKvaultWithdrawInstruction(ix, {
|
|
579
911
|
wallet: intent.wallet,
|
|
580
912
|
vault: intent.vault,
|
|
@@ -597,20 +929,30 @@ function validateWithdrawalIntentTransaction(params) {
|
|
|
597
929
|
if (!sawWithdraw) {
|
|
598
930
|
reject("missing_instruction", "Withdrawal transaction has no KVault withdraw");
|
|
599
931
|
}
|
|
932
|
+
if (farmInstructionCount === 1) {
|
|
933
|
+
reject(
|
|
934
|
+
"farm_instruction_mismatch",
|
|
935
|
+
"A farm unstake must be followed by a farm withdrawal"
|
|
936
|
+
);
|
|
937
|
+
}
|
|
600
938
|
}
|
|
601
|
-
function assertVaultIntentTargets(intent) {
|
|
602
|
-
if (intent.vault !==
|
|
939
|
+
function assertVaultIntentTargets(intent, vault) {
|
|
940
|
+
if (intent.vault !== vault.address) {
|
|
603
941
|
reject("vault_mismatch", "Unsupported vault");
|
|
604
942
|
}
|
|
605
|
-
if (intent.shareMint !==
|
|
943
|
+
if (intent.shareMint !== vault.shareMint) {
|
|
606
944
|
reject("share_mint_mismatch", "Unsupported share mint");
|
|
607
945
|
}
|
|
608
|
-
if (intent.
|
|
946
|
+
if (intent.farm !== vault.farm) {
|
|
947
|
+
reject("farm_mismatch", "Unsupported Kamino farm");
|
|
948
|
+
}
|
|
949
|
+
if (intent.asset !== vault.usdcMint) {
|
|
609
950
|
reject("asset_mismatch", "Only USDC is supported");
|
|
610
951
|
}
|
|
611
952
|
}
|
|
612
953
|
function resolveIntentValidationPolicy(policy) {
|
|
613
954
|
const resolved = {
|
|
955
|
+
vault: policy?.vault ?? SUBLY_VAULT,
|
|
614
956
|
maxComputeUnitLimit: policy?.maxComputeUnitLimit ?? DEFAULT_MAX_COMPUTE_UNIT_LIMIT,
|
|
615
957
|
maxComputeUnitPriceMicroLamports: policy?.maxComputeUnitPriceMicroLamports ?? DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
|
|
616
958
|
maxTemporaryAccountLamports: policy?.maxTemporaryAccountLamports ?? MAX_TEMP_ACCOUNT_LAMPORTS
|
|
@@ -677,7 +1019,7 @@ function expectCreateTemporaryAccount(ixs, intent, policy) {
|
|
|
677
1019
|
}
|
|
678
1020
|
const lamports = readU64LE(ix.data, 4);
|
|
679
1021
|
const space = readU64LE(ix.data, 12);
|
|
680
|
-
const owner =
|
|
1022
|
+
const owner = bs585.encode(ix.data.subarray(20, 52));
|
|
681
1023
|
if (space !== 165n) {
|
|
682
1024
|
reject("temp_account_mismatch", "Temporary account space must be 165 bytes");
|
|
683
1025
|
}
|
|
@@ -699,7 +1041,7 @@ function expectInitializeTemporaryAccount(ixs, intent) {
|
|
|
699
1041
|
if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 18) {
|
|
700
1042
|
reject("temp_account_mismatch", "Expected InitializeAccount3 instruction");
|
|
701
1043
|
}
|
|
702
|
-
const owner =
|
|
1044
|
+
const owner = bs585.encode(ix.data.subarray(1, 33));
|
|
703
1045
|
if (owner !== intent.wallet) {
|
|
704
1046
|
reject(
|
|
705
1047
|
"temp_account_mismatch",
|
|
@@ -713,15 +1055,90 @@ function expectInitializeTemporaryAccount(ixs, intent) {
|
|
|
713
1055
|
reject("temp_account_mismatch", "Temporary account mint must be USDC");
|
|
714
1056
|
}
|
|
715
1057
|
}
|
|
716
|
-
function consumeFarmInstructions(ixs,
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
+
);
|
|
725
1142
|
}
|
|
726
1143
|
}
|
|
727
1144
|
function expectKvaultWithdraw(ixs, expectation) {
|
|
@@ -856,6 +1273,16 @@ function readU32LE(data, offset) {
|
|
|
856
1273
|
}
|
|
857
1274
|
return Buffer.from(data.subarray(offset, offset + 4)).readUInt32LE(0);
|
|
858
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
|
+
}
|
|
859
1286
|
function readShortVec(bytes, startOffset) {
|
|
860
1287
|
let value = 0;
|
|
861
1288
|
let shift = 0;
|
|
@@ -876,55 +1303,57 @@ function readShortVec(bytes, startOffset) {
|
|
|
876
1303
|
}
|
|
877
1304
|
|
|
878
1305
|
// ../../src/client/agent-wallet-signer.ts
|
|
879
|
-
var
|
|
1306
|
+
var IntentValidatingAgentWalletSigner = class {
|
|
1307
|
+
vault;
|
|
880
1308
|
validationMode = "structured_intent_transaction";
|
|
881
|
-
keyPairSigner;
|
|
882
1309
|
validationPolicy;
|
|
883
|
-
constructor(
|
|
884
|
-
this.
|
|
885
|
-
this.validationPolicy = validationPolicy;
|
|
886
|
-
}
|
|
887
|
-
get walletAddress() {
|
|
888
|
-
return this.keyPairSigner.address;
|
|
1310
|
+
constructor(validationPolicy) {
|
|
1311
|
+
this.vault = Object.freeze({ ...validationPolicy?.vault ?? SUBLY_VAULT });
|
|
1312
|
+
this.validationPolicy = { ...validationPolicy, vault: this.vault };
|
|
889
1313
|
}
|
|
890
1314
|
async signPayment(params) {
|
|
891
1315
|
this.assertIntentWallet(params.intent.wallet);
|
|
892
|
-
validatePaymentIntentTransaction({
|
|
893
|
-
...params,
|
|
894
|
-
...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
|
|
895
|
-
});
|
|
1316
|
+
validatePaymentIntentTransaction({ ...params, ...this.policySpread() });
|
|
896
1317
|
return this.sign(params.serializedTransaction);
|
|
897
1318
|
}
|
|
898
1319
|
async signDeposit(params) {
|
|
899
1320
|
this.assertIntentWallet(params.intent.wallet);
|
|
900
|
-
validateDepositIntentTransaction({
|
|
901
|
-
...params,
|
|
902
|
-
...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
|
|
903
|
-
});
|
|
1321
|
+
validateDepositIntentTransaction({ ...params, ...this.policySpread() });
|
|
904
1322
|
return this.sign(params.serializedTransaction);
|
|
905
1323
|
}
|
|
906
1324
|
async signWithdrawal(params) {
|
|
907
1325
|
this.assertIntentWallet(params.intent.wallet);
|
|
908
|
-
validateWithdrawalIntentTransaction({
|
|
909
|
-
...params,
|
|
910
|
-
...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
|
|
911
|
-
});
|
|
1326
|
+
validateWithdrawalIntentTransaction({ ...params, ...this.policySpread() });
|
|
912
1327
|
return this.sign(params.serializedTransaction);
|
|
913
1328
|
}
|
|
1329
|
+
policySpread() {
|
|
1330
|
+
return this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy };
|
|
1331
|
+
}
|
|
914
1332
|
assertIntentWallet(wallet) {
|
|
915
|
-
if (wallet !== this.
|
|
1333
|
+
if (wallet !== this.walletAddress) {
|
|
916
1334
|
throw new IntentValidationError(
|
|
917
1335
|
"wallet_mismatch",
|
|
918
1336
|
"Intent wallet does not match this signer's wallet"
|
|
919
1337
|
);
|
|
920
1338
|
}
|
|
921
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
|
+
}
|
|
922
1351
|
async signApiMessage(message) {
|
|
923
1352
|
const signature = await signBytes(
|
|
924
1353
|
this.keyPairSigner.keyPair.privateKey,
|
|
925
1354
|
message
|
|
926
1355
|
);
|
|
927
|
-
return
|
|
1356
|
+
return bs586.encode(signature);
|
|
928
1357
|
}
|
|
929
1358
|
async sign(serializedTransaction) {
|
|
930
1359
|
const { serializedBase64, transaction } = await addSignaturesToSerializedTransaction({
|
|
@@ -944,6 +1373,418 @@ var LocalKeypairAgentWalletSigner = class {
|
|
|
944
1373
|
return { serializedTransaction: serializedBase64, agentSignature };
|
|
945
1374
|
}
|
|
946
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 rpc2 = 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 rpc2({
|
|
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 rpc2({
|
|
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
|
+
}
|
|
947
1788
|
|
|
948
1789
|
// ../../src/client/lookup-tables.ts
|
|
949
1790
|
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
|
|
@@ -987,8 +1828,8 @@ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
|
|
|
987
1828
|
|
|
988
1829
|
// ../../src/api/wallet-auth.ts
|
|
989
1830
|
import { createHash as createHash3 } from "node:crypto";
|
|
990
|
-
import
|
|
991
|
-
import
|
|
1831
|
+
import bs587 from "bs58";
|
|
1832
|
+
import nacl3 from "tweetnacl";
|
|
992
1833
|
var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
|
|
993
1834
|
var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
|
|
994
1835
|
var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
|
|
@@ -1008,7 +1849,10 @@ async function walletAuthHeaders(params) {
|
|
|
1008
1849
|
const signedAtMs = String(Date.now());
|
|
1009
1850
|
const message = walletAuthMessage({
|
|
1010
1851
|
method: params.method,
|
|
1011
|
-
path:
|
|
1852
|
+
path: (() => {
|
|
1853
|
+
const url = new URL(params.url);
|
|
1854
|
+
return url.pathname + url.search;
|
|
1855
|
+
})(),
|
|
1012
1856
|
rawBody: params.body ?? "",
|
|
1013
1857
|
signedAtMs
|
|
1014
1858
|
});
|
|
@@ -1035,6 +1879,8 @@ var VaultFlowClientError = class extends Error {
|
|
|
1035
1879
|
errorDetails;
|
|
1036
1880
|
};
|
|
1037
1881
|
var VaultFlowClient = class {
|
|
1882
|
+
vault;
|
|
1883
|
+
rpc;
|
|
1038
1884
|
baseUrl;
|
|
1039
1885
|
signer;
|
|
1040
1886
|
fetchImpl;
|
|
@@ -1042,6 +1888,11 @@ var VaultFlowClient = class {
|
|
|
1042
1888
|
pollTimeoutMs;
|
|
1043
1889
|
pollIntervalMs;
|
|
1044
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
|
+
}
|
|
1045
1896
|
this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
|
|
1046
1897
|
this.signer = config.signer;
|
|
1047
1898
|
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
@@ -1063,6 +1914,7 @@ var VaultFlowClient = class {
|
|
|
1063
1914
|
try {
|
|
1064
1915
|
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
1065
1916
|
wallet: this.signer.walletAddress,
|
|
1917
|
+
vault: this.vault.address,
|
|
1066
1918
|
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1067
1919
|
...approvalId2 === void 0 ? {} : { approvalId: approvalId2 }
|
|
1068
1920
|
});
|
|
@@ -1076,10 +1928,14 @@ var VaultFlowClient = class {
|
|
|
1076
1928
|
}
|
|
1077
1929
|
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
1078
1930
|
wallet: this.signer.walletAddress,
|
|
1931
|
+
vault: this.vault.address,
|
|
1079
1932
|
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1080
1933
|
approvalId: approvalId2
|
|
1081
1934
|
});
|
|
1082
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
|
+
}
|
|
1083
1939
|
const signed = await this.signer.signDeposit({
|
|
1084
1940
|
intent: prepared.signingIntent,
|
|
1085
1941
|
serializedTransaction: prepared.serializedTransaction,
|
|
@@ -1117,12 +1973,23 @@ var VaultFlowClient = class {
|
|
|
1117
1973
|
"/v1/withdrawals/prepare",
|
|
1118
1974
|
{
|
|
1119
1975
|
wallet: this.signer.walletAddress,
|
|
1976
|
+
vault: this.vault.address,
|
|
1120
1977
|
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1121
1978
|
...input.purpose === void 0 ? {} : { purpose: input.purpose },
|
|
1122
1979
|
...input.payment === void 0 ? {} : { payment: input.payment },
|
|
1123
1980
|
...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
|
|
1124
1981
|
}
|
|
1125
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
|
+
});
|
|
1126
1993
|
const signed = await this.signer.signWithdrawal({
|
|
1127
1994
|
intent: prepared.signingIntent,
|
|
1128
1995
|
serializedTransaction: prepared.serializedTransaction,
|
|
@@ -1160,12 +2027,12 @@ var VaultFlowClient = class {
|
|
|
1160
2027
|
await this.postJson(
|
|
1161
2028
|
"sync",
|
|
1162
2029
|
`/v1/wallets/${this.signer.walletAddress}/sync`,
|
|
1163
|
-
{ source: "chain" }
|
|
2030
|
+
{ source: "chain", vault: this.vault.address }
|
|
1164
2031
|
);
|
|
1165
2032
|
} catch {
|
|
1166
2033
|
}
|
|
1167
2034
|
}
|
|
1168
|
-
const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
|
|
2035
|
+
const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget?vault=${this.vault.address}`;
|
|
1169
2036
|
const response = await this.fetchImpl(url, {
|
|
1170
2037
|
headers: await walletAuthHeaders({
|
|
1171
2038
|
signer: this.signer,
|
|
@@ -1191,8 +2058,12 @@ var VaultFlowClient = class {
|
|
|
1191
2058
|
);
|
|
1192
2059
|
}
|
|
1193
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
|
+
}
|
|
1194
2064
|
return {
|
|
1195
2065
|
wallet: this.signer.walletAddress,
|
|
2066
|
+
vault: this.vault.address,
|
|
1196
2067
|
principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
|
|
1197
2068
|
positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
|
|
1198
2069
|
grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
|
|
@@ -1210,7 +2081,7 @@ var VaultFlowClient = class {
|
|
|
1210
2081
|
/** Wallet's approvals as the relayer sees them (optionally by status). */
|
|
1211
2082
|
async listApprovals(status) {
|
|
1212
2083
|
const body = await this.getJson(
|
|
1213
|
-
`/v1/wallets/${this.signer.walletAddress}/approvals${status === void 0 ? "" :
|
|
2084
|
+
`/v1/wallets/${this.signer.walletAddress}/approvals${`?vault=${this.vault.address}${status === void 0 ? "" : `&status=${encodeURIComponent(status)}`}`}`
|
|
1214
2085
|
);
|
|
1215
2086
|
return body.approvals ?? [];
|
|
1216
2087
|
}
|
|
@@ -1219,16 +2090,21 @@ var VaultFlowClient = class {
|
|
|
1219
2090
|
* policy + initial deposit). Paste `setupUrl` into the chat verbatim.
|
|
1220
2091
|
*/
|
|
1221
2092
|
async createSetupSession(input) {
|
|
1222
|
-
|
|
2093
|
+
const session = await this.postJson(
|
|
1223
2094
|
"prepare",
|
|
1224
2095
|
`/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
|
|
1225
2096
|
{
|
|
2097
|
+
vault: this.vault.address,
|
|
1226
2098
|
...input.policy === void 0 ? {} : { policy: input.policy },
|
|
1227
2099
|
...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
|
|
1228
2100
|
...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
|
|
1229
2101
|
...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
|
|
1230
2102
|
}
|
|
1231
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;
|
|
1232
2108
|
}
|
|
1233
2109
|
/** Polls a setup session (public capability URL — no auth needed). */
|
|
1234
2110
|
async getSetupSession(sessionId) {
|
|
@@ -1375,31 +2251,6 @@ function parseRelayerError(text) {
|
|
|
1375
2251
|
}
|
|
1376
2252
|
}
|
|
1377
2253
|
|
|
1378
|
-
// ../../src/solana/keys.ts
|
|
1379
|
-
import { readFileSync } from "node:fs";
|
|
1380
|
-
import bs586 from "bs58";
|
|
1381
|
-
import {
|
|
1382
|
-
createKeyPairSignerFromBytes
|
|
1383
|
-
} from "@solana/kit";
|
|
1384
|
-
async function loadKeyPairSigner(params) {
|
|
1385
|
-
const { base58Secret, jsonFilePath, label } = params;
|
|
1386
|
-
if (base58Secret !== void 0 && base58Secret.length > 0) {
|
|
1387
|
-
const bytes = bs586.decode(base58Secret);
|
|
1388
|
-
if (bytes.length !== 64) {
|
|
1389
|
-
throw new Error(`${label} base58 secret must decode to 64 bytes`);
|
|
1390
|
-
}
|
|
1391
|
-
return createKeyPairSignerFromBytes(bytes);
|
|
1392
|
-
}
|
|
1393
|
-
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
1394
|
-
const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
|
|
1395
|
-
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
1396
|
-
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
1397
|
-
}
|
|
1398
|
-
return createKeyPairSignerFromBytes(Uint8Array.from(raw));
|
|
1399
|
-
}
|
|
1400
|
-
throw new Error(`${label} keypair is not configured`);
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
2254
|
// ../../src/solana/rpc.ts
|
|
1404
2255
|
import { createSolanaRpc } from "@solana/kit";
|
|
1405
2256
|
function createRpc(url) {
|
|
@@ -1430,12 +2281,7 @@ var approvalId = process.argv[3];
|
|
|
1430
2281
|
if (approvalId !== void 0 && !/^apr_[0-9a-f]+$/i.test(approvalId)) {
|
|
1431
2282
|
fail(`unrecognized argument: ${approvalId} (expected apr_<approvalId>)`);
|
|
1432
2283
|
}
|
|
1433
|
-
var
|
|
1434
|
-
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
1435
|
-
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
1436
|
-
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
1437
|
-
});
|
|
1438
|
-
var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
|
|
2284
|
+
var { signer } = await agentWalletSignerFromEnv();
|
|
1439
2285
|
var rpc = createRpc(
|
|
1440
2286
|
process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com"
|
|
1441
2287
|
);
|