@subly_fi/pay 0.3.0 → 0.4.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/dist/withdraw.js CHANGED
@@ -48,10 +48,10 @@ function decodeSerializedTransaction(serializedBase64) {
48
48
  }
49
49
  async function addSignaturesToSerializedTransaction(params) {
50
50
  const decoded = decodeSerializedTransaction(params.serializedBase64);
51
- const signed2 = await partiallySignTransaction(params.signers, decoded);
51
+ const signed = await partiallySignTransaction(params.signers, decoded);
52
52
  return {
53
- serializedBase64: getBase64EncodedWireTransaction(signed2),
54
- transaction: signed2
53
+ serializedBase64: getBase64EncodedWireTransaction(signed),
54
+ transaction: signed
55
55
  };
56
56
  }
57
57
  function signatureBase58ForSigner(transaction, signer2) {
@@ -945,40 +945,6 @@ var LocalKeypairAgentWalletSigner = class {
945
945
  }
946
946
  };
947
947
 
948
- // ../../src/api/wallet-auth.ts
949
- import { createHash as createHash3 } from "node:crypto";
950
- import bs585 from "bs58";
951
- import nacl from "tweetnacl";
952
- var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
953
- var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
954
- var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
955
- function sha256Hex(data) {
956
- return createHash3("sha256").update(data, "utf8").digest("hex");
957
- }
958
- function walletAuthMessage(params) {
959
- return new TextEncoder().encode(
960
- `subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
961
- params.rawBody
962
- )}:${params.signedAtMs}`
963
- );
964
- }
965
-
966
- // ../../src/client/wallet-auth-headers.ts
967
- async function walletAuthHeaders(params) {
968
- const signedAtMs = String(Date.now());
969
- const message = walletAuthMessage({
970
- method: params.method,
971
- path: new URL(params.url).pathname,
972
- rawBody: params.body ?? "",
973
- signedAtMs
974
- });
975
- return {
976
- [WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
977
- [WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
978
- [WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
979
- };
980
- }
981
-
982
948
  // ../../src/client/lookup-tables.ts
983
949
  import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
984
950
  import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
@@ -1019,6 +985,256 @@ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
1019
985
  return result;
1020
986
  }
1021
987
 
988
+ // ../../src/api/wallet-auth.ts
989
+ import { createHash as createHash3 } from "node:crypto";
990
+ import bs585 from "bs58";
991
+ import nacl from "tweetnacl";
992
+ var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
993
+ var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
994
+ var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
995
+ function sha256Hex(data) {
996
+ return createHash3("sha256").update(data, "utf8").digest("hex");
997
+ }
998
+ function walletAuthMessage(params) {
999
+ return new TextEncoder().encode(
1000
+ `subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
1001
+ params.rawBody
1002
+ )}:${params.signedAtMs}`
1003
+ );
1004
+ }
1005
+
1006
+ // ../../src/client/wallet-auth-headers.ts
1007
+ async function walletAuthHeaders(params) {
1008
+ const signedAtMs = String(Date.now());
1009
+ const message = walletAuthMessage({
1010
+ method: params.method,
1011
+ path: new URL(params.url).pathname,
1012
+ rawBody: params.body ?? "",
1013
+ signedAtMs
1014
+ });
1015
+ return {
1016
+ [WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
1017
+ [WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
1018
+ [WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
1019
+ };
1020
+ }
1021
+
1022
+ // ../../src/client/vault-flows.ts
1023
+ var VaultFlowClientError = class extends Error {
1024
+ constructor(step, message, detail = null) {
1025
+ super(message);
1026
+ this.step = step;
1027
+ this.detail = detail;
1028
+ this.name = "VaultFlowClientError";
1029
+ }
1030
+ step;
1031
+ detail;
1032
+ };
1033
+ var VaultFlowClient = class {
1034
+ baseUrl;
1035
+ signer;
1036
+ fetchImpl;
1037
+ lookupTablesFor;
1038
+ pollTimeoutMs;
1039
+ pollIntervalMs;
1040
+ constructor(config) {
1041
+ this.baseUrl = config.facilitatorBaseUrl.replace(/\/$/, "");
1042
+ this.signer = config.signer;
1043
+ this.fetchImpl = config.fetchImpl ?? fetch;
1044
+ this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
1045
+ this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
1046
+ this.pollIntervalMs = config.pollIntervalMs ?? 2500;
1047
+ }
1048
+ /** Moves USDC from the agent wallet into the vault (fee sponsored). */
1049
+ async deposit(input) {
1050
+ const prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1051
+ wallet: this.signer.walletAddress,
1052
+ amountRawUsdc: input.amountRawUsdc.toString()
1053
+ });
1054
+ const signed = await this.signer.signDeposit({
1055
+ intent: prepared.signingIntent,
1056
+ serializedTransaction: prepared.serializedTransaction,
1057
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1058
+ });
1059
+ let outcome = await this.postJson("submit", "/v1/deposits/submit", {
1060
+ depositId: prepared.depositId,
1061
+ serializedTransaction: signed.serializedTransaction,
1062
+ agentSignature: signed.agentSignature
1063
+ });
1064
+ if (outcome.status === "submitted") {
1065
+ outcome = await this.pollUntilTerminal(
1066
+ `/v1/deposits/${prepared.depositId}`,
1067
+ outcome
1068
+ );
1069
+ }
1070
+ return {
1071
+ depositId: prepared.depositId,
1072
+ status: outcome.status,
1073
+ txSignature: outcome.txSignature ?? null,
1074
+ actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
1075
+ sharesMintedRaw: outcome.sharesMintedRaw ?? null,
1076
+ errorCode: outcome.errorCode ?? null
1077
+ };
1078
+ }
1079
+ /**
1080
+ * Moves USDC from the vault back to the agent wallet's USDC ATA (fee
1081
+ * sponsored). A plain withdrawal is the exit path and MAY spend principal;
1082
+ * with purpose "yield_realize" the relayer refuses anything beyond the
1083
+ * spendable yield (the payment path, via RelayerYieldRealizer).
1084
+ */
1085
+ async withdraw(input) {
1086
+ const prepared = await this.postJson(
1087
+ "prepare",
1088
+ "/v1/withdrawals/prepare",
1089
+ {
1090
+ wallet: this.signer.walletAddress,
1091
+ amountRawUsdc: input.amountRawUsdc.toString(),
1092
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose }
1093
+ }
1094
+ );
1095
+ const signed = await this.signer.signWithdrawal({
1096
+ intent: prepared.signingIntent,
1097
+ serializedTransaction: prepared.serializedTransaction,
1098
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1099
+ });
1100
+ let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
1101
+ withdrawalId: prepared.withdrawalId,
1102
+ serializedTransaction: signed.serializedTransaction,
1103
+ agentSignature: signed.agentSignature
1104
+ });
1105
+ if (outcome.status === "submitted") {
1106
+ outcome = await this.pollUntilTerminal(
1107
+ `/v1/withdrawals/${prepared.withdrawalId}`,
1108
+ outcome
1109
+ );
1110
+ }
1111
+ return {
1112
+ withdrawalId: prepared.withdrawalId,
1113
+ status: outcome.status,
1114
+ txSignature: outcome.txSignature ?? null,
1115
+ destinationUsdcAta: prepared.destinationUsdcAta,
1116
+ actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
1117
+ actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
1118
+ errorCode: outcome.errorCode ?? null
1119
+ };
1120
+ }
1121
+ /**
1122
+ * Reads the yield budget. Syncs the relayer's ledger from chain first (so
1123
+ * yield accrued since the last sync shows up); the sync is best-effort and
1124
+ * on failure the last-synced view is returned.
1125
+ */
1126
+ async getBudget(options = {}) {
1127
+ if (options.refreshFromChain !== false) {
1128
+ try {
1129
+ await this.postJson(
1130
+ "sync",
1131
+ `/v1/wallets/${this.signer.walletAddress}/sync`,
1132
+ { source: "chain" }
1133
+ );
1134
+ } catch {
1135
+ }
1136
+ }
1137
+ const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
1138
+ const response = await this.fetchImpl(url, {
1139
+ headers: await walletAuthHeaders({
1140
+ signer: this.signer,
1141
+ method: "GET",
1142
+ url
1143
+ })
1144
+ });
1145
+ const text = await response.text();
1146
+ if (response.status !== 200) {
1147
+ throw new VaultFlowClientError(
1148
+ "budget",
1149
+ `budget endpoint returned ${response.status}: ${text}`
1150
+ );
1151
+ }
1152
+ let parsed;
1153
+ try {
1154
+ parsed = JSON.parse(text);
1155
+ } catch {
1156
+ throw new VaultFlowClientError(
1157
+ "budget",
1158
+ "budget endpoint returned 200 with a non-JSON body",
1159
+ text
1160
+ );
1161
+ }
1162
+ const body = parsed;
1163
+ return {
1164
+ wallet: this.signer.walletAddress,
1165
+ principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
1166
+ positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
1167
+ grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
1168
+ spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
1169
+ };
1170
+ }
1171
+ /**
1172
+ * Polls the reconciling GET endpoint until the intent leaves "submitted"
1173
+ * (each read looks the tx up on-chain) or the timeout elapses.
1174
+ */
1175
+ async pollUntilTerminal(path, last) {
1176
+ const deadline = Date.now() + this.pollTimeoutMs;
1177
+ let latest = last;
1178
+ while (Date.now() < deadline) {
1179
+ await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
1180
+ const url = `${this.baseUrl}${path}`;
1181
+ const response = await this.fetchImpl(url, {
1182
+ headers: await walletAuthHeaders({
1183
+ signer: this.signer,
1184
+ method: "GET",
1185
+ url
1186
+ })
1187
+ });
1188
+ if (response.status !== 200) {
1189
+ continue;
1190
+ }
1191
+ try {
1192
+ latest = await response.json();
1193
+ } catch {
1194
+ continue;
1195
+ }
1196
+ if (latest.status !== "submitted") {
1197
+ return latest;
1198
+ }
1199
+ }
1200
+ return latest;
1201
+ }
1202
+ async postJson(step, path, body) {
1203
+ const url = `${this.baseUrl}${path}`;
1204
+ const serialized = JSON.stringify(body);
1205
+ const response = await this.fetchImpl(url, {
1206
+ method: "POST",
1207
+ headers: {
1208
+ ...await walletAuthHeaders({
1209
+ signer: this.signer,
1210
+ method: "POST",
1211
+ url,
1212
+ body: serialized
1213
+ }),
1214
+ "content-type": "application/json"
1215
+ },
1216
+ body: serialized
1217
+ });
1218
+ const text = await response.text();
1219
+ if (response.status !== 200) {
1220
+ throw new VaultFlowClientError(
1221
+ step,
1222
+ `${path} failed with ${response.status}: ${text}`,
1223
+ text
1224
+ );
1225
+ }
1226
+ try {
1227
+ return JSON.parse(text);
1228
+ } catch {
1229
+ throw new VaultFlowClientError(
1230
+ step,
1231
+ `${path} returned 200 with a non-JSON body`,
1232
+ text
1233
+ );
1234
+ }
1235
+ }
1236
+ };
1237
+
1022
1238
  // ../../src/solana/keys.ts
