@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/deposit.js
CHANGED
|
@@ -1,9 +1,272 @@
|
|
|
1
|
+
// ../../src/config/vault-catalog.ts
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
// ../../src/lib/solana-address.ts
|
|
6
|
+
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
7
|
+
var BASE58_LOOKUP = new Map(
|
|
8
|
+
[...BASE58_ALPHABET].map((character, index) => [character, BigInt(index)])
|
|
9
|
+
);
|
|
10
|
+
function assertSolanaAddress(value, fieldName) {
|
|
11
|
+
if (value.length < 32 || value.length > 44) {
|
|
12
|
+
throw new Error(`${fieldName} must be a valid Solana public key`);
|
|
13
|
+
}
|
|
14
|
+
if (decodeBase58(value).length !== 32) {
|
|
15
|
+
throw new Error(`${fieldName} must be a valid Solana public key`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function decodeBase58(value) {
|
|
20
|
+
if (value.length === 0) {
|
|
21
|
+
return new Uint8Array();
|
|
22
|
+
}
|
|
23
|
+
let decoded = 0n;
|
|
24
|
+
for (const character of value) {
|
|
25
|
+
const digit = BASE58_LOOKUP.get(character);
|
|
26
|
+
if (digit === void 0) {
|
|
27
|
+
return new Uint8Array();
|
|
28
|
+
}
|
|
29
|
+
decoded = decoded * 58n + digit;
|
|
30
|
+
}
|
|
31
|
+
const bytes = [];
|
|
32
|
+
while (decoded > 0n) {
|
|
33
|
+
bytes.push(Number(decoded & 0xffn));
|
|
34
|
+
decoded >>= 8n;
|
|
35
|
+
}
|
|
36
|
+
for (const character of value) {
|
|
37
|
+
if (character !== "1") {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
bytes.push(0);
|
|
41
|
+
}
|
|
42
|
+
return Uint8Array.from(bytes.reverse());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ../../src/config/vault.ts
|
|
46
|
+
var MAINNET_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
47
|
+
var KAMINO_VAULT_PROGRAM_ID = "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd";
|
|
48
|
+
var NO_VAULT_FARM = "11111111111111111111111111111111";
|
|
49
|
+
var DEFAULT_VAULT_CONFIG = Object.freeze({
|
|
50
|
+
address: "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr",
|
|
51
|
+
programId: KAMINO_VAULT_PROGRAM_ID,
|
|
52
|
+
usdcMint: MAINNET_USDC_MINT,
|
|
53
|
+
shareMint: "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a",
|
|
54
|
+
farm: "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
|
|
55
|
+
});
|
|
56
|
+
function vaultConfigFromEnv(env = process.env) {
|
|
57
|
+
const value = (name) => env[name]?.trim() || void 0;
|
|
58
|
+
const vaultAddress = value("SUBLY_VAULT_ADDRESS") ?? DEFAULT_VAULT_CONFIG.address;
|
|
59
|
+
const customVault = vaultAddress !== DEFAULT_VAULT_CONFIG.address;
|
|
60
|
+
const anchor = (name, fallback) => {
|
|
61
|
+
const configured = value(name);
|
|
62
|
+
if (customVault && configured === void 0) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`${name} is required for a custom vault. Generate its settings with npm run configure:vault -- <vault-address>; use ${NO_VAULT_FARM} for a vault without a farm.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return assertSolanaAddress(configured ?? fallback, name);
|
|
68
|
+
};
|
|
69
|
+
const usdcMint = value("SUBLY_VAULT_USDC_MINT") ?? MAINNET_USDC_MINT;
|
|
70
|
+
if (usdcMint !== MAINNET_USDC_MINT) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"SUBLY_VAULT_USDC_MINT must be mainnet USDC; other deposit assets are not supported"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
address: assertSolanaAddress(vaultAddress, "SUBLY_VAULT_ADDRESS"),
|
|
77
|
+
programId: KAMINO_VAULT_PROGRAM_ID,
|
|
78
|
+
usdcMint,
|
|
79
|
+
shareMint: anchor("SUBLY_VAULT_SHARE_MINT", DEFAULT_VAULT_CONFIG.shareMint),
|
|
80
|
+
farm: anchor("SUBLY_VAULT_FARM", DEFAULT_VAULT_CONFIG.farm)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ../../src/config/vault-catalog.ts
|
|
85
|
+
var publicKey = z.string().refine((value) => {
|
|
86
|
+
try {
|
|
87
|
+
assertSolanaAddress(value, "vault catalog address");
|
|
88
|
+
return true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}, "Invalid Solana public key");
|
|
93
|
+
var catalogSchema = z.object({
|
|
94
|
+
version: z.literal(1),
|
|
95
|
+
defaultVault: publicKey,
|
|
96
|
+
vaults: z.array(z.object({
|
|
97
|
+
address: publicKey,
|
|
98
|
+
programId: z.literal(KAMINO_VAULT_PROGRAM_ID),
|
|
99
|
+
usdcMint: z.literal(MAINNET_USDC_MINT),
|
|
100
|
+
shareMint: publicKey,
|
|
101
|
+
farm: publicKey,
|
|
102
|
+
name: z.string().min(1).max(128).optional(),
|
|
103
|
+
depositsEnabled: z.boolean().optional(),
|
|
104
|
+
extraLookupTables: z.array(publicKey).max(16).optional()
|
|
105
|
+
}).strict()).min(1).max(100)
|
|
106
|
+
}).strict();
|
|
107
|
+
function parseVaultCatalog(value) {
|
|
108
|
+
const catalog = catalogSchema.parse(value);
|
|
109
|
+
const addresses = new Set(catalog.vaults.map((vault) => vault.address));
|
|
110
|
+
if (addresses.size !== catalog.vaults.length) throw new Error("Duplicate vault in catalog");
|
|
111
|
+
if (!addresses.has(catalog.defaultVault)) throw new Error("defaultVault must be in the vault catalog");
|
|
112
|
+
return { ...catalog, vaults: catalog.vaults.map((vault) => Object.freeze(vault)) };
|
|
113
|
+
}
|
|
114
|
+
function vaultCatalogFromEnv(env = process.env) {
|
|
115
|
+
const path = env.SUBLY_VAULTS_FILE?.trim();
|
|
116
|
+
if (!path) {
|
|
117
|
+
const vault = vaultConfigFromEnv(env);
|
|
118
|
+
return { version: 1, defaultVault: vault.address, vaults: [vault] };
|
|
119
|
+
}
|
|
120
|
+
const catalog = parseVaultCatalog(JSON.parse(readFileSync(path, "utf8")));
|
|
121
|
+
const selected = env.SUBLY_VAULT_ADDRESS?.trim() || catalog.defaultVault;
|
|
122
|
+
if (!catalog.vaults.some((vault) => vault.address === selected)) {
|
|
123
|
+
throw new Error("SUBLY_VAULT_ADDRESS must be in SUBLY_VAULTS_FILE");
|
|
124
|
+
}
|
|
125
|
+
return { ...catalog, defaultVault: selected };
|
|
126
|
+
}
|
|
127
|
+
function defaultCatalogVault(catalog) {
|
|
128
|
+
return catalog.vaults.find((vault) => vault.address === catalog.defaultVault);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ../../src/config/constants.ts
|
|
132
|
+
var PAYMENT_SCHEME = "subly-yield-exact";
|
|
133
|
+
var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
134
|
+
var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
135
|
+
var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
136
|
+
var SUBLY_VAULT = defaultCatalogVault(vaultCatalogFromEnv());
|
|
137
|
+
var USDC_DECIMALS = 6;
|
|
138
|
+
|
|
139
|
+
// ../../src/api/wallet-auth.ts
|
|
140
|
+
import { createHash } from "node:crypto";
|
|
141
|
+
import bs58 from "bs58";
|
|
142
|
+
import nacl from "tweetnacl";
|
|
143
|
+
var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
|
|
144
|
+
var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
|
|
145
|
+
var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
|
|
146
|
+
function sha256Hex(data) {
|
|
147
|
+
return createHash("sha256").update(data, "utf8").digest("hex");
|
|
148
|
+
}
|
|
149
|
+
function walletAuthMessage(params) {
|
|
150
|
+
return new TextEncoder().encode(
|
|
151
|
+
`subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
|
|
152
|
+
params.rawBody
|
|
153
|
+
)}:${params.signedAtMs}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ../../src/client/wallet-auth-headers.ts
|
|
158
|
+
async function walletAuthHeaders(params) {
|
|
159
|
+
const signedAtMs = String(Date.now());
|
|
160
|
+
const message = walletAuthMessage({
|
|
161
|
+
method: params.method,
|
|
162
|
+
path: (() => {
|
|
163
|
+
const url = new URL(params.url);
|
|
164
|
+
return url.pathname + url.search;
|
|
165
|
+
})(),
|
|
166
|
+
rawBody: params.body ?? "",
|
|
167
|
+
signedAtMs
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
[WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
|
|
171
|
+
[WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
|
|
172
|
+
[WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ../../src/client/onboarding.ts
|
|
177
|
+
var SELF_SERVE_POLICY_ID = "self-serve";
|
|
178
|
+
var OnboardingError = class extends Error {
|
|
179
|
+
constructor(step, message, detail = null) {
|
|
180
|
+
super(message);
|
|
181
|
+
this.step = step;
|
|
182
|
+
this.detail = detail;
|
|
183
|
+
this.name = "OnboardingError";
|
|
184
|
+
}
|
|
185
|
+
step;
|
|
186
|
+
detail;
|
|
187
|
+
};
|
|
188
|
+
async function ensureWalletOnboarded(params) {
|
|
189
|
+
const fetchImpl = params.fetchImpl ?? fetch;
|
|
190
|
+
const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
|
|
191
|
+
const post = async (step, path, body) => {
|
|
192
|
+
const url = `${baseUrl}${path}`;
|
|
193
|
+
const serialized = JSON.stringify(body);
|
|
194
|
+
const response = await fetchImpl(url, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: {
|
|
197
|
+
...await walletAuthHeaders({
|
|
198
|
+
signer: params.signer,
|
|
199
|
+
method: "POST",
|
|
200
|
+
url,
|
|
201
|
+
body: serialized
|
|
202
|
+
}),
|
|
203
|
+
"content-type": "application/json"
|
|
204
|
+
},
|
|
205
|
+
body: serialized
|
|
206
|
+
});
|
|
207
|
+
if (response.status !== 200) {
|
|
208
|
+
let detail = null;
|
|
209
|
+
try {
|
|
210
|
+
detail = await response.json();
|
|
211
|
+
} catch {
|
|
212
|
+
detail = null;
|
|
213
|
+
}
|
|
214
|
+
throw new OnboardingError(
|
|
215
|
+
step,
|
|
216
|
+
`wallet onboarding ${step} failed with ${response.status}`,
|
|
217
|
+
detail
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
const wallet = params.signer.walletAddress;
|
|
222
|
+
const vault = params.vault ?? params.signer.vault?.address ?? SUBLY_VAULT.address;
|
|
223
|
+
await post("register", "/v1/wallets/agent", {
|
|
224
|
+
wallet,
|
|
225
|
+
vault,
|
|
226
|
+
signingPolicyId: SELF_SERVE_POLICY_ID,
|
|
227
|
+
signingMode: "non_interactive",
|
|
228
|
+
signerValidationMode: params.signer.validationMode,
|
|
229
|
+
signerProvider: params.signer.provider ?? "local-keypair",
|
|
230
|
+
activateForPayments: true
|
|
231
|
+
});
|
|
232
|
+
await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain", vault });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ../../src/client/signer-env.ts
|
|
236
|
+
import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
|
|
237
|
+
|
|
238
|
+
// ../../src/solana/keys.ts
|
|
239
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
240
|
+
import bs582 from "bs58";
|
|
241
|
+
import {
|
|
242
|
+
createKeyPairSignerFromBytes
|
|
243
|
+
} from "@solana/kit";
|
|
244
|
+
function loadSecretKeyBytes(params) {
|
|
245
|
+
const { base58Secret, jsonFilePath, label } = params;
|
|
246
|
+
if (base58Secret !== void 0 && base58Secret.length > 0) {
|
|
247
|
+
const bytes = bs582.decode(base58Secret);
|
|
248
|
+
if (bytes.length !== 64) {
|
|
249
|
+
throw new Error(`${label} base58 secret must decode to 64 bytes`);
|
|
250
|
+
}
|
|
251
|
+
return bytes;
|
|
252
|
+
}
|
|
253
|
+
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
254
|
+
const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
|
|
255
|
+
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
256
|
+
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
257
|
+
}
|
|
258
|
+
return Uint8Array.from(raw);
|
|
259
|
+
}
|
|
260
|
+
throw new Error(`${label} keypair is not configured`);
|
|
261
|
+
}
|
|
262
|
+
|
|
1
263
|
// ../../src/client/agent-wallet-signer.ts
|
|
2
264
|
import { signBytes } from "@solana/kit";
|
|
3
|
-
import
|
|
265
|
+
import bs587 from "bs58";
|
|
266
|
+
import nacl3 from "tweetnacl";
|
|
4
267
|
|
|
5
268
|
// ../../src/solana/tx.ts
|
|
6
|
-
import
|
|
269
|
+
import bs583 from "bs58";
|
|
7
270
|
import {
|
|
8
271
|
appendTransactionMessageInstructions,
|
|
9
272
|
compileTransaction,
|
|
@@ -18,9 +281,9 @@ import {
|
|
|
18
281
|
} from "@solana/kit";
|
|
19
282
|
|
|
20
283
|
// ../../src/lib/hash.ts
|
|
21
|
-
import { createHash } from "node:crypto";
|
|
284
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
22
285
|
function sha256TaggedHex(data) {
|
|
23
|
-
return `sha256-${
|
|
286
|
+
return `sha256-${createHash2("sha256").update(data).digest("hex")}`;
|
|
24
287
|
}
|
|
25
288
|
function stableStringify(value) {
|
|
26
289
|
if (value === null) {
|
|
@@ -46,6 +309,24 @@ function hashStableJson(value) {
|
|
|
46
309
|
function decodeSerializedTransaction(serializedBase64) {
|
|
47
310
|
return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
|
|
48
311
|
}
|
|
312
|
+
function attachExternalSignatureToTransaction(params) {
|
|
313
|
+
if (!(params.signer in params.transaction.signatures)) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`transaction does not expect a signature from ${params.signer}`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const transaction = Object.freeze({
|
|
319
|
+
...params.transaction,
|
|
320
|
+
signatures: Object.freeze({
|
|
321
|
+
...params.transaction.signatures,
|
|
322
|
+
[params.signer]: params.signature
|
|
323
|
+
})
|
|
324
|
+
});
|
|
325
|
+
return {
|
|
326
|
+
serializedBase64: getBase64EncodedWireTransaction(transaction),
|
|
327
|
+
transaction
|
|
328
|
+
};
|
|
329
|
+
}
|
|
49
330
|
async function addSignaturesToSerializedTransaction(params) {
|
|
50
331
|
const decoded = decodeSerializedTransaction(params.serializedBase64);
|
|
51
332
|
const signed = await partiallySignTransaction(params.signers, decoded);
|
|
@@ -59,29 +340,150 @@ function signatureBase58ForSigner(transaction, signer2) {
|
|
|
59
340
|
if (signature === null || signature === void 0) {
|
|
60
341
|
return null;
|
|
61
342
|
}
|
|
62
|
-
return
|
|
343
|
+
return bs583.encode(signature);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ../../src/client/remote-signer-transport.ts
|
|
347
|
+
import bs584 from "bs58";
|
|
348
|
+
import nacl2 from "tweetnacl";
|
|
349
|
+
var RemoteSigningError = class extends Error {
|
|
350
|
+
constructor(provider, message, detail = null) {
|
|
351
|
+
super(`[${provider}] ${message}`);
|
|
352
|
+
this.provider = provider;
|
|
353
|
+
this.detail = detail;
|
|
354
|
+
this.name = "RemoteSigningError";
|
|
355
|
+
}
|
|
356
|
+
provider;
|
|
357
|
+
detail;
|
|
358
|
+
};
|
|
359
|
+
function ed25519PublicKeyBytes(provider, walletAddress) {
|
|
360
|
+
let bytes;
|
|
361
|
+
try {
|
|
362
|
+
bytes = bs584.decode(walletAddress);
|
|
363
|
+
} catch {
|
|
364
|
+
throw new RemoteSigningError(
|
|
365
|
+
provider,
|
|
366
|
+
`wallet address ${walletAddress} is not base58`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (bytes.length !== 32) {
|
|
370
|
+
throw new RemoteSigningError(
|
|
371
|
+
provider,
|
|
372
|
+
`wallet address ${walletAddress} is not a 32-byte ed25519 key`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return bytes;
|
|
376
|
+
}
|
|
377
|
+
function verifiedEd25519Signature(params) {
|
|
378
|
+
const publicKey2 = ed25519PublicKeyBytes(params.provider, params.walletAddress);
|
|
379
|
+
const encoded = params.encodedSignature.trim();
|
|
380
|
+
for (const candidate of decodeSignatureCandidates(encoded)) {
|
|
381
|
+
if (nacl2.sign.detached.verify(params.message, candidate, publicKey2)) {
|
|
382
|
+
return candidate;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
throw new RemoteSigningError(
|
|
386
|
+
params.provider,
|
|
387
|
+
`signature did not verify for wallet ${params.walletAddress}`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
function decodeSignatureCandidates(encoded) {
|
|
391
|
+
const candidates = [];
|
|
392
|
+
const hex = encoded.startsWith("0x") ? encoded.slice(2) : encoded;
|
|
393
|
+
if (/^[0-9a-fA-F]{128}$/.test(hex)) {
|
|
394
|
+
candidates.push(Uint8Array.from(Buffer.from(hex, "hex")));
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const fromBase58 = bs584.decode(encoded);
|
|
398
|
+
if (fromBase58.length === 64) {
|
|
399
|
+
candidates.push(fromBase58);
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
if (/^[A-Za-z0-9+/=_-]+$/.test(encoded)) {
|
|
404
|
+
const fromBase64 = Uint8Array.from(
|
|
405
|
+
Buffer.from(encoded.replace(/-/g, "+").replace(/_/g, "/"), "base64")
|
|
406
|
+
);
|
|
407
|
+
if (fromBase64.length === 64) {
|
|
408
|
+
candidates.push(fromBase64);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return candidates;
|
|
412
|
+
}
|
|
413
|
+
async function requestVerifiedTransactionSignature(params) {
|
|
414
|
+
const { transport } = params;
|
|
415
|
+
const signedBase64 = await transport.signTransaction(
|
|
416
|
+
params.serializedTransactionBase64
|
|
417
|
+
);
|
|
418
|
+
let returned;
|
|
419
|
+
try {
|
|
420
|
+
returned = decodeSerializedTransaction(signedBase64);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
throw new RemoteSigningError(
|
|
423
|
+
transport.provider,
|
|
424
|
+
"provider returned an undecodable signed transaction",
|
|
425
|
+
error
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
const signature = returned.signatures[transport.walletAddress] ?? null;
|
|
429
|
+
if (signature === null) {
|
|
430
|
+
throw new RemoteSigningError(
|
|
431
|
+
transport.provider,
|
|
432
|
+
`signed transaction is missing the signature for ${transport.walletAddress}`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const publicKey2 = params.publicKey ?? ed25519PublicKeyBytes(transport.provider, transport.walletAddress);
|
|
436
|
+
if (!nacl2.sign.detached.verify(params.messageBytes, signature, publicKey2)) {
|
|
437
|
+
throw new RemoteSigningError(
|
|
438
|
+
transport.provider,
|
|
439
|
+
"returned signature does not verify over the requested transaction"
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
return signature;
|
|
443
|
+
}
|
|
444
|
+
async function externallySignedAgentTransaction(params) {
|
|
445
|
+
const original = decodeSerializedTransaction(params.serializedTransaction);
|
|
446
|
+
const signature = await requestVerifiedTransactionSignature({
|
|
447
|
+
transport: params.transport,
|
|
448
|
+
serializedTransactionBase64: params.serializedTransaction,
|
|
449
|
+
messageBytes: original.messageBytes
|
|
450
|
+
});
|
|
451
|
+
const attached = attachExternalSignatureToTransaction({
|
|
452
|
+
transaction: original,
|
|
453
|
+
signer: params.transport.walletAddress,
|
|
454
|
+
signature
|
|
455
|
+
});
|
|
456
|
+
return {
|
|
457
|
+
serializedTransaction: attached.serializedBase64,
|
|
458
|
+
agentSignature: bs584.encode(signature)
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
async function providerJsonRequest(params) {
|
|
462
|
+
const response = await params.fetchImpl(`${params.baseUrl}${params.path}`, {
|
|
463
|
+
method: params.method,
|
|
464
|
+
headers: { ...params.headers, "content-type": "application/json" },
|
|
465
|
+
...params.body === void 0 ? {} : { body: JSON.stringify(params.body) }
|
|
466
|
+
});
|
|
467
|
+
let json = null;
|
|
468
|
+
try {
|
|
469
|
+
json = await response.json();
|
|
470
|
+
} catch {
|
|
471
|
+
json = null;
|
|
472
|
+
}
|
|
473
|
+
if (!response.ok) {
|
|
474
|
+
throw new RemoteSigningError(
|
|
475
|
+
params.provider,
|
|
476
|
+
`${params.method} ${params.path} failed with ${response.status}`,
|
|
477
|
+
json
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
return json;
|
|
63
481
|
}
|
|
64
482
|
|
|
65
483
|
// ../../src/client/transaction-intent-validator.ts
|
|
66
|
-
import
|
|
484
|
+
import bs586 from "bs58";
|
|
67
485
|
import { getCompiledTransactionMessageDecoder } from "@solana/kit";
|
|
68
486
|
|
|
69
|
-
// ../../src/config/constants.ts
|
|
70
|
-
var PAYMENT_SCHEME = "subly-yield-exact";
|
|
71
|
-
var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
72
|
-
var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
73
|
-
var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
74
|
-
var SUBLY_VAULT = {
|
|
75
|
-
name: "Subly USDC Payment Vault Alpha",
|
|
76
|
-
address: "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr",
|
|
77
|
-
programId: "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd",
|
|
78
|
-
usdcMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
79
|
-
shareMint: "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a",
|
|
80
|
-
lookupTable: "7UbXhDnpK7WVnwsfivzQRENoqKqAULQ5s19gS1xJrQEo",
|
|
81
|
-
farm: "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
|
|
82
|
-
};
|
|
83
|
-
var USDC_DECIMALS = 6;
|
|
84
|
-
|
|
85
487
|
// ../../src/domain/request-binding.ts
|
|
86
488
|
function computeRequestBindingHash(fields) {
|
|
87
489
|
return hashStableJson({
|
|
@@ -98,8 +500,8 @@ function computeRequestBindingHash(fields) {
|
|
|
98
500
|
}
|
|
99
501
|
|
|
100
502
|
// ../../src/lib/associated-token-account.ts
|
|
101
|
-
import { createHash as
|
|
102
|
-
import
|
|
503
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
504
|
+
import bs585 from "bs58";
|
|
103
505
|
var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
|
|
104
506
|
var ED25519_P = (1n << 255n) - 19n;
|
|
105
507
|
var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
|
|
@@ -120,13 +522,13 @@ function deriveAssociatedTokenAddress(params) {
|
|
|
120
522
|
associatedTokenProgramId
|
|
121
523
|
);
|
|
122
524
|
if (address2 !== null) {
|
|
123
|
-
return
|
|
525
|
+
return bs585.encode(address2);
|
|
124
526
|
}
|
|
125
527
|
}
|
|
126
528
|
throw new Error("Unable to derive associated token account address");
|
|
127
529
|
}
|
|
128
530
|
function createProgramAddress(seeds, programId) {
|
|
129
|
-
const hash =
|
|
531
|
+
const hash = createHash3("sha256");
|
|
130
532
|
for (const seed of seeds) {
|
|
131
533
|
hash.update(seed);
|
|
132
534
|
}
|
|
@@ -136,7 +538,7 @@ function createProgramAddress(seeds, programId) {
|
|
|
136
538
|
return isEd25519Point(digest) ? null : new Uint8Array(digest);
|
|
137
539
|
}
|
|
138
540
|
function decodePublicKey(value, fieldName) {
|
|
139
|
-
const decoded =
|
|
541
|
+
const decoded = bs585.decode(value);
|
|
140
542
|
if (decoded.length !== 32) {
|
|
141
543
|
throw new Error(`${fieldName} must be a 32-byte public key`);
|
|
142
544
|
}
|
|
@@ -331,13 +733,16 @@ function validatePaymentIntentTransaction(params) {
|
|
|
331
733
|
if (intent.network !== SOLANA_MAINNET_NETWORK) {
|
|
332
734
|
reject("network_mismatch", "Unsupported network");
|
|
333
735
|
}
|
|
334
|
-
if (intent.vault !==
|
|
736
|
+
if (intent.vault !== policy.vault.address) {
|
|
335
737
|
reject("vault_mismatch", "Unsupported vault");
|
|
336
738
|
}
|
|
337
|
-
if (intent.shareMint !==
|
|
739
|
+
if (intent.shareMint !== policy.vault.shareMint) {
|
|
338
740
|
reject("share_mint_mismatch", "Unsupported share mint");
|
|
339
741
|
}
|
|
340
|
-
if (intent.
|
|
742
|
+
if (intent.farm !== policy.vault.farm) {
|
|
743
|
+
reject("farm_mismatch", "Unsupported Kamino farm");
|
|
744
|
+
}
|
|
745
|
+
if (intent.asset !== policy.vault.usdcMint) {
|
|
341
746
|
reject("asset_mismatch", "Only USDC payments are supported");
|
|
342
747
|
}
|
|
343
748
|
if (intent.memo !== intent.paymentId) {
|
|
@@ -405,7 +810,7 @@ function validatePaymentIntentTransaction(params) {
|
|
|
405
810
|
expectComputeBudgetPair(ixs, policy);
|
|
406
811
|
expectCreateTemporaryAccount(ixs, intent, policy);
|
|
407
812
|
expectInitializeTemporaryAccount(ixs, intent);
|
|
408
|
-
consumeFarmInstructions(ixs, intent
|
|
813
|
+
consumeFarmInstructions(ixs, intent);
|
|
409
814
|
expectKvaultWithdraw(ixs, {
|
|
410
815
|
wallet: intent.wallet,
|
|
411
816
|
vault: intent.vault,
|
|
@@ -453,7 +858,7 @@ function validateDepositIntentTransaction(params) {
|
|
|
453
858
|
if (new Date(intent.expiresAt).getTime() <= now) {
|
|
454
859
|
reject("expired", "Deposit intent has expired");
|
|
455
860
|
}
|
|
456
|
-
assertVaultIntentTargets(intent);
|
|
861
|
+
assertVaultIntentTargets(intent, policy.vault);
|
|
457
862
|
const decoded = decodeIntentTransaction({
|
|
458
863
|
serializedTransaction: params.serializedTransaction,
|
|
459
864
|
...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
|
|
@@ -476,6 +881,9 @@ function validateDepositIntentTransaction(params) {
|
|
|
476
881
|
case MEMO_PROGRAM_ID:
|
|
477
882
|
break;
|
|
478
883
|
case KVAULT_PROGRAM_ID: {
|
|
884
|
+
if (sawDeposit) {
|
|
885
|
+
reject("duplicate_deposit", "A deposit intent authorizes exactly one KVault deposit");
|
|
886
|
+
}
|
|
479
887
|
if (!bytesStartWith(ix.data, KVAULT_DEPOSIT_DISCRIMINATOR)) {
|
|
480
888
|
reject("unexpected_instruction", "Unexpected KVault instruction in deposit");
|
|
481
889
|
}
|
|
@@ -526,7 +934,7 @@ function validateWithdrawalIntentTransaction(params) {
|
|
|
526
934
|
if (new Date(intent.expiresAt).getTime() <= now) {
|
|
527
935
|
reject("expired", "Withdrawal intent has expired");
|
|
528
936
|
}
|
|
529
|
-
assertVaultIntentTargets(intent);
|
|
937
|
+
assertVaultIntentTargets(intent, policy.vault);
|
|
530
938
|
const expectedDestination = deriveAssociatedTokenAddress({
|
|
531
939
|
owner: intent.wallet,
|
|
532
940
|
mint: intent.asset
|
|
@@ -548,13 +956,27 @@ function validateWithdrawalIntentTransaction(params) {
|
|
|
548
956
|
reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
|
|
549
957
|
}
|
|
550
958
|
let sawWithdraw = false;
|
|
959
|
+
let farmUserState = null;
|
|
960
|
+
let farmInstructionCount = 0;
|
|
551
961
|
for (const ix of decoded.instructions) {
|
|
552
962
|
switch (ix.programAddress) {
|
|
553
963
|
case COMPUTE_BUDGET_PROGRAM_ID:
|
|
554
964
|
validateComputeBudgetInstruction(ix, policy);
|
|
555
965
|
break;
|
|
556
966
|
case MEMO_PROGRAM_ID:
|
|
967
|
+
break;
|
|
557
968
|
case KAMINO_FARMS_PROGRAM_ID:
|
|
969
|
+
farmInstructionCount += 1;
|
|
970
|
+
if (farmInstructionCount === 1) {
|
|
971
|
+
farmUserState = validateFarmUnstakeInstruction(ix, intent);
|
|
972
|
+
} else if (farmInstructionCount === 2) {
|
|
973
|
+
validateFarmWithdrawInstruction(ix, intent, farmUserState);
|
|
974
|
+
} else {
|
|
975
|
+
reject(
|
|
976
|
+
"farm_instruction_mismatch",
|
|
977
|
+
"Withdrawal may contain only one farm unstake and one farm withdrawal"
|
|
978
|
+
);
|
|
979
|
+
}
|
|
558
980
|
break;
|
|
559
981
|
case ASSOCIATED_TOKEN_PROGRAM_ID2:
|
|
560
982
|
expectAtaCreateForOwner(ix, intent.wallet);
|
|
@@ -575,6 +997,12 @@ function validateWithdrawalIntentTransaction(params) {
|
|
|
575
997
|
break;
|
|
576
998
|
}
|
|
577
999
|
case KVAULT_PROGRAM_ID: {
|
|
1000
|
+
if (sawWithdraw) {
|
|
1001
|
+
reject(
|
|
1002
|
+
"withdraw_mismatch",
|
|
1003
|
+
"Withdrawal may contain only one KVault withdraw instruction"
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
578
1006
|
validateKvaultWithdrawInstruction(ix, {
|
|
579
1007
|
wallet: intent.wallet,
|
|
580
1008
|
vault: intent.vault,
|
|
@@ -597,20 +1025,30 @@ function validateWithdrawalIntentTransaction(params) {
|
|
|
597
1025
|
if (!sawWithdraw) {
|
|
598
1026
|
reject("missing_instruction", "Withdrawal transaction has no KVault withdraw");
|
|
599
1027
|
}
|
|
1028
|
+
if (farmInstructionCount === 1) {
|
|
1029
|
+
reject(
|
|
1030
|
+
"farm_instruction_mismatch",
|
|
1031
|
+
"A farm unstake must be followed by a farm withdrawal"
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
600
1034
|
}
|
|
601
|
-
function assertVaultIntentTargets(intent) {
|
|
602
|
-
if (intent.vault !==
|
|
1035
|
+
function assertVaultIntentTargets(intent, vault) {
|
|
1036
|
+
if (intent.vault !== vault.address) {
|
|
603
1037
|
reject("vault_mismatch", "Unsupported vault");
|
|
604
1038
|
}
|
|
605
|
-
if (intent.shareMint !==
|
|
1039
|
+
if (intent.shareMint !== vault.shareMint) {
|
|
606
1040
|
reject("share_mint_mismatch", "Unsupported share mint");
|
|
607
1041
|
}
|
|
608
|
-
if (intent.
|
|
1042
|
+
if (intent.farm !== vault.farm) {
|
|
1043
|
+
reject("farm_mismatch", "Unsupported Kamino farm");
|
|
1044
|
+
}
|
|
1045
|
+
if (intent.asset !== vault.usdcMint) {
|
|
609
1046
|
reject("asset_mismatch", "Only USDC is supported");
|
|
610
1047
|
}
|
|
611
1048
|
}
|
|
612
1049
|
function resolveIntentValidationPolicy(policy) {
|
|
613
1050
|
const resolved = {
|
|
1051
|
+
vault: policy?.vault ?? SUBLY_VAULT,
|
|
614
1052
|
maxComputeUnitLimit: policy?.maxComputeUnitLimit ?? DEFAULT_MAX_COMPUTE_UNIT_LIMIT,
|
|
615
1053
|
maxComputeUnitPriceMicroLamports: policy?.maxComputeUnitPriceMicroLamports ?? DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
|
|
616
1054
|
maxTemporaryAccountLamports: policy?.maxTemporaryAccountLamports ?? MAX_TEMP_ACCOUNT_LAMPORTS
|
|
@@ -677,7 +1115,7 @@ function expectCreateTemporaryAccount(ixs, intent, policy) {
|
|
|
677
1115
|
}
|
|
678
1116
|
const lamports = readU64LE(ix.data, 4);
|
|
679
1117
|
const space = readU64LE(ix.data, 12);
|
|
680
|
-
const owner =
|
|
1118
|
+
const owner = bs586.encode(ix.data.subarray(20, 52));
|
|
681
1119
|
if (space !== 165n) {
|
|
682
1120
|
reject("temp_account_mismatch", "Temporary account space must be 165 bytes");
|
|
683
1121
|
}
|
|
@@ -699,7 +1137,7 @@ function expectInitializeTemporaryAccount(ixs, intent) {
|
|
|
699
1137
|
if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 18) {
|
|
700
1138
|
reject("temp_account_mismatch", "Expected InitializeAccount3 instruction");
|
|
701
1139
|
}
|
|
702
|
-
const owner =
|
|
1140
|
+
const owner = bs586.encode(ix.data.subarray(1, 33));
|
|
703
1141
|
if (owner !== intent.wallet) {
|
|
704
1142
|
reject(
|
|
705
1143
|
"temp_account_mismatch",
|
|
@@ -713,15 +1151,90 @@ function expectInitializeTemporaryAccount(ixs, intent) {
|
|
|
713
1151
|
reject("temp_account_mismatch", "Temporary account mint must be USDC");
|
|
714
1152
|
}
|
|
715
1153
|
}
|
|
716
|
-
function consumeFarmInstructions(ixs,
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1154
|
+
function consumeFarmInstructions(ixs, intent) {
|
|
1155
|
+
if (ixs[0]?.programAddress !== KAMINO_FARMS_PROGRAM_ID) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const unstake = ixs.shift();
|
|
1159
|
+
const userState = validateFarmUnstakeInstruction(unstake, intent);
|
|
1160
|
+
if (ixs[0]?.programAddress !== KAMINO_FARMS_PROGRAM_ID) {
|
|
1161
|
+
reject(
|
|
1162
|
+
"farm_instruction_mismatch",
|
|
1163
|
+
"A farm unstake must be followed by a farm withdrawal"
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
const withdraw = ixs.shift();
|
|
1167
|
+
validateFarmWithdrawInstruction(withdraw, intent, userState);
|
|
1168
|
+
if (ixs[0]?.programAddress === KAMINO_FARMS_PROGRAM_ID) {
|
|
1169
|
+
reject(
|
|
1170
|
+
"farm_instruction_mismatch",
|
|
1171
|
+
"Payment may contain only one farm unstake and one farm withdrawal"
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
var KAMINO_FARMS_UNSTAKE_DISCRIMINATOR = Uint8Array.from([
|
|
1176
|
+
90,
|
|
1177
|
+
95,
|
|
1178
|
+
107,
|
|
1179
|
+
42,
|
|
1180
|
+
205,
|
|
1181
|
+
124,
|
|
1182
|
+
50,
|
|
1183
|
+
225
|
|
1184
|
+
]);
|
|
1185
|
+
var KAMINO_FARMS_WITHDRAW_UNSTAKED_DISCRIMINATOR = Uint8Array.from([
|
|
1186
|
+
36,
|
|
1187
|
+
102,
|
|
1188
|
+
187,
|
|
1189
|
+
49,
|
|
1190
|
+
220,
|
|
1191
|
+
36,
|
|
1192
|
+
132,
|
|
1193
|
+
67
|
|
1194
|
+
]);
|
|
1195
|
+
function validateFarmUnstakeInstruction(ix, intent) {
|
|
1196
|
+
if (ix.programAddress !== KAMINO_FARMS_PROGRAM_ID || !bytesStartWith(ix.data, KAMINO_FARMS_UNSTAKE_DISCRIMINATOR) || ix.data.length !== 24 || readU128LE(ix.data, 8) <= 0n) {
|
|
1197
|
+
reject(
|
|
1198
|
+
"farm_instruction_mismatch",
|
|
1199
|
+
"Expected a non-zero Kamino farm unstake instruction"
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
if (ix.accounts.length !== 4) {
|
|
1203
|
+
reject(
|
|
1204
|
+
"farm_instruction_mismatch",
|
|
1205
|
+
"Farm unstake account list is not canonical"
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
if (ix.accounts[0] !== intent.wallet) {
|
|
1209
|
+
reject("farm_instruction_mismatch", "Farm unstake owner must be the agent wallet");
|
|
1210
|
+
}
|
|
1211
|
+
if (ix.accounts[2] !== intent.farm) {
|
|
1212
|
+
reject("farm_instruction_mismatch", "Farm unstake target is not the approved farm");
|
|
1213
|
+
}
|
|
1214
|
+
return ix.accounts[1];
|
|
1215
|
+
}
|
|
1216
|
+
function validateFarmWithdrawInstruction(ix, intent, expectedUserState) {
|
|
1217
|
+
if (ix.programAddress !== KAMINO_FARMS_PROGRAM_ID || !bytesStartWith(ix.data, KAMINO_FARMS_WITHDRAW_UNSTAKED_DISCRIMINATOR) || ix.data.length !== 8) {
|
|
1218
|
+
reject(
|
|
1219
|
+
"farm_instruction_mismatch",
|
|
1220
|
+
"Expected a canonical Kamino farm withdrawal instruction"
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
if (ix.accounts.length !== 7) {
|
|
1224
|
+
reject(
|
|
1225
|
+
"farm_instruction_mismatch",
|
|
1226
|
+
"Farm withdrawal account list is not canonical"
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
const expectedSharesAta = deriveAssociatedTokenAddress({
|
|
1230
|
+
owner: intent.wallet,
|
|
1231
|
+
mint: intent.shareMint
|
|
1232
|
+
});
|
|
1233
|
+
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) {
|
|
1234
|
+
reject(
|
|
1235
|
+
"farm_instruction_mismatch",
|
|
1236
|
+
"Farm withdrawal must return the approved vault shares to the agent wallet"
|
|
1237
|
+
);
|
|
725
1238
|
}
|
|
726
1239
|
}
|
|
727
1240
|
function expectKvaultWithdraw(ixs, expectation) {
|
|
@@ -856,6 +1369,16 @@ function readU32LE(data, offset) {
|
|
|
856
1369
|
}
|
|
857
1370
|
return Buffer.from(data.subarray(offset, offset + 4)).readUInt32LE(0);
|
|
858
1371
|
}
|
|
1372
|
+
function readU128LE(data, offset) {
|
|
1373
|
+
if (data.length < offset + 16) {
|
|
1374
|
+
reject("invalid_transaction_encoding", "Instruction data too short for u128");
|
|
1375
|
+
}
|
|
1376
|
+
let value = 0n;
|
|
1377
|
+
for (let index = 0; index < 16; index += 1) {
|
|
1378
|
+
value |= BigInt(data[offset + index]) << BigInt(index * 8);
|
|
1379
|
+
}
|
|
1380
|
+
return value;
|
|
1381
|
+
}
|
|
859
1382
|
function readShortVec(bytes, startOffset) {
|
|
860
1383
|
let value = 0;
|
|
861
1384
|
let shift = 0;
|
|
@@ -876,55 +1399,57 @@ function readShortVec(bytes, startOffset) {
|
|
|
876
1399
|
}
|
|
877
1400
|
|
|
878
1401
|
// ../../src/client/agent-wallet-signer.ts
|
|
879
|
-
var
|
|
1402
|
+
var IntentValidatingAgentWalletSigner = class {
|
|
1403
|
+
vault;
|
|
880
1404
|
validationMode = "structured_intent_transaction";
|
|
881
|
-
keyPairSigner;
|
|
882
1405
|
validationPolicy;
|
|
883
|
-
constructor(
|
|
884
|
-
this.
|
|
885
|
-
this.validationPolicy = validationPolicy;
|
|
886
|
-
}
|
|
887
|
-
get walletAddress() {
|
|
888
|
-
return this.keyPairSigner.address;
|
|
1406
|
+
constructor(validationPolicy) {
|
|
1407
|
+
this.vault = Object.freeze({ ...validationPolicy?.vault ?? SUBLY_VAULT });
|
|
1408
|
+
this.validationPolicy = { ...validationPolicy, vault: this.vault };
|
|
889
1409
|
}
|
|
890
1410
|
async signPayment(params) {
|
|
891
1411
|
this.assertIntentWallet(params.intent.wallet);
|
|
892
|
-
validatePaymentIntentTransaction({
|
|
893
|
-
...params,
|
|
894
|
-
...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
|
|
895
|
-
});
|
|
1412
|
+
validatePaymentIntentTransaction({ ...params, ...this.policySpread() });
|
|
896
1413
|
return this.sign(params.serializedTransaction);
|
|
897
1414
|
}
|
|
898
1415
|
async signDeposit(params) {
|
|
899
1416
|
this.assertIntentWallet(params.intent.wallet);
|
|
900
|
-
validateDepositIntentTransaction({
|
|
901
|
-
...params,
|
|
902
|
-
...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
|
|
903
|
-
});
|
|
1417
|
+
validateDepositIntentTransaction({ ...params, ...this.policySpread() });
|
|
904
1418
|
return this.sign(params.serializedTransaction);
|
|
905
1419
|
}
|
|
906
1420
|
async signWithdrawal(params) {
|
|
907
1421
|
this.assertIntentWallet(params.intent.wallet);
|
|
908
|
-
validateWithdrawalIntentTransaction({
|
|
909
|
-
...params,
|
|
910
|
-
...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
|
|
911
|
-
});
|
|
1422
|
+
validateWithdrawalIntentTransaction({ ...params, ...this.policySpread() });
|
|
912
1423
|
return this.sign(params.serializedTransaction);
|
|
913
1424
|
}
|
|
1425
|
+
policySpread() {
|
|
1426
|
+
return this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy };
|
|
1427
|
+
}
|
|
914
1428
|
assertIntentWallet(wallet) {
|
|
915
|
-
if (wallet !== this.
|
|
1429
|
+
if (wallet !== this.walletAddress) {
|
|
916
1430
|
throw new IntentValidationError(
|
|
917
1431
|
"wallet_mismatch",
|
|
918
1432
|
"Intent wallet does not match this signer's wallet"
|
|
919
1433
|
);
|
|
920
1434
|
}
|
|
921
1435
|
}
|
|
1436
|
+
};
|
|
1437
|
+
var LocalKeypairAgentWalletSigner = class extends IntentValidatingAgentWalletSigner {
|
|
1438
|
+
provider = "local-keypair";
|
|
1439
|
+
keyPairSigner;
|
|
1440
|
+
constructor(keyPairSigner, validationPolicy) {
|
|
1441
|
+
super(validationPolicy);
|
|
1442
|
+
this.keyPairSigner = keyPairSigner;
|
|
1443
|
+
}
|
|
1444
|
+
get walletAddress() {
|
|
1445
|
+
return this.keyPairSigner.address;
|
|
1446
|
+
}
|
|
922
1447
|
async signApiMessage(message) {
|
|
923
1448
|
const signature = await signBytes(
|
|
924
1449
|
this.keyPairSigner.keyPair.privateKey,
|
|
925
1450
|
message
|
|
926
1451
|
);
|
|
927
|
-
return
|
|
1452
|
+
return bs587.encode(signature);
|
|
928
1453
|
}
|
|
929
1454
|
async sign(serializedTransaction) {
|
|
930
1455
|
const { serializedBase64, transaction } = await addSignaturesToSerializedTransaction({
|
|
@@ -944,96 +1469,417 @@ var LocalKeypairAgentWalletSigner = class {
|
|
|
944
1469
|
return { serializedTransaction: serializedBase64, agentSignature };
|
|
945
1470
|
}
|
|
946
1471
|
};
|
|
1472
|
+
var RemoteAgentWalletSigner = class extends IntentValidatingAgentWalletSigner {
|
|
1473
|
+
transport;
|
|
1474
|
+
publicKey;
|
|
1475
|
+
constructor(transport, validationPolicy) {
|
|
1476
|
+
super(validationPolicy);
|
|
1477
|
+
this.transport = transport;
|
|
1478
|
+
this.publicKey = ed25519PublicKeyBytes(
|
|
1479
|
+
transport.provider,
|
|
1480
|
+
transport.walletAddress
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
get walletAddress() {
|
|
1484
|
+
return this.transport.walletAddress;
|
|
1485
|
+
}
|
|
1486
|
+
get provider() {
|
|
1487
|
+
return this.transport.provider;
|
|
1488
|
+
}
|
|
1489
|
+
async signApiMessage(message) {
|
|
1490
|
+
const signature = await this.transport.signMessage(message);
|
|
1491
|
+
if (!nacl3.sign.detached.verify(message, signature, this.publicKey)) {
|
|
1492
|
+
throw new RemoteSigningError(
|
|
1493
|
+
this.transport.provider,
|
|
1494
|
+
"message signature did not verify for the agent wallet"
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
return bs587.encode(signature);
|
|
1498
|
+
}
|
|
1499
|
+
sign(serializedTransaction) {
|
|
1500
|
+
return externallySignedAgentTransaction({
|
|
1501
|
+
transport: this.transport,
|
|
1502
|
+
serializedTransaction
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
947
1506
|
|
|
948
|
-
// ../../src/
|
|
949
|
-
import {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
var
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
);
|
|
1507
|
+
// ../../src/client/signer-transports/circle.ts
|
|
1508
|
+
import {
|
|
1509
|
+
constants,
|
|
1510
|
+
createPublicKey,
|
|
1511
|
+
publicEncrypt
|
|
1512
|
+
} from "node:crypto";
|
|
1513
|
+
var PROVIDER = "circle";
|
|
1514
|
+
var DEFAULT_BASE_URL = "https://api.circle.com";
|
|
1515
|
+
async function createCircleSignerTransport(config) {
|
|
1516
|
+
if (!/^[0-9a-fA-F]{64}$/.test(config.entitySecret)) {
|
|
1517
|
+
throw new RemoteSigningError(
|
|
1518
|
+
PROVIDER,
|
|
1519
|
+
"entity secret must be 32 bytes of hex (64 hex chars)"
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
1523
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
1524
|
+
const request = async (method, path, body) => {
|
|
1525
|
+
const json = await providerJsonRequest({
|
|
1526
|
+
provider: PROVIDER,
|
|
1527
|
+
fetchImpl,
|
|
1528
|
+
baseUrl,
|
|
1529
|
+
path,
|
|
1530
|
+
method,
|
|
1531
|
+
headers: { authorization: `Bearer ${config.apiKey}` },
|
|
1532
|
+
body
|
|
1533
|
+
});
|
|
1534
|
+
const data = json?.data;
|
|
1535
|
+
if (data === void 0) {
|
|
1536
|
+
throw new RemoteSigningError(
|
|
1537
|
+
PROVIDER,
|
|
1538
|
+
`${method} ${path} returned no data envelope`,
|
|
1539
|
+
json
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
return data;
|
|
1543
|
+
};
|
|
1544
|
+
const walletData = await request("GET", `/v1/w3s/wallets/${config.walletId}`);
|
|
1545
|
+
const wallet = walletData.wallet;
|
|
1546
|
+
if (wallet?.address === void 0) {
|
|
1547
|
+
throw new RemoteSigningError(
|
|
1548
|
+
PROVIDER,
|
|
1549
|
+
`wallet ${config.walletId} has no address`,
|
|
1550
|
+
walletData
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
if (wallet.blockchain !== "SOL") {
|
|
1554
|
+
throw new RemoteSigningError(
|
|
1555
|
+
PROVIDER,
|
|
1556
|
+
`wallet ${config.walletId} is on ${String(
|
|
1557
|
+
wallet.blockchain
|
|
1558
|
+
)}, expected SOL (Solana mainnet)`
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
const walletAddress = wallet.address;
|
|
1562
|
+
let entityPublicKey = null;
|
|
1563
|
+
const entitySecretCiphertext = async () => {
|
|
1564
|
+
if (entityPublicKey === null) {
|
|
1565
|
+
const data = await request("GET", "/v1/w3s/config/entity/publicKey");
|
|
1566
|
+
const publicKey2 = data.publicKey;
|
|
1567
|
+
if (typeof publicKey2 !== "string") {
|
|
1568
|
+
throw new RemoteSigningError(
|
|
1569
|
+
PROVIDER,
|
|
1570
|
+
"entity public key response has no publicKey",
|
|
1571
|
+
data
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
entityPublicKey = createPublicKey(publicKey2);
|
|
1575
|
+
}
|
|
1576
|
+
return publicEncrypt(
|
|
1577
|
+
{
|
|
1578
|
+
key: entityPublicKey,
|
|
1579
|
+
padding: constants.RSA_PKCS1_OAEP_PADDING,
|
|
1580
|
+
oaepHash: "sha256"
|
|
1581
|
+
},
|
|
1582
|
+
Buffer.from(config.entitySecret, "hex")
|
|
1583
|
+
).toString("base64");
|
|
1584
|
+
};
|
|
1585
|
+
return {
|
|
1586
|
+
provider: PROVIDER,
|
|
1587
|
+
walletAddress,
|
|
1588
|
+
async signMessage(message) {
|
|
1589
|
+
const data = await request("POST", "/v1/w3s/developer/sign/message", {
|
|
1590
|
+
walletId: config.walletId,
|
|
1591
|
+
message: `0x${Buffer.from(message).toString("hex")}`,
|
|
1592
|
+
encodedByHex: true,
|
|
1593
|
+
entitySecretCiphertext: await entitySecretCiphertext()
|
|
1594
|
+
});
|
|
1595
|
+
const signature = data.signature;
|
|
1596
|
+
if (typeof signature !== "string") {
|
|
1597
|
+
throw new RemoteSigningError(
|
|
1598
|
+
PROVIDER,
|
|
1599
|
+
"sign/message returned no signature",
|
|
1600
|
+
data
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
return verifiedEd25519Signature({
|
|
1604
|
+
provider: PROVIDER,
|
|
1605
|
+
encodedSignature: signature,
|
|
1606
|
+
message,
|
|
1607
|
+
walletAddress
|
|
1608
|
+
});
|
|
1609
|
+
},
|
|
1610
|
+
async signTransaction(serializedTransactionBase64) {
|
|
1611
|
+
const data = await request(
|
|
1612
|
+
"POST",
|
|
1613
|
+
"/v1/w3s/developer/sign/transaction",
|
|
1614
|
+
{
|
|
1615
|
+
walletId: config.walletId,
|
|
1616
|
+
rawTransaction: serializedTransactionBase64,
|
|
1617
|
+
entitySecretCiphertext: await entitySecretCiphertext()
|
|
1618
|
+
}
|
|
1619
|
+
);
|
|
1620
|
+
const signedTransaction = data.signedTransaction;
|
|
1621
|
+
if (typeof signedTransaction !== "string") {
|
|
1622
|
+
throw new RemoteSigningError(
|
|
1623
|
+
PROVIDER,
|
|
1624
|
+
"sign/transaction returned no signedTransaction",
|
|
1625
|
+
data
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
return signedTransaction;
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
964
1631
|
}
|
|
965
1632
|
|
|
966
|
-
// ../../src/client/
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1633
|
+
// ../../src/client/signer-transports/privy.ts
|
|
1634
|
+
import { createPrivateKey, createSign } from "node:crypto";
|
|
1635
|
+
var PROVIDER2 = "privy";
|
|
1636
|
+
var DEFAULT_BASE_URL2 = "https://api.privy.io";
|
|
1637
|
+
function canonicalJson(value) {
|
|
1638
|
+
if (Array.isArray(value)) {
|
|
1639
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
1640
|
+
}
|
|
1641
|
+
if (value !== null && typeof value === "object") {
|
|
1642
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1643
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
|
|
1644
|
+
}
|
|
1645
|
+
return JSON.stringify(value);
|
|
1646
|
+
}
|
|
1647
|
+
function parseAuthorizationKey(base64Pkcs8) {
|
|
1648
|
+
const stripped = base64Pkcs8.replace(/^wallet-auth:/, "").trim();
|
|
1649
|
+
try {
|
|
1650
|
+
return createPrivateKey({
|
|
1651
|
+
key: Buffer.from(stripped, "base64"),
|
|
1652
|
+
format: "der",
|
|
1653
|
+
type: "pkcs8"
|
|
1654
|
+
});
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
throw new RemoteSigningError(
|
|
1657
|
+
PROVIDER2,
|
|
1658
|
+
"authorization key is not a base64 PKCS#8 P-256 private key",
|
|
1659
|
+
error
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
function authorizationSignature(params) {
|
|
1664
|
+
const payload = {
|
|
1665
|
+
version: 1,
|
|
970
1666
|
method: params.method,
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
}
|
|
1667
|
+
url: params.url,
|
|
1668
|
+
body: params.body,
|
|
1669
|
+
headers: { "privy-app-id": params.appId }
|
|
1670
|
+
};
|
|
1671
|
+
const signer2 = createSign("sha256");
|
|
1672
|
+
signer2.update(canonicalJson(payload));
|
|
1673
|
+
return signer2.sign(params.key).toString("base64");
|
|
1674
|
+
}
|
|
1675
|
+
async function createPrivySignerTransport(config) {
|
|
1676
|
+
const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/$/, "");
|
|
1677
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
1678
|
+
const authorizationKey = config.authorizationPrivateKey === void 0 ? null : parseAuthorizationKey(config.authorizationPrivateKey);
|
|
1679
|
+
const baseHeaders = {
|
|
1680
|
+
authorization: `Basic ${Buffer.from(
|
|
1681
|
+
`${config.appId}:${config.appSecret}`
|
|
1682
|
+
).toString("base64")}`,
|
|
1683
|
+
"privy-app-id": config.appId
|
|
1684
|
+
};
|
|
1685
|
+
const request = async (method, path, body) => {
|
|
1686
|
+
const headers = authorizationKey !== null && method !== "GET" && body !== void 0 ? {
|
|
1687
|
+
...baseHeaders,
|
|
1688
|
+
"privy-authorization-signature": authorizationSignature({
|
|
1689
|
+
key: authorizationKey,
|
|
1690
|
+
appId: config.appId,
|
|
1691
|
+
method,
|
|
1692
|
+
url: `${baseUrl}${path}`,
|
|
1693
|
+
body
|
|
1694
|
+
})
|
|
1695
|
+
} : baseHeaders;
|
|
1696
|
+
const json = await providerJsonRequest({
|
|
1697
|
+
provider: PROVIDER2,
|
|
1698
|
+
fetchImpl,
|
|
1699
|
+
baseUrl,
|
|
1700
|
+
path,
|
|
1701
|
+
method,
|
|
1702
|
+
headers,
|
|
1703
|
+
body
|
|
1704
|
+
});
|
|
1705
|
+
if (json === null || typeof json !== "object") {
|
|
1706
|
+
throw new RemoteSigningError(
|
|
1707
|
+
PROVIDER2,
|
|
1708
|
+
`${method} ${path} returned a non-JSON body`
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
return json;
|
|
1712
|
+
};
|
|
1713
|
+
const rpc2 = async (body) => {
|
|
1714
|
+
const response = await request(
|
|
1715
|
+
"POST",
|
|
1716
|
+
`/v1/wallets/${config.walletId}/rpc`,
|
|
1717
|
+
body
|
|
1718
|
+
);
|
|
1719
|
+
const data = response.data;
|
|
1720
|
+
if (data === null || typeof data !== "object") {
|
|
1721
|
+
throw new RemoteSigningError(PROVIDER2, "rpc returned no data", response);
|
|
1722
|
+
}
|
|
1723
|
+
return data;
|
|
1724
|
+
};
|
|
1725
|
+
const wallet = await request("GET", `/v1/wallets/${config.walletId}`);
|
|
1726
|
+
const walletAddress = wallet.address;
|
|
1727
|
+
if (typeof walletAddress !== "string") {
|
|
1728
|
+
throw new RemoteSigningError(
|
|
1729
|
+
PROVIDER2,
|
|
1730
|
+
`wallet ${config.walletId} has no address`,
|
|
1731
|
+
wallet
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
if (wallet.chain_type !== "solana") {
|
|
1735
|
+
throw new RemoteSigningError(
|
|
1736
|
+
PROVIDER2,
|
|
1737
|
+
`wallet ${config.walletId} is ${String(
|
|
1738
|
+
wallet.chain_type
|
|
1739
|
+
)}, expected solana`
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
975
1742
|
return {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1743
|
+
provider: PROVIDER2,
|
|
1744
|
+
walletAddress,
|
|
1745
|
+
async signMessage(message) {
|
|
1746
|
+
const data = await rpc2({
|
|
1747
|
+
chain_type: "solana",
|
|
1748
|
+
method: "signMessage",
|
|
1749
|
+
params: {
|
|
1750
|
+
message: Buffer.from(message).toString("base64"),
|
|
1751
|
+
encoding: "base64"
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
const signature = data.signature;
|
|
1755
|
+
if (typeof signature !== "string") {
|
|
1756
|
+
throw new RemoteSigningError(
|
|
1757
|
+
PROVIDER2,
|
|
1758
|
+
"signMessage returned no signature",
|
|
1759
|
+
data
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
return verifiedEd25519Signature({
|
|
1763
|
+
provider: PROVIDER2,
|
|
1764
|
+
encodedSignature: signature,
|
|
1765
|
+
message,
|
|
1766
|
+
walletAddress
|
|
1767
|
+
});
|
|
1768
|
+
},
|
|
1769
|
+
async signTransaction(serializedTransactionBase64) {
|
|
1770
|
+
const data = await rpc2({
|
|
1771
|
+
chain_type: "solana",
|
|
1772
|
+
method: "signTransaction",
|
|
1773
|
+
params: {
|
|
1774
|
+
transaction: serializedTransactionBase64,
|
|
1775
|
+
encoding: "base64"
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
const signedTransaction = data.signed_transaction;
|
|
1779
|
+
if (typeof signedTransaction !== "string") {
|
|
1780
|
+
throw new RemoteSigningError(
|
|
1781
|
+
PROVIDER2,
|
|
1782
|
+
"signTransaction returned no signed_transaction",
|
|
1783
|
+
data
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
return signedTransaction;
|
|
1787
|
+
}
|
|
979
1788
|
};
|
|
980
1789
|
}
|
|
981
1790
|
|
|
982
|
-
// ../../src/client/
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1791
|
+
// ../../src/client/signer-env.ts
|
|
1792
|
+
async function agentWalletSignerFromEnv(env = process.env) {
|
|
1793
|
+
const nonEmpty = (value) => {
|
|
1794
|
+
const trimmed = value?.trim();
|
|
1795
|
+
return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
|
|
1796
|
+
};
|
|
1797
|
+
const provider = nonEmpty(env.SUBLY_SIGNER_PROVIDER)?.toLowerCase() ?? "local";
|
|
1798
|
+
const pickVar = (name) => nonEmpty(env[`SUBLY_${name}`]) ?? nonEmpty(env[name]);
|
|
1799
|
+
const requireVar = (name) => {
|
|
1800
|
+
const value = pickVar(name);
|
|
1801
|
+
if (value === void 0) {
|
|
1802
|
+
throw new Error(
|
|
1803
|
+
`${name} (or SUBLY_${name}) is required for SUBLY_SIGNER_PROVIDER=${provider}`
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
return value;
|
|
1807
|
+
};
|
|
1808
|
+
if (provider === "local") {
|
|
1809
|
+
const localSecretKey = loadSecretKeyBytes({
|
|
1810
|
+
base58Secret: env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
1811
|
+
jsonFilePath: env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
1812
|
+
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
1813
|
+
});
|
|
1814
|
+
return {
|
|
1815
|
+
provider,
|
|
1816
|
+
signer: new LocalKeypairAgentWalletSigner(
|
|
1817
|
+
await createKeyPairSignerFromBytes2(localSecretKey)
|
|
1818
|
+
),
|
|
1819
|
+
localSecretKey
|
|
1820
|
+
};
|
|
990
1821
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const post = async (step, path, body) => {
|
|
998
|
-
const url = `${baseUrl}${path}`;
|
|
999
|
-
const serialized = JSON.stringify(body);
|
|
1000
|
-
const response = await fetchImpl(url, {
|
|
1001
|
-
method: "POST",
|
|
1002
|
-
headers: {
|
|
1003
|
-
...await walletAuthHeaders({
|
|
1004
|
-
signer: params.signer,
|
|
1005
|
-
method: "POST",
|
|
1006
|
-
url,
|
|
1007
|
-
body: serialized
|
|
1008
|
-
}),
|
|
1009
|
-
"content-type": "application/json"
|
|
1010
|
-
},
|
|
1011
|
-
body: serialized
|
|
1822
|
+
if (provider === "circle") {
|
|
1823
|
+
const transport = await createCircleSignerTransport({
|
|
1824
|
+
apiKey: requireVar("CIRCLE_API_KEY"),
|
|
1825
|
+
entitySecret: requireVar("CIRCLE_ENTITY_SECRET"),
|
|
1826
|
+
walletId: requireVar("CIRCLE_WALLET_ID"),
|
|
1827
|
+
baseUrl: pickVar("CIRCLE_BASE_URL")
|
|
1012
1828
|
});
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1829
|
+
return { provider, signer: new RemoteAgentWalletSigner(transport), transport };
|
|
1830
|
+
}
|
|
1831
|
+
if (provider === "privy") {
|
|
1832
|
+
const transport = await createPrivySignerTransport({
|
|
1833
|
+
appId: requireVar("PRIVY_APP_ID"),
|
|
1834
|
+
appSecret: requireVar("PRIVY_APP_SECRET"),
|
|
1835
|
+
walletId: requireVar("PRIVY_WALLET_ID"),
|
|
1836
|
+
authorizationPrivateKey: pickVar("PRIVY_AUTHORIZATION_KEY"),
|
|
1837
|
+
baseUrl: pickVar("PRIVY_BASE_URL")
|
|
1838
|
+
});
|
|
1839
|
+
return { provider, signer: new RemoteAgentWalletSigner(transport), transport };
|
|
1840
|
+
}
|
|
1841
|
+
throw new Error(
|
|
1842
|
+
`unknown SUBLY_SIGNER_PROVIDER "${provider}" (expected local, circle, or privy)`
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// ../../src/client/withdrawal-preview.ts
|
|
1847
|
+
var ROUNDING_RAW_USDC = 10n;
|
|
1848
|
+
async function assertWithdrawalPreview(input) {
|
|
1849
|
+
const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
|
|
1850
|
+
const simulation = await input.rpc.simulateTransaction(
|
|
1851
|
+
input.serializedTransaction,
|
|
1852
|
+
{
|
|
1853
|
+
encoding: "base64",
|
|
1854
|
+
commitment: "confirmed",
|
|
1855
|
+
sigVerify: false,
|
|
1856
|
+
replaceRecentBlockhash: false,
|
|
1857
|
+
innerInstructions: true
|
|
1858
|
+
}
|
|
1859
|
+
).send({ abortSignal: AbortSignal.timeout(15e3) });
|
|
1860
|
+
if (simulation.value.err !== null) {
|
|
1861
|
+
throw new Error("Withdrawal preview failed on the client RPC; no transaction was signed. Check liquidity, RPC and blockhash, then prepare again.");
|
|
1862
|
+
}
|
|
1863
|
+
let received = 0n;
|
|
1864
|
+
for (const group of simulation.value.innerInstructions ?? []) {
|
|
1865
|
+
for (const instruction of group.instructions) {
|
|
1866
|
+
if (!("parsed" in instruction) || instruction.programId !== SPL_TOKEN_PROGRAM_ID) continue;
|
|
1867
|
+
const parsed = instruction.parsed;
|
|
1868
|
+
if (parsed.type !== "transfer" && parsed.type !== "transferChecked") continue;
|
|
1869
|
+
const info = parsed.info;
|
|
1870
|
+
if (!info || info.destination !== destination && info.source !== destination) continue;
|
|
1871
|
+
const raw = parsed.type === "transferChecked" ? info.tokenAmount?.amount : info.amount;
|
|
1872
|
+
if (typeof raw !== "string" || !/^\d+$/.test(raw)) {
|
|
1873
|
+
throw new Error("Withdrawal preview returned an invalid token amount");
|
|
1019
1874
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
detail
|
|
1024
|
-
);
|
|
1875
|
+
const amount = BigInt(raw);
|
|
1876
|
+
if (info.destination === destination) received += amount;
|
|
1877
|
+
if (info.source === destination) received -= amount;
|
|
1025
1878
|
}
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
signingPolicyId: SELF_SERVE_POLICY_ID,
|
|
1031
|
-
signingMode: "non_interactive",
|
|
1032
|
-
signerValidationMode: params.signer.validationMode,
|
|
1033
|
-
signerProvider: "local-keypair",
|
|
1034
|
-
activateForPayments: true
|
|
1035
|
-
});
|
|
1036
|
-
await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
|
|
1879
|
+
}
|
|
1880
|
+
if (received <= 0n || received > input.amountRawUsdc + ROUNDING_RAW_USDC || received < input.amountRawUsdc - ROUNDING_RAW_USDC) {
|
|
1881
|
+
throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
|
|
1882
|
+
}
|
|
1037
1883
|
}
|
|
1038
1884
|
|
|
1039
1885
|
// ../../src/client/lookup-tables.ts
|
|
@@ -1092,6 +1938,8 @@ var VaultFlowClientError = class extends Error {
|
|
|
1092
1938
|
errorDetails;
|
|
1093
1939
|
};
|
|
1094
1940
|
var VaultFlowClient = class {
|
|
1941
|
+
vault;
|
|
1942
|
+
rpc;
|
|
1095
1943
|
baseUrl;
|
|
1096
1944
|
signer;
|
|
1097
1945
|
fetchImpl;
|
|
@@ -1099,6 +1947,11 @@ var VaultFlowClient = class {
|
|
|
1099
1947
|
pollTimeoutMs;
|
|
1100
1948
|
pollIntervalMs;
|
|
1101
1949
|
constructor(config) {
|
|
1950
|
+
this.rpc = config.rpc;
|
|
1951
|
+
this.vault = config.vault ?? config.signer.vault ?? SUBLY_VAULT;
|
|
1952
|
+
if (config.signer.vault && config.signer.vault.address !== this.vault.address) {
|
|
1953
|
+
throw new Error("Vault flow client and signer must select the same vault");
|
|
1954
|
+
}
|
|
1102
1955
|
this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
|
|
1103
1956
|
this.signer = config.signer;
|
|
1104
1957
|
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
@@ -1120,6 +1973,7 @@ var VaultFlowClient = class {
|
|
|
1120
1973
|
try {
|
|
1121
1974
|
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
1122
1975
|
wallet: this.signer.walletAddress,
|
|
1976
|
+
vault: this.vault.address,
|
|
1123
1977
|
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1124
1978
|
...approvalId2 === void 0 ? {} : { approvalId: approvalId2 }
|
|
1125
1979
|
});
|
|
@@ -1133,10 +1987,14 @@ var VaultFlowClient = class {
|
|
|
1133
1987
|
}
|
|
1134
1988
|
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
1135
1989
|
wallet: this.signer.walletAddress,
|
|
1990
|
+
vault: this.vault.address,
|
|
1136
1991
|
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1137
1992
|
approvalId: approvalId2
|
|
1138
1993
|
});
|
|
1139
1994
|
}
|
|
1995
|
+
if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.signingIntent.amountRawUsdc !== input.amountRawUsdc.toString()) {
|
|
1996
|
+
throw new VaultFlowClientError("prepare", "Prepared deposit differs from the requested wallet, vault or amount");
|
|
1997
|
+
}
|
|
1140
1998
|
const signed = await this.signer.signDeposit({
|
|
1141
1999
|
intent: prepared.signingIntent,
|
|
1142
2000
|
serializedTransaction: prepared.serializedTransaction,
|
|
@@ -1174,12 +2032,23 @@ var VaultFlowClient = class {
|
|
|
1174
2032
|
"/v1/withdrawals/prepare",
|
|
1175
2033
|
{
|
|
1176
2034
|
wallet: this.signer.walletAddress,
|
|
2035
|
+
vault: this.vault.address,
|
|
1177
2036
|
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1178
2037
|
...input.purpose === void 0 ? {} : { purpose: input.purpose },
|
|
1179
2038
|
...input.payment === void 0 ? {} : { payment: input.payment },
|
|
1180
2039
|
...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
|
|
1181
2040
|
}
|
|
1182
2041
|
);
|
|
2042
|
+
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) {
|
|
2043
|
+
throw new VaultFlowClientError("prepare", "Prepared withdrawal differs from the requested operation");
|
|
2044
|
+
}
|
|
2045
|
+
await assertWithdrawalPreview({
|
|
2046
|
+
rpc: this.rpc,
|
|
2047
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
2048
|
+
wallet: this.signer.walletAddress,
|
|
2049
|
+
vault: this.vault,
|
|
2050
|
+
amountRawUsdc: input.amountRawUsdc
|
|
2051
|
+
});
|
|
1183
2052
|
const signed = await this.signer.signWithdrawal({
|
|
1184
2053
|
intent: prepared.signingIntent,
|
|
1185
2054
|
serializedTransaction: prepared.serializedTransaction,
|
|
@@ -1217,12 +2086,12 @@ var VaultFlowClient = class {
|
|
|
1217
2086
|
await this.postJson(
|
|
1218
2087
|
"sync",
|
|
1219
2088
|
`/v1/wallets/${this.signer.walletAddress}/sync`,
|
|
1220
|
-
{ source: "chain" }
|
|
2089
|
+
{ source: "chain", vault: this.vault.address }
|
|
1221
2090
|
);
|
|
1222
2091
|
} catch {
|
|
1223
2092
|
}
|
|
1224
2093
|
}
|
|
1225
|
-
const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
|
|
2094
|
+
const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget?vault=${this.vault.address}`;
|
|
1226
2095
|
const response = await this.fetchImpl(url, {
|
|
1227
2096
|
headers: await walletAuthHeaders({
|
|
1228
2097
|
signer: this.signer,
|
|
@@ -1248,8 +2117,12 @@ var VaultFlowClient = class {
|
|
|
1248
2117
|
);
|
|
1249
2118
|
}
|
|
1250
2119
|
const body = parsed;
|
|
2120
|
+
if (body.position?.vault !== void 0 && body.position.vault !== this.vault.address) {
|
|
2121
|
+
throw new VaultFlowClientError("budget", "Relayer returned the budget for a different vault");
|
|
2122
|
+
}
|
|
1251
2123
|
return {
|
|
1252
2124
|
wallet: this.signer.walletAddress,
|
|
2125
|
+
vault: this.vault.address,
|
|
1253
2126
|
principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
|
|
1254
2127
|
positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
|
|
1255
2128
|
grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
|
|
@@ -1267,7 +2140,7 @@ var VaultFlowClient = class {
|
|
|
1267
2140
|
/** Wallet's approvals as the relayer sees them (optionally by status). */
|
|
1268
2141
|
async listApprovals(status) {
|
|
1269
2142
|
const body = await this.getJson(
|
|
1270
|
-
`/v1/wallets/${this.signer.walletAddress}/approvals${status === void 0 ? "" :
|
|
2143
|
+
`/v1/wallets/${this.signer.walletAddress}/approvals${`?vault=${this.vault.address}${status === void 0 ? "" : `&status=${encodeURIComponent(status)}`}`}`
|
|
1271
2144
|
);
|
|
1272
2145
|
return body.approvals ?? [];
|
|
1273
2146
|
}
|
|
@@ -1276,16 +2149,21 @@ var VaultFlowClient = class {
|
|
|
1276
2149
|
* policy + initial deposit). Paste `setupUrl` into the chat verbatim.
|
|
1277
2150
|
*/
|
|
1278
2151
|
async createSetupSession(input) {
|
|
1279
|
-
|
|
2152
|
+
const session = await this.postJson(
|
|
1280
2153
|
"prepare",
|
|
1281
2154
|
`/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
|
|
1282
2155
|
{
|
|
2156
|
+
vault: this.vault.address,
|
|
1283
2157
|
...input.policy === void 0 ? {} : { policy: input.policy },
|
|
1284
2158
|
...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
|
|
1285
2159
|
...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
|
|
1286
2160
|
...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
|
|
1287
2161
|
}
|
|
1288
2162
|
);
|
|
2163
|
+
if (session.vault !== this.vault.address || session.wallet !== this.signer.walletAddress) {
|
|
2164
|
+
throw new VaultFlowClientError("prepare", "Relayer returned a setup session for a different wallet or vault");
|
|
2165
|
+
}
|
|
2166
|
+
return session;
|
|
1289
2167
|
}
|
|
1290
2168
|
/** Polls a setup session (public capability URL — no auth needed). */
|
|
1291
2169
|
async getSetupSession(sessionId) {
|
|
@@ -1432,31 +2310,6 @@ function parseRelayerError(text) {
|
|
|
1432
2310
|
}
|
|
1433
2311
|
}
|
|
1434
2312
|
|
|
1435
|
-
// ../../src/solana/keys.ts
|
|
1436
|
-
import { readFileSync } from "node:fs";
|
|
1437
|
-
import bs586 from "bs58";
|
|
1438
|
-
import {
|
|
1439
|
-
createKeyPairSignerFromBytes
|
|
1440
|
-
} from "@solana/kit";
|
|
1441
|
-
async function loadKeyPairSigner(params) {
|
|
1442
|
-
const { base58Secret, jsonFilePath, label } = params;
|
|
1443
|
-
if (base58Secret !== void 0 && base58Secret.length > 0) {
|
|
1444
|
-
const bytes = bs586.decode(base58Secret);
|
|
1445
|
-
if (bytes.length !== 64) {
|
|
1446
|
-
throw new Error(`${label} base58 secret must decode to 64 bytes`);
|
|
1447
|
-
}
|
|
1448
|
-
return createKeyPairSignerFromBytes(bytes);
|
|
1449
|
-
}
|
|
1450
|
-
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
1451
|
-
const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
|
|
1452
|
-
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
1453
|
-
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
1454
|
-
}
|
|
1455
|
-
return createKeyPairSignerFromBytes(Uint8Array.from(raw));
|
|
1456
|
-
}
|
|
1457
|
-
throw new Error(`${label} keypair is not configured`);
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
2313
|
// ../../src/solana/rpc.ts
|
|
1461
2314
|
import { createSolanaRpc } from "@solana/kit";
|
|
1462
2315
|
function createRpc(url) {
|
|
@@ -1487,12 +2340,7 @@ var approvalId = process.argv[3];
|
|
|
1487
2340
|
if (approvalId !== void 0 && !/^apr_[0-9a-f]+$/i.test(approvalId)) {
|
|
1488
2341
|
fail(`unrecognized argument: ${approvalId} (expected apr_<approvalId>)`);
|
|
1489
2342
|
}
|
|
1490
|
-
var
|
|
1491
|
-
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
1492
|
-
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
1493
|
-
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
1494
|
-
});
|
|
1495
|
-
var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
|
|
2343
|
+
var { signer } = await agentWalletSignerFromEnv();
|
|
1496
2344
|
var rpc = createRpc(
|
|
1497
2345
|
process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com"
|
|
1498
2346
|
);
|