@subly_fi/pay 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,38 @@
1
+ // ../../src/client/signer-env.ts
2
+ import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
3
+
4
+ // ../../src/solana/keys.ts
5
+ import { readFileSync } from "node:fs";
6
+ import bs58 from "bs58";
7
+ import {
8
+ createKeyPairSignerFromBytes
9
+ } from "@solana/kit";
10
+ function loadSecretKeyBytes(params) {
11
+ const { base58Secret, jsonFilePath, label } = params;
12
+ if (base58Secret !== void 0 && base58Secret.length > 0) {
13
+ const bytes = bs58.decode(base58Secret);
14
+ if (bytes.length !== 64) {
15
+ throw new Error(`${label} base58 secret must decode to 64 bytes`);
16
+ }
17
+ return bytes;
18
+ }
19
+ if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
20
+ const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
21
+ if (!Array.isArray(raw) || raw.length !== 64) {
22
+ throw new Error(`${label} keypair file must be a 64-byte JSON array`);
23
+ }
24
+ return Uint8Array.from(raw);
25
+ }
26
+ throw new Error(`${label} keypair is not configured`);
27
+ }
28
+
1
29
  // ../../src/client/agent-wallet-signer.ts
2
30
  import { signBytes } from "@solana/kit";
3
- import bs584 from "bs58";
31
+ import bs586 from "bs58";
32
+ import nacl2 from "tweetnacl";
4
33
 
5
34
  // ../../src/solana/tx.ts
