@subly_fi/pay 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -152
- package/dist/budget.js +2267 -0
- package/dist/cli.js +37 -20
- package/dist/deposit.js +243 -58
- package/dist/doctor.js +188 -0
- package/dist/mcp-server.js +1537 -1227
- package/dist/pay.js +437 -225
- package/dist/setup-link.js +238 -55
- package/dist/vaults.js +129 -0
- package/dist/withdraw.js +238 -55
- package/package.json +7 -5
package/dist/withdraw.js
CHANGED
|
@@ -26,6 +26,144 @@ function loadSecretKeyBytes(params) {
|
|
|
26
26
|
throw new Error(`${label} keypair is not configured`);
|
|
27
27
|
}
|
|
28
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
|
+
|
|
29
167
|
// ../../src/client/agent-wallet-signer.ts
|
|
30
168
|
import { signBytes } from "@solana/kit";
|
|
31
169
|
import bs586 from "bs58";
|
|
@@ -141,10 +279,10 @@ function ed25519PublicKeyBytes(provider, walletAddress) {
|
|
|
141
279
|
return bytes;
|
|
142
280
|
}
|
|
143
281
|
function verifiedEd25519Signature(params) {
|
|
144
|
-
const
|
|
282
|
+
const publicKey2 = ed25519PublicKeyBytes(params.provider, params.walletAddress);
|
|
145
283
|
const encoded = params.encodedSignature.trim();
|
|
146
284
|
for (const candidate of decodeSignatureCandidates(encoded)) {
|
|
147
|
-
if (nacl.sign.detached.verify(params.message, candidate,
|
|
285
|
+
if (nacl.sign.detached.verify(params.message, candidate, publicKey2)) {
|
|
148
286
|
return candidate;
|
|
149
287
|
}
|
|
150
288
|
}
|
|
@@ -198,8 +336,8 @@ async function requestVerifiedTransactionSignature(params) {
|
|
|
198
336
|
`signed transaction is missing the signature for ${transport.walletAddress}`
|
|
199
337
|
);
|
|
200
338
|
}
|
|
201
|
-
const
|
|
202
|
-
if (!nacl.sign.detached.verify(params.messageBytes, signature,
|
|
339
|
+
const publicKey2 = params.publicKey ?? ed25519PublicKeyBytes(transport.provider, transport.walletAddress);
|
|
340
|
+
if (!nacl.sign.detached.verify(params.messageBytes, signature, publicKey2)) {
|
|
203
341
|
throw new RemoteSigningError(
|
|
204
342
|
transport.provider,
|
|
205
343
|
"returned signature does not verify over the requested transaction"
|
|
@@ -250,38 +388,6 @@ async function providerJsonRequest(params) {
|
|
|
250
388
|
import bs585 from "bs58";
|
|
251
389
|
import { getCompiledTransactionMessageDecoder } from "@solana/kit";
|
|
252
390
|
|
|
253
|
-
// ../../src/config/constants.ts
|
|
254
|
-
var PAYMENT_SCHEME = "subly-yield-exact";
|
|
255
|
-
var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
256
|
-
var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
257
|
-
var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
258
|
-
var envOr = (name, fallback) => {
|
|
259
|
-
const value = process.env[name]?.trim();
|
|
260
|
-
return value ? value : fallback;
|
|
261
|
-
};
|
|
262
|
-
var SUBLY_VAULT = {
|
|
263
|
-
name: "Subly USDC Payment Vault Alpha",
|
|
264
|
-
address: envOr(
|
|
265
|
-
"SUBLY_VAULT_ADDRESS",
|
|
266
|
-
"5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr"
|
|
267
|
-
),
|
|
268
|
-
programId: "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd",
|
|
269
|
-
usdcMint: envOr(
|
|
270
|
-
"SUBLY_VAULT_USDC_MINT",
|
|
271
|
-
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
272
|
-
),
|
|
273
|
-
shareMint: envOr(
|
|
274
|
-
"SUBLY_VAULT_SHARE_MINT",
|
|
275
|
-
"7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a"
|
|
276
|
-
),
|
|
277
|
-
lookupTable: "7UbXhDnpK7WVnwsfivzQRENoqKqAULQ5s19gS1xJrQEo",
|
|
278
|
-
farm: envOr(
|
|
279
|
-
"SUBLY_VAULT_FARM",
|
|
280
|
-
"E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
|
|
281
|
-
)
|
|
282
|
-
};
|
|
283
|
-
var USDC_DECIMALS = 6;
|
|
284
|
-
|
|
285
391
|
// ../../src/domain/request-binding.ts
|
|
286
392
|
function computeRequestBindingHash(fields) {
|
|
287
393
|
return hashStableJson({
|
|
@@ -531,16 +637,16 @@ function validatePaymentIntentTransaction(params) {
|
|
|
531
637
|
if (intent.network !== SOLANA_MAINNET_NETWORK) {
|
|
532
638
|
reject("network_mismatch", "Unsupported network");
|
|
533
639
|
}
|
|
534
|
-
if (intent.vault !==
|
|
640
|
+
if (intent.vault !== policy.vault.address) {
|
|
535
641
|
reject("vault_mismatch", "Unsupported vault");
|
|
536
642
|
}
|
|
537
|
-
if (intent.shareMint !==
|
|
643
|
+
if (intent.shareMint !== policy.vault.shareMint) {
|
|
538
644
|
reject("share_mint_mismatch", "Unsupported share mint");
|
|
539
645
|
}
|
|
540
|
-
if (intent.farm !==
|
|
646
|
+
if (intent.farm !== policy.vault.farm) {
|
|
541
647
|
reject("farm_mismatch", "Unsupported Kamino farm");
|
|
542
648
|
}
|
|
543
|
-
if (intent.asset !==
|
|
649
|
+
if (intent.asset !== policy.vault.usdcMint) {
|
|
544
650
|
reject("asset_mismatch", "Only USDC payments are supported");
|
|
545
651
|
}
|
|
546
652
|
if (intent.memo !== intent.paymentId) {
|
|
@@ -656,7 +762,7 @@ function validateDepositIntentTransaction(params) {
|
|
|
656
762
|
if (new Date(intent.expiresAt).getTime() <= now) {
|
|
657
763
|
reject("expired", "Deposit intent has expired");
|
|
658
764
|
}
|
|
659
|
-
assertVaultIntentTargets(intent);
|
|
765
|
+
assertVaultIntentTargets(intent, policy.vault);
|
|
660
766
|
const decoded = decodeIntentTransaction({
|
|
661
767
|
serializedTransaction: params.serializedTransaction,
|
|
662
768
|
...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
|
|
@@ -679,6 +785,9 @@ function validateDepositIntentTransaction(params) {
|
|
|
679
785
|
case MEMO_PROGRAM_ID:
|
|
680
786
|
break;
|
|
681
787
|
case KVAULT_PROGRAM_ID: {
|
|
788
|
+
if (sawDeposit) {
|
|
789
|
+
reject("duplicate_deposit", "A deposit intent authorizes exactly one KVault deposit");
|
|
790
|
+
}
|
|
682
791
|
if (!bytesStartWith(ix.data, KVAULT_DEPOSIT_DISCRIMINATOR)) {
|
|
683
792
|
reject("unexpected_instruction", "Unexpected KVault instruction in deposit");
|
|
684
793
|
}
|
|
@@ -729,7 +838,7 @@ function validateWithdrawalIntentTransaction(params) {
|
|
|
729
838
|
if (new Date(intent.expiresAt).getTime() <= now) {
|
|
730
839
|
reject("expired", "Withdrawal intent has expired");
|
|
731
840
|
}
|
|
732
|
-
assertVaultIntentTargets(intent);
|
|
841
|
+
assertVaultIntentTargets(intent, policy.vault);
|
|
733
842
|
const expectedDestination = deriveAssociatedTokenAddress({
|
|
734
843
|
owner: intent.wallet,
|
|
735
844
|
mint: intent.asset
|
|
@@ -827,22 +936,23 @@ function validateWithdrawalIntentTransaction(params) {
|
|
|
827
936
|
);
|
|
828
937
|
}
|
|
829
938
|
}
|
|
830
|
-
function assertVaultIntentTargets(intent) {
|
|
831
|
-
if (intent.vault !==
|
|
939
|
+
function assertVaultIntentTargets(intent, vault) {
|
|
940
|
+
if (intent.vault !== vault.address) {
|
|
832
941
|
reject("vault_mismatch", "Unsupported vault");
|
|
833
942
|
}
|
|
834
|
-
if (intent.shareMint !==
|
|
943
|
+
if (intent.shareMint !== vault.shareMint) {
|
|
835
944
|
reject("share_mint_mismatch", "Unsupported share mint");
|
|
836
945
|
}
|
|
837
|
-
if (intent.farm !==
|
|
946
|
+
if (intent.farm !== vault.farm) {
|
|
838
947
|
reject("farm_mismatch", "Unsupported Kamino farm");
|
|
839
948
|
}
|
|
840
|
-
if (intent.asset !==
|
|
949
|
+
if (intent.asset !== vault.usdcMint) {
|
|
841
950
|
reject("asset_mismatch", "Only USDC is supported");
|
|
842
951
|
}
|
|
843
952
|
}
|
|
844
953
|
function resolveIntentValidationPolicy(policy) {
|
|
845
954
|
const resolved = {
|
|
955
|
+
vault: policy?.vault ?? SUBLY_VAULT,
|
|
846
956
|
maxComputeUnitLimit: policy?.maxComputeUnitLimit ?? DEFAULT_MAX_COMPUTE_UNIT_LIMIT,
|
|
847
957
|
maxComputeUnitPriceMicroLamports: policy?.maxComputeUnitPriceMicroLamports ?? DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
|
|
848
958
|
maxTemporaryAccountLamports: policy?.maxTemporaryAccountLamports ?? MAX_TEMP_ACCOUNT_LAMPORTS
|
|
@@ -1194,10 +1304,12 @@ function readShortVec(bytes, startOffset) {
|
|
|
1194
1304
|
|
|
1195
1305
|
// ../../src/client/agent-wallet-signer.ts
|
|
1196
1306
|
var IntentValidatingAgentWalletSigner = class {
|
|
1307
|
+
vault;
|
|
1197
1308
|
validationMode = "structured_intent_transaction";
|
|
1198
1309
|
validationPolicy;
|
|
1199
1310
|
constructor(validationPolicy) {
|
|
1200
|
-
this.
|
|
1311
|
+
this.vault = Object.freeze({ ...validationPolicy?.vault ?? SUBLY_VAULT });
|
|
1312
|
+
this.validationPolicy = { ...validationPolicy, vault: this.vault };
|
|
1201
1313
|
}
|
|
1202
1314
|
async signPayment(params) {
|
|
1203
1315
|
this.assertIntentWallet(params.intent.wallet);
|
|
@@ -1355,15 +1467,15 @@ async function createCircleSignerTransport(config) {
|
|
|
1355
1467
|
const entitySecretCiphertext = async () => {
|
|
1356
1468
|
if (entityPublicKey === null) {
|
|
1357
1469
|
const data = await request("GET", "/v1/w3s/config/entity/publicKey");
|
|
1358
|
-
const
|
|
1359
|
-
if (typeof
|
|
1470
|
+
const publicKey2 = data.publicKey;
|
|
1471
|
+
if (typeof publicKey2 !== "string") {
|
|
1360
1472
|
throw new RemoteSigningError(
|
|
1361
1473
|
PROVIDER,
|
|
1362
1474
|
"entity public key response has no publicKey",
|
|
1363
1475
|
data
|
|
1364
1476
|
);
|
|
1365
1477
|
}
|
|
1366
|
-
entityPublicKey = createPublicKey(
|
|
1478
|
+
entityPublicKey = createPublicKey(publicKey2);
|
|
1367
1479
|
}
|
|
1368
1480
|
return publicEncrypt(
|
|
1369
1481
|
{
|
|
@@ -1635,6 +1747,45 @@ async function agentWalletSignerFromEnv(env = process.env) {
|
|
|
1635
1747
|
);
|
|
1636
1748
|
}
|
|
1637
1749
|
|
|
1750
|
+
// ../../src/client/withdrawal-preview.ts
|
|
1751
|
+
var ROUNDING_RAW_USDC = 10n;
|
|
1752
|
+
async function assertWithdrawalPreview(input) {
|
|
1753
|
+
const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
|
|
1754
|
+
const simulation = await input.rpc.simulateTransaction(
|
|
1755
|
+
input.serializedTransaction,
|
|
1756
|
+
{
|
|
1757
|
+
encoding: "base64",
|
|
1758
|
+
commitment: "confirmed",
|
|
1759
|
+
sigVerify: false,
|
|
1760
|
+
replaceRecentBlockhash: false,
|
|
1761
|
+
innerInstructions: true
|
|
1762
|
+
}
|
|
1763
|
+
).send({ abortSignal: AbortSignal.timeout(15e3) });
|
|
1764
|
+
if (simulation.value.err !== null) {
|
|
1765
|
+
throw new Error("Withdrawal preview failed on the client RPC; no transaction was signed. Check liquidity, RPC and blockhash, then prepare again.");
|
|
1766
|
+
}
|
|
1767
|
+
let received = 0n;
|
|
1768
|
+
for (const group of simulation.value.innerInstructions ?? []) {
|
|
1769
|
+
for (const instruction of group.instructions) {
|
|
1770
|
+
if (!("parsed" in instruction) || instruction.programId !== SPL_TOKEN_PROGRAM_ID) continue;
|
|
1771
|
+
const parsed = instruction.parsed;
|
|
1772
|
+
if (parsed.type !== "transfer" && parsed.type !== "transferChecked") continue;
|
|
1773
|
+
const info = parsed.info;
|
|
1774
|
+
if (!info || info.destination !== destination && info.source !== destination) continue;
|
|
1775
|
+
const raw = parsed.type === "transferChecked" ? info.tokenAmount?.amount : info.amount;
|
|
1776
|
+
if (typeof raw !== "string" || !/^\d+$/.test(raw)) {
|
|
1777
|
+
throw new Error("Withdrawal preview returned an invalid token amount");
|
|
1778
|
+
}
|
|
1779
|
+
const amount = BigInt(raw);
|
|
1780
|
+
if (info.destination === destination) received += amount;
|
|
1781
|
+
if (info.source === destination) received -= amount;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (received <= 0n || received > input.amountRawUsdc + ROUNDING_RAW_USDC || received < input.amountRawUsdc - ROUNDING_RAW_USDC) {
|
|
1785
|
+
throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1638
1789
|
// ../../src/client/lookup-tables.ts
|
|
1639
1790
|
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
|
|
1640
1791
|
import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
|
|
@@ -1728,6 +1879,8 @@ var VaultFlowClientError = class extends Error {
|
|
|
1728
1879
|
errorDetails;
|
|
1729
1880
|
};
|
|
1730
1881
|
var VaultFlowClient = class {
|
|
1882
|
+
vault;
|
|
1883
|
+
rpc;
|
|
1731
1884
|
baseUrl;
|
|
1732
1885
|
signer;
|
|
1733
1886
|
fetchImpl;
|
|
@@ -1735,6 +1888,11 @@ var VaultFlowClient = class {
|
|
|
1735
1888
|
pollTimeoutMs;
|
|
1736
1889
|
pollIntervalMs;
|
|
1737
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
|
+
}
|
|
1738
1896
|
this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
|
|
1739
1897
|
this.signer = config.signer;
|
|
1740
1898
|
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
@@ -1756,6 +1914,7 @@ var VaultFlowClient = class {
|
|
|
1756
1914
|
try {
|
|
1757
1915
|
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
1758
1916
|
wallet: this.signer.walletAddress,
|
|
1917
|
+
vault: this.vault.address,
|
|
1759
1918
|
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1760
1919
|
...approvalId2 === void 0 ? {} : { approvalId: approvalId2 }
|
|
1761
1920
|
});
|
|
@@ -1769,10 +1928,14 @@ var VaultFlowClient = class {
|
|
|
1769
1928
|
}
|
|
1770
1929
|
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
1771
1930
|
wallet: this.signer.walletAddress,
|
|
1931
|
+
vault: this.vault.address,
|
|
1772
1932
|
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1773
1933
|
approvalId: approvalId2
|
|
1774
1934
|
});
|
|
1775
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
|
+
}
|
|
1776
1939
|
const signed = await this.signer.signDeposit({
|
|
1777
1940
|
intent: prepared.signingIntent,
|
|
1778
1941
|
serializedTransaction: prepared.serializedTransaction,
|
|
@@ -1810,12 +1973,23 @@ var VaultFlowClient = class {
|
|
|
1810
1973
|
"/v1/withdrawals/prepare",
|
|
1811
1974
|
{
|
|
1812
1975
|
wallet: this.signer.walletAddress,
|
|
1976
|
+
vault: this.vault.address,
|
|
1813
1977
|
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1814
1978
|
...input.purpose === void 0 ? {} : { purpose: input.purpose },
|
|
1815
1979
|
...input.payment === void 0 ? {} : { payment: input.payment },
|
|
1816
1980
|
...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
|
|
1817
1981
|
}
|
|
1818
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
|
+
});
|
|
1819
1993
|
const signed = await this.signer.signWithdrawal({
|
|
1820
1994
|
intent: prepared.signingIntent,
|
|
1821
1995
|
serializedTransaction: prepared.serializedTransaction,
|
|
@@ -1853,12 +2027,12 @@ var VaultFlowClient = class {
|
|
|
1853
2027
|
await this.postJson(
|
|
1854
2028
|
"sync",
|
|
1855
2029
|
`/v1/wallets/${this.signer.walletAddress}/sync`,
|
|
1856
|
-
{ source: "chain" }
|
|
2030
|
+
{ source: "chain", vault: this.vault.address }
|
|
1857
2031
|
);
|
|
1858
2032
|
} catch {
|
|
1859
2033
|
}
|
|
1860
2034
|
}
|
|
1861
|
-
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}`;
|
|
1862
2036
|
const response = await this.fetchImpl(url, {
|
|
1863
2037
|
headers: await walletAuthHeaders({
|
|
1864
2038
|
signer: this.signer,
|
|
@@ -1884,8 +2058,12 @@ var VaultFlowClient = class {
|
|
|
1884
2058
|
);
|
|
1885
2059
|
}
|
|
1886
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
|
+
}
|
|
1887
2064
|
return {
|
|
1888
2065
|
wallet: this.signer.walletAddress,
|
|
2066
|
+
vault: this.vault.address,
|
|
1889
2067
|
principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
|
|
1890
2068
|
positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
|
|
1891
2069
|
grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
|
|
@@ -1903,7 +2081,7 @@ var VaultFlowClient = class {
|
|
|
1903
2081
|
/** Wallet's approvals as the relayer sees them (optionally by status). */
|
|
1904
2082
|
async listApprovals(status) {
|
|
1905
2083
|
const body = await this.getJson(
|
|
1906
|
-
`/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)}`}`}`
|
|
1907
2085
|
);
|
|
1908
2086
|
return body.approvals ?? [];
|
|
1909
2087
|
}
|
|
@@ -1912,16 +2090,21 @@ var VaultFlowClient = class {
|
|
|
1912
2090
|
* policy + initial deposit). Paste `setupUrl` into the chat verbatim.
|
|
1913
2091
|
*/
|
|
1914
2092
|
async createSetupSession(input) {
|
|
1915
|
-
|
|
2093
|
+
const session = await this.postJson(
|
|
1916
2094
|
"prepare",
|
|
1917
2095
|
`/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
|
|
1918
2096
|
{
|
|
2097
|
+
vault: this.vault.address,
|
|
1919
2098
|
...input.policy === void 0 ? {} : { policy: input.policy },
|
|
1920
2099
|
...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
|
|
1921
2100
|
...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
|
|
1922
2101
|
...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
|
|
1923
2102
|
}
|
|
1924
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;
|
|
1925
2108
|
}
|
|
1926
2109
|
/** Polls a setup session (public capability URL — no auth needed). */
|
|
1927
2110
|
async getSetupSession(sessionId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@subly_fi/pay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Subly client: pay compatible standard x402 Solana USDC exact APIs from Kamino vault yield — the seller needs no Subly integration. Ships an MCP server and a one-shot pay/deposit CLI; non-custodial (signs locally with your own key).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"x402",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"type": "module",
|
|
17
17
|
"engines": {
|
|
18
|
-
"node": ">=
|
|
18
|
+
"node": ">=24"
|
|
19
19
|
},
|
|
20
20
|
"bin": {
|
|
21
21
|
"pay": "dist/cli.js"
|
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "node build.mjs",
|
|
30
30
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
31
|
+
"pack:check": "npm pack --dry-run --json",
|
|
32
|
+
"prepack": "npm run build",
|
|
33
|
+
"check": "npm run typecheck && npm run build && npm run pack:check"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
},
|
|
55
56
|
"repository": {
|
|
56
57
|
"type": "git",
|
|
57
|
-
"url": "git+https://github.com/SublyFi/subly-payment-protocol.git"
|
|
58
|
+
"url": "git+https://github.com/SublyFi/subly-payment-protocol.git",
|
|
59
|
+
"directory": "packages/pay"
|
|
58
60
|
}
|
|
59
61
|
}
|