1023
1239
  import { readFileSync } from "node:fs";
1024
1240
  import bs586 from "bs58";
@@ -1063,7 +1279,7 @@ function formatRawUsdc(raw) {
1063
1279
  }
1064
1280
 
1065
1281
  // ../../demo/withdraw.ts
1066
- var facilitatorBaseUrl = process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
1282
+ var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
1067
1283
  var amountRawUsdc = process.argv[2];
1068
1284
  if (amountRawUsdc === void 0 || !/^[1-9]\d*$/.test(amountRawUsdc)) {
1069
1285
  fail(
@@ -1079,67 +1295,44 @@ var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
1079
1295
  var rpc = createRpc(
1080
1296
  process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com"
1081
1297
  );
1082
- async function postJson(path, body) {
1083
- const url = `${facilitatorBaseUrl}${path}`;
1084
- const serialized = JSON.stringify(body);
1085
- const response = await fetch(url, {
1086
- method: "POST",
1087
- headers: {
1088
- ...await walletAuthHeaders({
1089
- signer,
1090
- method: "POST",
1091
- url,
1092
- body: serialized
1093
- }),
1094
- "content-type": "application/json"
1095
- },
1096
- body: serialized
1097
- });
1098
- const text = await response.text();
1099
- if (response.status !== 200) {
1100
- fail(`[withdraw] ${path} failed with ${response.status}: ${text}`);
1101
- }
1102
- return JSON.parse(text);
1103
- }
1298
+ var vaultFlows = new VaultFlowClient({
1299
+ facilitatorBaseUrl: relayerBaseUrl,
1300
+ signer,
1301
+ rpc
1302
+ });
1104
1303
  console.log(`[withdraw] agent wallet: ${signer.walletAddress}`);
1105
- console.log(`[withdraw] facilitator: ${facilitatorBaseUrl}`);
1304
+ console.log(`[withdraw] relayer: ${relayerBaseUrl}`);
1106
1305
  console.log(
1107
1306
  `
1108
- [withdraw] step 1: prepare withdraw of ${formatRawUsdc(amountRawUsdc)} USDC`
1307
+ [withdraw] step 1: withdraw ${formatRawUsdc(amountRawUsdc)} USDC (prepare -> validate intent + sign locally -> sponsor submits)`
1109
1308
  );
1110
- var prepared = await postJson("/v1/withdrawals/prepare", {
1111
- wallet: signer.walletAddress,
1112
- amountRawUsdc
1113
- });
1114
- console.log(`[withdraw] prepared (withdrawalId=${prepared.withdrawalId})`);
1115
- console.log(`[withdraw] destination USDC ATA: ${prepared.destinationUsdcAta}`);
1116
- console.log(
1117
- "\n[withdraw] step 2: validate the signing intent against the transaction, sign locally"
1118
- );
1119
- var signed = await signer.signWithdrawal({
1120
- intent: prepared.signingIntent,
1121
- serializedTransaction: prepared.serializedTransaction,
1122
- lookupTables: await fetchLookupTablesForTransaction(
1123
- rpc,
1124
- prepared.serializedTransaction
1125
- )
1126
- });
1127
- console.log("[withdraw] agent signature attached");
1128
- console.log("\n[withdraw] step 3: submit (sponsor co-signs and broadcasts)");
1129
- var submitted = await postJson("/v1/withdrawals/submit", {
1130
- withdrawalId: prepared.withdrawalId,
1131
- serializedTransaction: signed.serializedTransaction,
1132
- agentSignature: signed.agentSignature
1133
- });
1309
+ var submitted;
1310
+ try {
1311
+ submitted = await vaultFlows.withdraw({
1312
+ amountRawUsdc: BigInt(amountRawUsdc)
1313
+ });
1314
+ } catch (error) {
1315
+ if (error instanceof VaultFlowClientError) {
1316
+ fail(`[withdraw] ${error.step} failed: ${error.message}`);
1317
+ }
1318
+ throw error;
1319
+ }
1320
+ console.log(`[withdraw] withdrawalId: ${submitted.withdrawalId}`);
1134
1321
  console.log(`[withdraw] status: ${submitted.status}`);
1322
+ console.log(`[withdraw] destination USDC ATA: ${submitted.destinationUsdcAta}`);
1135
1323
  if (submitted.txSignature !== null) {
1136
1324
  console.log(
1137
1325
  `[withdraw] transaction: https://solscan.io/tx/${submitted.txSignature}`
1138
1326
  );
1139
1327
  }
1328
+ if (submitted.status === "submitted") {
1329
+ fail(
1330
+ `[withdraw] broadcast but not yet confirmed \u2014 it may still land. Do NOT resubmit; check GET /v1/withdrawals/${submitted.withdrawalId} (or the tx link above) first`
1331
+ );
1332
+ }
1140
1333
  if (submitted.status !== "confirmed") {
1141
1334
  fail(
1142
- `[withdraw] not confirmed (errorCode=${submitted.errorCode}); check GET /v1/withdrawals/${prepared.withdrawalId} and the facilitator logs`
1335
+ `[withdraw] not confirmed (errorCode=${submitted.errorCode}); check GET /v1/withdrawals/${submitted.withdrawalId} and the relayer logs`
1143
1336
  );
1144
1337
  }
1145
1338
  console.log(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@subly_fi/pay",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Subly client: pay for ANY standard x402 (HTTP 402) paid API 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
  "license": "MIT",
6
6
  "type": "module",