6
- import bs58 from "bs58";
35
+ import bs582 from "bs58";
7
36
  import {
8
37
  appendTransactionMessageInstructions,
9
38
  compileTransaction,
@@ -46,6 +75,24 @@ function hashStableJson(value) {
46
75
  function decodeSerializedTransaction(serializedBase64) {
47
76
  return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
48
77
  }
78
+ function attachExternalSignatureToTransaction(params) {
79
+ if (!(params.signer in params.transaction.signatures)) {
80
+ throw new Error(
81
+ `transaction does not expect a signature from ${params.signer}`
82
+ );
83
+ }
84
+ const transaction = Object.freeze({
85
+ ...params.transaction,
86
+ signatures: Object.freeze({
87
+ ...params.transaction.signatures,
88
+ [params.signer]: params.signature
89
+ })
90
+ });
91
+ return {
92
+ serializedBase64: getBase64EncodedWireTransaction(transaction),
93
+ transaction
94
+ };
95
+ }
49
96
  async function addSignaturesToSerializedTransaction(params) {
50
97
  const decoded = decodeSerializedTransaction(params.serializedBase64);
51
98
  const signed = await partiallySignTransaction(params.signers, decoded);
@@ -59,11 +106,148 @@ function signatureBase58ForSigner(transaction, signer2) {
59
106
  if (signature === null || signature === void 0) {
60
107
  return null;
61
108
  }
62
- return bs58.encode(signature);
109
+ return bs582.encode(signature);
63
110
  }
64
111
 
65
- // ../../src/client/transaction-intent-validator.ts
112
+ // ../../src/client/remote-signer-transport.ts
66
113
  import bs583 from "bs58";
114
+ import nacl from "tweetnacl";
115
+ var RemoteSigningError = class extends Error {
116
+ constructor(provider, message, detail = null) {
117
+ super(`[${provider}] ${message}`);
118
+ this.provider = provider;
119
+ this.detail = detail;
120
+ this.name = "RemoteSigningError";
121
+ }
122
+ provider;
123
+ detail;
124
+ };
125
+ function ed25519PublicKeyBytes(provider, walletAddress) {
126
+ let bytes;
127
+ try {
128
+ bytes = bs583.decode(walletAddress);
129
+ } catch {
130
+ throw new RemoteSigningError(
131
+ provider,
132
+ `wallet address ${walletAddress} is not base58`
133
+ );
134
+ }
135
+ if (bytes.length !== 32) {
136
+ throw new RemoteSigningError(
137
+ provider,
138
+ `wallet address ${walletAddress} is not a 32-byte ed25519 key`
139
+ );
140
+ }
141
+ return bytes;
142
+ }
143
+ function verifiedEd25519Signature(params) {
144
+ const publicKey = ed25519PublicKeyBytes(params.provider, params.walletAddress);
145
+ const encoded = params.encodedSignature.trim();
146
+ for (const candidate of decodeSignatureCandidates(encoded)) {
147
+ if (nacl.sign.detached.verify(params.message, candidate, publicKey)) {
148
+ return candidate;
149
+ }
150
+ }
151
+ throw new RemoteSigningError(
152
+ params.provider,
153
+ `signature did not verify for wallet ${params.walletAddress}`
154
+ );
155
+ }
156
+ function decodeSignatureCandidates(encoded) {
157
+ const candidates = [];
158
+ const hex = encoded.startsWith("0x") ? encoded.slice(2) : encoded;
159
+ if (/^[0-9a-fA-F]{128}$/.test(hex)) {
160
+ candidates.push(Uint8Array.from(Buffer.from(hex, "hex")));
161
+ }
162
+ try {
163
+ const fromBase58 = bs583.decode(encoded);
164
+ if (fromBase58.length === 64) {
165
+ candidates.push(fromBase58);
166
+ }
167
+ } catch {
168
+ }
169
+ if (/^[A-Za-z0-9+/=_-]+$/.test(encoded)) {
170
+ const fromBase64 = Uint8Array.from(
171
+ Buffer.from(encoded.replace(/-/g, "+").replace(/_/g, "/"), "base64")
172
+ );
173
+ if (fromBase64.length === 64) {
174
+ candidates.push(fromBase64);
175
+ }
176
+ }
177
+ return candidates;
178
+ }
179
+ async function requestVerifiedTransactionSignature(params) {
180
+ const { transport } = params;
181
+ const signedBase64 = await transport.signTransaction(
182
+ params.serializedTransactionBase64
183
+ );
184
+ let returned;
185
+ try {
186
+ returned = decodeSerializedTransaction(signedBase64);
187
+ } catch (error) {
188
+ throw new RemoteSigningError(
189
+ transport.provider,
190
+ "provider returned an undecodable signed transaction",
191
+ error
192
+ );
193
+ }
194
+ const signature = returned.signatures[transport.walletAddress] ?? null;
195
+ if (signature === null) {
196
+ throw new RemoteSigningError(
197
+ transport.provider,
198
+ `signed transaction is missing the signature for ${transport.walletAddress}`
199
+ );
200
+ }
201
+ const publicKey = params.publicKey ?? ed25519PublicKeyBytes(transport.provider, transport.walletAddress);
202
+ if (!nacl.sign.detached.verify(params.messageBytes, signature, publicKey)) {
203
+ throw new RemoteSigningError(
204
+ transport.provider,
205
+ "returned signature does not verify over the requested transaction"
206
+ );
207
+ }
208
+ return signature;
209
+ }
210
+ async function externallySignedAgentTransaction(params) {
211
+ const original = decodeSerializedTransaction(params.serializedTransaction);
212
+ const signature = await requestVerifiedTransactionSignature({
213
+ transport: params.transport,
214
+ serializedTransactionBase64: params.serializedTransaction,
215
+ messageBytes: original.messageBytes
216
+ });
217
+ const attached = attachExternalSignatureToTransaction({
218
+ transaction: original,
219
+ signer: params.transport.walletAddress,
220
+ signature
221
+ });
222
+ return {
223
+ serializedTransaction: attached.serializedBase64,
224
+ agentSignature: bs583.encode(signature)
225
+ };
226
+ }
227
+ async function providerJsonRequest(params) {
228
+ const response = await params.fetchImpl(`${params.baseUrl}${params.path}`, {
229
+ method: params.method,
230
+ headers: { ...params.headers, "content-type": "application/json" },
231
+ ...params.body === void 0 ? {} : { body: JSON.stringify(params.body) }
232
+ });
233
+ let json = null;
234
+ try {
235
+ json = await response.json();
236
+ } catch {
237
+ json = null;
238
+ }
239
+ if (!response.ok) {
240
+ throw new RemoteSigningError(
241
+ params.provider,
242
+ `${params.method} ${params.path} failed with ${response.status}`,
243
+ json
244
+ );
245
+ }
246
+ return json;
247
+ }
248
+
249
+ // ../../src/client/transaction-intent-validator.ts
250
+ import bs585 from "bs58";
67
251
  import { getCompiledTransactionMessageDecoder } from "@solana/kit";
68
252
 
69
253
  // ../../src/config/constants.ts
@@ -71,14 +255,30 @@ var PAYMENT_SCHEME = "subly-yield-exact";
71
255
  var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
72
256
  var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
73
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
+ };
74
262
  var SUBLY_VAULT = {
75
263
  name: "Subly USDC Payment Vault Alpha",
76
- address: "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr",
264
+ address: envOr(
265
+ "SUBLY_VAULT_ADDRESS",
266
+ "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr"
267
+ ),
77
268
  programId: "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd",
78
- usdcMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
79
- shareMint: "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a",
269
+ usdcMint: envOr(
270
+ "SUBLY_VAULT_USDC_MINT",
271
+ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
272
+ ),
273
+ shareMint: envOr(
274
+ "SUBLY_VAULT_SHARE_MINT",
275
+ "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a"
276
+ ),
80
277
  lookupTable: "7UbXhDnpK7WVnwsfivzQRENoqKqAULQ5s19gS1xJrQEo",
81
- farm: "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
278
+ farm: envOr(
279
+ "SUBLY_VAULT_FARM",
280
+ "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
281
+ )
82
282
  };
83
283
  var USDC_DECIMALS = 6;
84
284
 
@@ -99,7 +299,7 @@ function computeRequestBindingHash(fields) {
99
299
 
100
300
  // ../../src/lib/associated-token-account.ts
101
301
  import { createHash as createHash2 } from "node:crypto";
102
- import bs582 from "bs58";
302
+ import bs584 from "bs58";
103
303
  var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
104
304
  var ED25519_P = (1n << 255n) - 19n;
105
305
  var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
@@ -120,7 +320,7 @@ function deriveAssociatedTokenAddress(params) {
120
320
  associatedTokenProgramId
121
321
  );
122
322
  if (address2 !== null) {
123
- return bs582.encode(address2);
323
+ return bs584.encode(address2);
124
324
  }
125
325
  }
126
326
  throw new Error("Unable to derive associated token account address");
@@ -136,7 +336,7 @@ function createProgramAddress(seeds, programId) {
136
336
  return isEd25519Point(digest) ? null : new Uint8Array(digest);
137
337
  }
138
338
  function decodePublicKey(value, fieldName) {
139
- const decoded = bs582.decode(value);
339
+ const decoded = bs584.decode(value);
140
340
  if (decoded.length !== 32) {
141
341
  throw new Error(`${fieldName} must be a 32-byte public key`);
142
342
  }
@@ -337,6 +537,9 @@ function validatePaymentIntentTransaction(params) {
337
537
  if (intent.shareMint !== SUBLY_VAULT.shareMint) {
338
538
  reject("share_mint_mismatch", "Unsupported share mint");
339
539
  }
540
+ if (intent.farm !== SUBLY_VAULT.farm) {
541
+ reject("farm_mismatch", "Unsupported Kamino farm");
542
+ }
340
543
  if (intent.asset !== SUBLY_VAULT.usdcMint) {
341
544
  reject("asset_mismatch", "Only USDC payments are supported");
342
545
  }
@@ -405,7 +608,7 @@ function validatePaymentIntentTransaction(params) {
405
608
  expectComputeBudgetPair(ixs, policy2);
406
609
  expectCreateTemporaryAccount(ixs, intent, policy2);
407
610
  expectInitializeTemporaryAccount(ixs, intent);
408
- consumeFarmInstructions(ixs, intent.wallet);
611
+ consumeFarmInstructions(ixs, intent);
409
612
  expectKvaultWithdraw(ixs, {
410
613
  wallet: intent.wallet,
411
614
  vault: intent.vault,
@@ -548,13 +751,27 @@ function validateWithdrawalIntentTransaction(params) {
548
751
  reject("fee_payer_mismatch", "Transaction fee payer is not the Subly sponsor");
549
752
  }
550
753
  let sawWithdraw = false;
754
+ let farmUserState = null;
755
+ let farmInstructionCount = 0;
551
756
  for (const ix of decoded.instructions) {
552
757
  switch (ix.programAddress) {
553
758
  case COMPUTE_BUDGET_PROGRAM_ID:
554
759
  validateComputeBudgetInstruction(ix, policy2);
555
760
  break;
556
761
  case MEMO_PROGRAM_ID:
762
+ break;
557
763
  case KAMINO_FARMS_PROGRAM_ID:
764
+ farmInstructionCount += 1;
765
+ if (farmInstructionCount === 1) {
766
+ farmUserState = validateFarmUnstakeInstruction(ix, intent);
767
+ } else if (farmInstructionCount === 2) {
768
+ validateFarmWithdrawInstruction(ix, intent, farmUserState);
769
+ } else {
770
+ reject(
771
+ "farm_instruction_mismatch",
772
+ "Withdrawal may contain only one farm unstake and one farm withdrawal"
773
+ );
774
+ }
558
775
  break;
559
776
  case ASSOCIATED_TOKEN_PROGRAM_ID2:
560
777
  expectAtaCreateForOwner(ix, intent.wallet);
@@ -575,6 +792,12 @@ function validateWithdrawalIntentTransaction(params) {
575
792
  break;
576
793
  }
577
794
  case KVAULT_PROGRAM_ID: {
795
+ if (sawWithdraw) {
796
+ reject(
797
+ "withdraw_mismatch",
798
+ "Withdrawal may contain only one KVault withdraw instruction"
799
+ );
800
+ }
578
801
  validateKvaultWithdrawInstruction(ix, {
579
802
  wallet: intent.wallet,
580
803
  vault: intent.vault,
@@ -597,6 +820,12 @@ function validateWithdrawalIntentTransaction(params) {
597
820
  if (!sawWithdraw) {
598
821
  reject("missing_instruction", "Withdrawal transaction has no KVault withdraw");
599
822
  }
823
+ if (farmInstructionCount === 1) {
824
+ reject(
825
+ "farm_instruction_mismatch",
826
+ "A farm unstake must be followed by a farm withdrawal"
827
+ );
828
+ }
600
829
  }
601
830
  function assertVaultIntentTargets(intent) {
602
831
  if (intent.vault !== SUBLY_VAULT.address) {
@@ -605,6 +834,9 @@ function assertVaultIntentTargets(intent) {
605
834
  if (intent.shareMint !== SUBLY_VAULT.shareMint) {
606
835
  reject("share_mint_mismatch", "Unsupported share mint");
607
836
  }
837
+ if (intent.farm !== SUBLY_VAULT.farm) {
838
+ reject("farm_mismatch", "Unsupported Kamino farm");
839
+ }
608
840
  if (intent.asset !== SUBLY_VAULT.usdcMint) {
609
841
  reject("asset_mismatch", "Only USDC is supported");
610
842
  }
@@ -677,7 +909,7 @@ function expectCreateTemporaryAccount(ixs, intent, policy2) {
677
909
  }
678
910
  const lamports = readU64LE(ix.data, 4);
679
911
  const space = readU64LE(ix.data, 12);
680
- const owner = bs583.encode(ix.data.subarray(20, 52));
912
+ const owner = bs585.encode(ix.data.subarray(20, 52));
681
913
  if (space !== 165n) {
682
914
  reject("temp_account_mismatch", "Temporary account space must be 165 bytes");
683
915
  }
@@ -699,7 +931,7 @@ function expectInitializeTemporaryAccount(ixs, intent) {
699
931
  if (ix === void 0 || ix.programAddress !== SPL_TOKEN_PROGRAM_ID || ix.data[0] !== 18) {
700
932
  reject("temp_account_mismatch", "Expected InitializeAccount3 instruction");
701
933
  }
702
- const owner = bs583.encode(ix.data.subarray(1, 33));
934
+ const owner = bs585.encode(ix.data.subarray(1, 33));
703
935
  if (owner !== intent.wallet) {
704
936
  reject(
705
937
  "temp_account_mismatch",
@@ -713,15 +945,90 @@ function expectInitializeTemporaryAccount(ixs, intent) {
713
945
  reject("temp_account_mismatch", "Temporary account mint must be USDC");
714
946
  }
715
947
  }
716
- function consumeFarmInstructions(ixs, wallet) {
717
- while (ixs[0] !== void 0 && ixs[0].programAddress === KAMINO_FARMS_PROGRAM_ID) {
718
- const ix = ixs.shift();
719
- if (!ix.accounts.includes(wallet)) {
720
- reject(
721
- "farm_instruction_mismatch",
722
- "Farm unstake instruction does not reference the agent wallet"
723
- );
724
- }
948
+ function consumeFarmInstructions(ixs, intent) {
949
+ if (ixs[0]?.programAddress !== KAMINO_FARMS_PROGRAM_ID) {
950
+ return;
951
+ }
952
+ const unstake = ixs.shift();
953
+ const userState = validateFarmUnstakeInstruction(unstake, intent);
954
+ if (ixs[0]?.programAddress !== KAMINO_FARMS_PROGRAM_ID) {
955
+ reject(
956
+ "farm_instruction_mismatch",
957
+ "A farm unstake must be followed by a farm withdrawal"
958
+ );
959
+ }
960
+ const withdraw = ixs.shift();
961
+ validateFarmWithdrawInstruction(withdraw, intent, userState);
962
+ if (ixs[0]?.programAddress === KAMINO_FARMS_PROGRAM_ID) {
963
+ reject(
964
+ "farm_instruction_mismatch",
965
+ "Payment may contain only one farm unstake and one farm withdrawal"
966
+ );
967
+ }
968
+ }
969
+ var KAMINO_FARMS_UNSTAKE_DISCRIMINATOR = Uint8Array.from([
970
+ 90,
971
+ 95,
972
+ 107,
973
+ 42,
974
+ 205,
975
+ 124,
976
+ 50,
977
+ 225
978
+ ]);
979
+ var KAMINO_FARMS_WITHDRAW_UNSTAKED_DISCRIMINATOR = Uint8Array.from([
980
+ 36,
981
+ 102,
982
+ 187,
983
+ 49,
984
+ 220,
985
+ 36,
986
+ 132,
987
+ 67
988
+ ]);
989
+ function validateFarmUnstakeInstruction(ix, intent) {
990
+ if (ix.programAddress !== KAMINO_FARMS_PROGRAM_ID || !bytesStartWith(ix.data, KAMINO_FARMS_UNSTAKE_DISCRIMINATOR) || ix.data.length !== 24 || readU128LE(ix.data, 8) <= 0n) {
991
+ reject(
992
+ "farm_instruction_mismatch",
993
+ "Expected a non-zero Kamino farm unstake instruction"
994
+ );
995
+ }
996
+ if (ix.accounts.length !== 4) {
997
+ reject(
998
+ "farm_instruction_mismatch",
999
+ "Farm unstake account list is not canonical"
1000
+ );
1001
+ }
1002
+ if (ix.accounts[0] !== intent.wallet) {
1003
+ reject("farm_instruction_mismatch", "Farm unstake owner must be the agent wallet");
1004
+ }
1005
+ if (ix.accounts[2] !== intent.farm) {
1006
+ reject("farm_instruction_mismatch", "Farm unstake target is not the approved farm");
1007
+ }
1008
+ return ix.accounts[1];
1009
+ }
1010
+ function validateFarmWithdrawInstruction(ix, intent, expectedUserState) {
1011
+ if (ix.programAddress !== KAMINO_FARMS_PROGRAM_ID || !bytesStartWith(ix.data, KAMINO_FARMS_WITHDRAW_UNSTAKED_DISCRIMINATOR) || ix.data.length !== 8) {
1012
+ reject(
1013
+ "farm_instruction_mismatch",
1014
+ "Expected a canonical Kamino farm withdrawal instruction"
1015
+ );
1016
+ }
1017
+ if (ix.accounts.length !== 7) {
1018
+ reject(
1019
+ "farm_instruction_mismatch",
1020
+ "Farm withdrawal account list is not canonical"
1021
+ );
1022
+ }
1023
+ const expectedSharesAta = deriveAssociatedTokenAddress({
1024
+ owner: intent.wallet,
1025
+ mint: intent.shareMint
1026
+ });
1027
+ 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) {
1028
+ reject(
1029
+ "farm_instruction_mismatch",
1030
+ "Farm withdrawal must return the approved vault shares to the agent wallet"
1031
+ );
725
1032
  }
726
1033
  }
727
1034
  function expectKvaultWithdraw(ixs, expectation) {
@@ -856,6 +1163,16 @@ function readU32LE(data, offset) {
856
1163
  }
857
1164
  return Buffer.from(data.subarray(offset, offset + 4)).readUInt32LE(0);
858
1165
  }
1166
+ function readU128LE(data, offset) {
1167
+ if (data.length < offset + 16) {
1168
+ reject("invalid_transaction_encoding", "Instruction data too short for u128");
1169
+ }
1170
+ let value = 0n;
1171
+ for (let index = 0; index < 16; index += 1) {
1172
+ value |= BigInt(data[offset + index]) << BigInt(index * 8);
1173
+ }
1174
+ return value;
1175
+ }
859
1176
  function readShortVec(bytes, startOffset) {
860
1177
  let value = 0;
861
1178
  let shift = 0;
@@ -876,55 +1193,55 @@ function readShortVec(bytes, startOffset) {
876
1193
  }
877
1194
 
878
1195
  // ../../src/client/agent-wallet-signer.ts
879
- var LocalKeypairAgentWalletSigner = class {
1196
+ var IntentValidatingAgentWalletSigner = class {
880
1197
  validationMode = "structured_intent_transaction";
881
- keyPairSigner;
882
1198
  validationPolicy;
883
- constructor(keyPairSigner2, validationPolicy) {
884
- this.keyPairSigner = keyPairSigner2;
1199
+ constructor(validationPolicy) {
885
1200
  this.validationPolicy = validationPolicy;
886
1201
  }
887
- get walletAddress() {
888
- return this.keyPairSigner.address;
889
- }
890
1202
  async signPayment(params) {
891
1203
  this.assertIntentWallet(params.intent.wallet);
892
- validatePaymentIntentTransaction({
893
- ...params,
894
- ...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
895
- });
1204
+ validatePaymentIntentTransaction({ ...params, ...this.policySpread() });
896
1205
  return this.sign(params.serializedTransaction);
897
1206
  }
898
1207
  async signDeposit(params) {
899
1208
  this.assertIntentWallet(params.intent.wallet);
900
- validateDepositIntentTransaction({
901
- ...params,
902
- ...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
903
- });
1209
+ validateDepositIntentTransaction({ ...params, ...this.policySpread() });
904
1210
  return this.sign(params.serializedTransaction);
905
1211
  }
906
1212
  async signWithdrawal(params) {
907
1213
  this.assertIntentWallet(params.intent.wallet);
908
- validateWithdrawalIntentTransaction({
909
- ...params,
910
- ...this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy }
911
- });
1214
+ validateWithdrawalIntentTransaction({ ...params, ...this.policySpread() });
912
1215
  return this.sign(params.serializedTransaction);
913
1216
  }
1217
+ policySpread() {
1218
+ return this.validationPolicy === void 0 ? {} : { policy: this.validationPolicy };
1219
+ }
914
1220
  assertIntentWallet(wallet) {
915
- if (wallet !== this.keyPairSigner.address) {
1221
+ if (wallet !== this.walletAddress) {
916
1222
  throw new IntentValidationError(
917
1223
  "wallet_mismatch",
918
1224
  "Intent wallet does not match this signer's wallet"
919
1225
  );
920
1226
  }
921
1227
  }
1228
+ };
1229
+ var LocalKeypairAgentWalletSigner = class extends IntentValidatingAgentWalletSigner {
1230
+ provider = "local-keypair";
1231
+ keyPairSigner;
1232
+ constructor(keyPairSigner, validationPolicy) {
1233
+ super(validationPolicy);
1234
+ this.keyPairSigner = keyPairSigner;
1235
+ }
1236
+ get walletAddress() {
1237
+ return this.keyPairSigner.address;
1238
+ }
922
1239
  async signApiMessage(message) {
923
1240
  const signature = await signBytes(
924
1241
  this.keyPairSigner.keyPair.privateKey,
925
1242
  message
926
1243
  );
927
- return bs584.encode(signature);
1244
+ return bs586.encode(signature);
928
1245
  }
929
1246
  async sign(serializedTransaction) {
930
1247
  const { serializedBase64, transaction } = await addSignaturesToSerializedTransaction({
@@ -944,6 +1261,379 @@ var LocalKeypairAgentWalletSigner = class {
944
1261
  return { serializedTransaction: serializedBase64, agentSignature };
945
1262
  }
946
1263
  };
1264
+ var RemoteAgentWalletSigner = class extends IntentValidatingAgentWalletSigner {
1265
+ transport;
1266
+ publicKey;
1267
+ constructor(transport, validationPolicy) {
1268
+ super(validationPolicy);
1269
+ this.transport = transport;
1270
+ this.publicKey = ed25519PublicKeyBytes(
1271
+ transport.provider,
1272
+ transport.walletAddress
1273
+ );
1274
+ }
1275
+ get walletAddress() {
1276
+ return this.transport.walletAddress;
1277
+ }
1278
+ get provider() {
1279
+ return this.transport.provider;
1280
+ }
1281
+ async signApiMessage(message) {
1282
+ const signature = await this.transport.signMessage(message);
1283
+ if (!nacl2.sign.detached.verify(message, signature, this.publicKey)) {
1284
+ throw new RemoteSigningError(
1285
+ this.transport.provider,
1286
+ "message signature did not verify for the agent wallet"
1287
+ );
1288
+ }
1289
+ return bs586.encode(signature);
1290
+ }
1291
+ sign(serializedTransaction) {
1292
+ return externallySignedAgentTransaction({
1293
+ transport: this.transport,
1294
+ serializedTransaction
1295
+ });
1296
+ }
1297
+ };
1298
+
1299
+ // ../../src/client/signer-transports/circle.ts
1300
+ import {
1301
+ constants,
1302
+ createPublicKey,
1303
+ publicEncrypt
1304
+ } from "node:crypto";
1305
+ var PROVIDER = "circle";
1306
+ var DEFAULT_BASE_URL = "https://api.circle.com";
1307
+ async function createCircleSignerTransport(config) {
1308
+ if (!/^[0-9a-fA-F]{64}$/.test(config.entitySecret)) {
1309
+ throw new RemoteSigningError(
1310
+ PROVIDER,
1311
+ "entity secret must be 32 bytes of hex (64 hex chars)"
1312
+ );
1313
+ }
1314
+ const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
1315
+ const fetchImpl = config.fetchImpl ?? fetch;
1316
+ const request = async (method, path, body) => {
1317
+ const json = await providerJsonRequest({
1318
+ provider: PROVIDER,
1319
+ fetchImpl,
1320
+ baseUrl,
1321
+ path,
1322
+ method,
1323
+ headers: { authorization: `Bearer ${config.apiKey}` },
1324
+ body
1325
+ });
1326
+ const data = json?.data;
1327
+ if (data === void 0) {
1328
+ throw new RemoteSigningError(
1329
+ PROVIDER,
1330
+ `${method} ${path} returned no data envelope`,
1331
+ json
1332
+ );
1333
+ }
1334
+ return data;
1335
+ };
1336
+ const walletData = await request("GET", `/v1/w3s/wallets/${config.walletId}`);
1337
+ const wallet = walletData.wallet;
1338
+ if (wallet?.address === void 0) {
1339
+ throw new RemoteSigningError(
1340
+ PROVIDER,
1341
+ `wallet ${config.walletId} has no address`,
1342
+ walletData
1343
+ );
1344
+ }
1345
+ if (wallet.blockchain !== "SOL") {
1346
+ throw new RemoteSigningError(
1347
+ PROVIDER,
1348
+ `wallet ${config.walletId} is on ${String(
1349
+ wallet.blockchain
1350
+ )}, expected SOL (Solana mainnet)`
1351
+ );
1352
+ }
1353
+ const walletAddress = wallet.address;
1354
+ let entityPublicKey = null;
1355
+ const entitySecretCiphertext = async () => {
1356
+ if (entityPublicKey === null) {
1357
+ const data = await request("GET", "/v1/w3s/config/entity/publicKey");
1358
+ const publicKey = data.publicKey;
1359
+ if (typeof publicKey !== "string") {
1360
+ throw new RemoteSigningError(
1361
+ PROVIDER,
1362
+ "entity public key response has no publicKey",
1363
+ data
1364
+ );
1365
+ }
1366
+ entityPublicKey = createPublicKey(publicKey);
1367
+ }
1368
+ return publicEncrypt(
1369
+ {
1370
+ key: entityPublicKey,
1371
+ padding: constants.RSA_PKCS1_OAEP_PADDING,
1372
+ oaepHash: "sha256"
1373
+ },
1374
+ Buffer.from(config.entitySecret, "hex")
1375
+ ).toString("base64");
1376
+ };
1377
+ return {
1378
+ provider: PROVIDER,
1379
+ walletAddress,
1380
+ async signMessage(message) {
1381
+ const data = await request("POST", "/v1/w3s/developer/sign/message", {
1382
+ walletId: config.walletId,
1383
+ message: `0x${Buffer.from(message).toString("hex")}`,
1384
+ encodedByHex: true,
1385
+ entitySecretCiphertext: await entitySecretCiphertext()
1386
+ });
1387
+ const signature = data.signature;
1388
+ if (typeof signature !== "string") {
1389
+ throw new RemoteSigningError(
1390
+ PROVIDER,
1391
+ "sign/message returned no signature",
1392
+ data
1393
+ );
1394
+ }
1395
+ return verifiedEd25519Signature({
1396
+ provider: PROVIDER,
1397
+ encodedSignature: signature,
1398
+ message,
1399
+ walletAddress
1400
+ });
1401
+ },
1402
+ async signTransaction(serializedTransactionBase64) {
1403
+ const data = await request(
1404
+ "POST",
1405
+ "/v1/w3s/developer/sign/transaction",
1406
+ {
1407
+ walletId: config.walletId,
1408
+ rawTransaction: serializedTransactionBase64,
1409
+ entitySecretCiphertext: await entitySecretCiphertext()
1410
+ }
1411
+ );
1412
+ const signedTransaction = data.signedTransaction;
1413
+ if (typeof signedTransaction !== "string") {
1414
+ throw new RemoteSigningError(
1415
+ PROVIDER,
1416
+ "sign/transaction returned no signedTransaction",
1417
+ data
1418
+ );
1419
+ }
1420
+ return signedTransaction;
1421
+ }
1422
+ };
1423
+ }
1424
+
1425
+ // ../../src/client/signer-transports/privy.ts
1426
+ import { createPrivateKey, createSign } from "node:crypto";
1427
+ var PROVIDER2 = "privy";
1428
+ var DEFAULT_BASE_URL2 = "https://api.privy.io";
1429
+ function canonicalJson(value) {
1430
+ if (Array.isArray(value)) {
1431
+ return `[${value.map(canonicalJson).join(",")}]`;
1432
+ }
1433
+ if (value !== null && typeof value === "object") {
1434
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1435
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
1436
+ }
1437
+ return JSON.stringify(value);
1438
+ }
1439
+ function parseAuthorizationKey(base64Pkcs8) {
1440
+ const stripped = base64Pkcs8.replace(/^wallet-auth:/, "").trim();
1441
+ try {
1442
+ return createPrivateKey({
1443
+ key: Buffer.from(stripped, "base64"),
1444
+ format: "der",
1445
+ type: "pkcs8"
1446
+ });
1447
+ } catch (error) {
1448
+ throw new RemoteSigningError(
1449
+ PROVIDER2,
1450
+ "authorization key is not a base64 PKCS#8 P-256 private key",
1451
+ error
1452
+ );
1453
+ }
1454
+ }
1455
+ function authorizationSignature(params) {
1456
+ const payload = {
1457
+ version: 1,
1458
+ method: params.method,
1459
+ url: params.url,
1460
+ body: params.body,
1461
+ headers: { "privy-app-id": params.appId }
1462
+ };
1463
+ const signer2 = createSign("sha256");
1464
+ signer2.update(canonicalJson(payload));
1465
+ return signer2.sign(params.key).toString("base64");
1466
+ }
1467
+ async function createPrivySignerTransport(config) {
1468
+ const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/$/, "");
1469
+ const fetchImpl = config.fetchImpl ?? fetch;
1470
+ const authorizationKey = config.authorizationPrivateKey === void 0 ? null : parseAuthorizationKey(config.authorizationPrivateKey);
1471
+ const baseHeaders = {
1472
+ authorization: `Basic ${Buffer.from(
1473
+ `${config.appId}:${config.appSecret}`
1474
+ ).toString("base64")}`,
1475
+ "privy-app-id": config.appId
1476
+ };
1477
+ const request = async (method, path, body) => {
1478
+ const headers = authorizationKey !== null && method !== "GET" && body !== void 0 ? {
1479
+ ...baseHeaders,
1480
+ "privy-authorization-signature": authorizationSignature({
1481
+ key: authorizationKey,
1482
+ appId: config.appId,
1483
+ method,
1484
+ url: `${baseUrl}${path}`,
1485
+ body
1486
+ })
1487
+ } : baseHeaders;
1488
+ const json = await providerJsonRequest({
1489
+ provider: PROVIDER2,
1490
+ fetchImpl,
1491
+ baseUrl,
1492
+ path,
1493
+ method,
1494
+ headers,
1495
+ body
1496
+ });
1497
+ if (json === null || typeof json !== "object") {
1498
+ throw new RemoteSigningError(
1499
+ PROVIDER2,
1500
+ `${method} ${path} returned a non-JSON body`
1501
+ );
1502
+ }
1503
+ return json;
1504
+ };
1505
+ const rpc = async (body) => {
1506
+ const response = await request(
1507
+ "POST",
1508
+ `/v1/wallets/${config.walletId}/rpc`,
1509
+ body
1510
+ );
1511
+ const data = response.data;
1512
+ if (data === null || typeof data !== "object") {
1513
+ throw new RemoteSigningError(PROVIDER2, "rpc returned no data", response);
1514
+ }
1515
+ return data;
1516
+ };
1517
+ const wallet = await request("GET", `/v1/wallets/${config.walletId}`);
1518
+ const walletAddress = wallet.address;
1519
+ if (typeof walletAddress !== "string") {
1520
+ throw new RemoteSigningError(
1521
+ PROVIDER2,
1522
+ `wallet ${config.walletId} has no address`,
1523
+ wallet
1524
+ );
1525
+ }
1526
+ if (wallet.chain_type !== "solana") {
1527
+ throw new RemoteSigningError(
1528
+ PROVIDER2,
1529
+ `wallet ${config.walletId} is ${String(
1530
+ wallet.chain_type
1531
+ )}, expected solana`
1532
+ );
1533
+ }
1534
+ return {
1535
+ provider: PROVIDER2,
1536
+ walletAddress,
1537
+ async signMessage(message) {
1538
+ const data = await rpc({
1539
+ chain_type: "solana",
1540
+ method: "signMessage",
1541
+ params: {
1542
+ message: Buffer.from(message).toString("base64"),
1543
+ encoding: "base64"
1544
+ }
1545
+ });
1546
+ const signature = data.signature;
1547
+ if (typeof signature !== "string") {
1548
+ throw new RemoteSigningError(
1549
+ PROVIDER2,
1550
+ "signMessage returned no signature",
1551
+ data
1552
+ );
1553
+ }
1554
+ return verifiedEd25519Signature({
1555
+ provider: PROVIDER2,
1556
+ encodedSignature: signature,
1557
+ message,
1558
+ walletAddress
1559
+ });
1560
+ },
1561
+ async signTransaction(serializedTransactionBase64) {
1562
+ const data = await rpc({
1563
+ chain_type: "solana",
1564
+ method: "signTransaction",
1565
+ params: {
1566
+ transaction: serializedTransactionBase64,
1567
+ encoding: "base64"
1568
+ }
1569
+ });
1570
+ const signedTransaction = data.signed_transaction;
1571
+ if (typeof signedTransaction !== "string") {
1572
+ throw new RemoteSigningError(
1573
+ PROVIDER2,
1574
+ "signTransaction returned no signed_transaction",
1575
+ data
1576
+ );
1577
+ }
1578
+ return signedTransaction;
1579
+ }
1580
+ };
1581
+ }
1582
+
1583
+ // ../../src/client/signer-env.ts
1584
+ async function agentWalletSignerFromEnv(env = process.env) {
1585
+ const nonEmpty = (value) => {
1586
+ const trimmed = value?.trim();
1587
+ return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
1588
+ };
1589
+ const provider = nonEmpty(env.SUBLY_SIGNER_PROVIDER)?.toLowerCase() ?? "local";
1590
+ const pickVar = (name) => nonEmpty(env[`SUBLY_${name}`]) ?? nonEmpty(env[name]);
1591
+ const requireVar = (name) => {
1592
+ const value = pickVar(name);
1593
+ if (value === void 0) {
1594
+ throw new Error(
1595
+ `${name} (or SUBLY_${name}) is required for SUBLY_SIGNER_PROVIDER=${provider}`
1596
+ );
1597
+ }
1598
+ return value;
1599
+ };
1600
+ if (provider === "local") {
1601
+ const localSecretKey = loadSecretKeyBytes({
1602
+ base58Secret: env.SUBLY_DEMO_AGENT_KEYPAIR,
1603
+ jsonFilePath: env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1604
+ label: "SUBLY_DEMO_AGENT_KEYPAIR"
1605
+ });
1606
+ return {
1607
+ provider,
1608
+ signer: new LocalKeypairAgentWalletSigner(
1609
+ await createKeyPairSignerFromBytes2(localSecretKey)
1610
+ ),
1611
+ localSecretKey
1612
+ };
1613
+ }
1614
+ if (provider === "circle") {
1615
+ const transport = await createCircleSignerTransport({
1616
+ apiKey: requireVar("CIRCLE_API_KEY"),
1617
+ entitySecret: requireVar("CIRCLE_ENTITY_SECRET"),
1618
+ walletId: requireVar("CIRCLE_WALLET_ID"),
1619
+ baseUrl: pickVar("CIRCLE_BASE_URL")
1620
+ });
1621
+ return { provider, signer: new RemoteAgentWalletSigner(transport), transport };
1622
+ }
1623
+ if (provider === "privy") {
1624
+ const transport = await createPrivySignerTransport({
1625
+ appId: requireVar("PRIVY_APP_ID"),
1626
+ appSecret: requireVar("PRIVY_APP_SECRET"),
1627
+ walletId: requireVar("PRIVY_WALLET_ID"),
1628
+ authorizationPrivateKey: pickVar("PRIVY_AUTHORIZATION_KEY"),
1629
+ baseUrl: pickVar("PRIVY_BASE_URL")
1630
+ });
1631
+ return { provider, signer: new RemoteAgentWalletSigner(transport), transport };
1632
+ }
1633
+ throw new Error(
1634
+ `unknown SUBLY_SIGNER_PROVIDER "${provider}" (expected local, circle, or privy)`
1635
+ );
1636
+ }
947
1637
 
948
1638
  // ../../src/client/lookup-tables.ts
949
1639
  import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
@@ -987,8 +1677,8 @@ async function fetchLookupTablesForTransaction(rpc, serializedTransaction) {
987
1677
 
988
1678
  // ../../src/api/wallet-auth.ts
989
1679
  import { createHash as createHash3 } from "node:crypto";
990
- import bs585 from "bs58";
991
- import nacl from "tweetnacl";
1680
+ import bs587 from "bs58";
1681
+ import nacl3 from "tweetnacl";
992
1682
  var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
993
1683
  var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
994
1684
  var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
@@ -1008,7 +1698,10 @@ async function walletAuthHeaders(params) {
1008
1698
  const signedAtMs = String(Date.now());
1009
1699
  const message = walletAuthMessage({
1010
1700
  method: params.method,
1011
- path: new URL(params.url).pathname,
1701
+ path: (() => {
1702
+ const url = new URL(params.url);
1703
+ return url.pathname + url.search;
1704
+ })(),
1012
1705
  rawBody: params.body ?? "",
1013
1706
  signedAtMs
1014
1707
  });
@@ -1375,31 +2068,6 @@ function parseRelayerError(text) {
1375
2068
  }
1376
2069
  }
1377
2070
 
1378
- // ../../src/solana/keys.ts
1379
- import { readFileSync } from "node:fs";
1380
- import bs586 from "bs58";
1381
- import {
1382
- createKeyPairSignerFromBytes
1383
- } from "@solana/kit";
1384
- async function loadKeyPairSigner(params) {
1385
- const { base58Secret, jsonFilePath, label } = params;
1386
- if (base58Secret !== void 0 && base58Secret.length > 0) {
1387
- const bytes = bs586.decode(base58Secret);
1388
- if (bytes.length !== 64) {
1389
- throw new Error(`${label} base58 secret must decode to 64 bytes`);
1390
- }
1391
- return createKeyPairSignerFromBytes(bytes);
1392
- }
1393
- if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
1394
- const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
1395
- if (!Array.isArray(raw) || raw.length !== 64) {
1396
- throw new Error(`${label} keypair file must be a 64-byte JSON array`);
1397
- }
1398
- return createKeyPairSignerFromBytes(Uint8Array.from(raw));
1399
- }
1400
- throw new Error(`${label} keypair is not configured`);
1401
- }
1402
-
1403
2071
  // ../../src/solana/rpc.ts
1404
2072
  import { createSolanaRpc } from "@solana/kit";
1405
2073
  function createRpc(url) {
@@ -1422,7 +2090,15 @@ var FLAG_TO_POLICY_KEY = {
1422
2090
  var policy = {};
1423
2091
  var initialDepositRawUsdc;
1424
2092
  var mandateTtlDays;
1425
- var args = process.argv.slice(2);
2093
+ var args = [];
2094
+ for (const raw of process.argv.slice(2)) {
2095
+ const eq = raw.startsWith("--") ? raw.indexOf("=") : -1;
2096
+ if (eq > 0) {
2097
+ args.push(raw.slice(0, eq), raw.slice(eq + 1));
2098
+ } else {
2099
+ args.push(raw);
2100
+ }
2101
+ }
1426
2102
  for (let i = 0; i < args.length; i += 2) {
1427
2103
  const flag = args[i];
1428
2104
  const value = args[i + 1];
@@ -1455,12 +2131,7 @@ ${USAGE}`);
1455
2131
  }
1456
2132
  }
1457
2133
  var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
1458
- var keyPairSigner = await loadKeyPairSigner({
1459
- base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
1460
- jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1461
- label: "SUBLY_DEMO_AGENT_KEYPAIR"
1462
- });
1463
- var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
2134
+ var { signer } = await agentWalletSignerFromEnv();
1464
2135
  var vaultFlows = new VaultFlowClient({
1465
2136
  relayerBaseUrl,
1466
2137
  signer,