@ic-pay/icpay-sdk 1.4.33 → 1.4.69

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/index.js CHANGED
@@ -57,6 +57,54 @@ function base58Encode(bytes) {
57
57
  out += B58_ALPHABET[digits[q]];
58
58
  return out;
59
59
  }
60
+ function base58Decode(s) {
61
+ if (!s || s.length === 0)
62
+ return new Uint8Array(0);
63
+ const MAP = {};
64
+ for (let i = 0; i < B58_ALPHABET.length; i++)
65
+ MAP[B58_ALPHABET.charAt(i)] = i;
66
+ let zeros = 0;
67
+ while (zeros < s.length && s.charAt(zeros) === '1')
68
+ zeros++;
69
+ const bytes = [0];
70
+ for (let i = zeros; i < s.length; i++) {
71
+ const ch = s.charAt(i);
72
+ const val = MAP[ch];
73
+ if (val === undefined)
74
+ throw new Error('invalid base58');
75
+ let carry = val;
76
+ for (let j = 0; j < bytes.length; j++) {
77
+ const prev = bytes[j];
78
+ const x = (prev * 58) + carry;
79
+ bytes[j] = x & 0xff;
80
+ carry = x >> 8;
81
+ }
82
+ while (carry > 0) {
83
+ bytes.push(carry & 0xff);
84
+ carry >>= 8;
85
+ }
86
+ }
87
+ const outArr = new Uint8Array(zeros + bytes.length);
88
+ let p = 0;
89
+ for (; p < zeros; p++)
90
+ outArr[p] = 0;
91
+ for (let q = bytes.length - 1; q >= 0; q--)
92
+ outArr[p++] = (bytes[q] == null ? 0 : bytes[q]);
93
+ return outArr;
94
+ }
95
+ function b64FromBytes(bytes) {
96
+ try {
97
+ const Buf = globalThis.Buffer;
98
+ if (Buf)
99
+ return Buf.from(bytes).toString('base64');
100
+ }
101
+ catch { }
102
+ let bin = '';
103
+ for (let i = 0; i < bytes.length; i++)
104
+ bin += String.fromCharCode(bytes[i]);
105
+ // btoa expects binary string
106
+ return globalThis?.btoa ? globalThis.btoa(bin) : '';
107
+ }
60
108
  function u8FromBase64(b64) {
61
109
  try {
62
110
  const Buf = globalThis.Buffer;
@@ -70,6 +118,28 @@ function u8FromBase64(b64) {
70
118
  arr[i] = bin.charCodeAt(i);
71
119
  return arr;
72
120
  }
121
+ // Normalize metadata so internal icpay-managed fields are nested under metadata.icpay.
122
+ // If icpay exists, merge; otherwise create. Move known internal keys and icpay_* keys under icpay.
123
+ function normalizeSdkMetadata(base) {
124
+ const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
125
+ const incoming = isObj(base) ? { ...base } : {};
126
+ const hasGroup = isObj(incoming.icpay);
127
+ const INTERNAL_KEYS = ['context', 'senderPrincipal', 'onrampProvider'];
128
+ const moved = {};
129
+ for (const [k, v] of Object.entries(incoming)) {
130
+ if (INTERNAL_KEYS.includes(k) || k.startsWith('icpay_')) {
131
+ moved[k] = v;
132
+ delete incoming[k];
133
+ }
134
+ }
135
+ if (hasGroup) {
136
+ incoming.icpay = { ...(incoming.icpay || {}), ...moved };
137
+ }
138
+ else {
139
+ incoming.icpay = moved;
140
+ }
141
+ return incoming;
142
+ }
73
143
  class Icpay {
74
144
  constructor(config) {
75
145
  this.privateApiClient = null;
@@ -85,7 +155,7 @@ class Icpay {
85
155
  debug: false,
86
156
  enableEvents: true,
87
157
  ...config,
88
- onrampDisabled: true,
158
+ onrampDisabled: false,
89
159
  };
90
160
  (0, utils_1.debugLog)(this.config.debug || false, 'constructor', { config: this.config });
91
161
  // Validate authentication configuration
@@ -465,6 +535,7 @@ class Icpay {
465
535
  case 'evm':
466
536
  case 'ethereum':
467
537
  return await this.processEvmPayment(params);
538
+ case 'sol':
468
539
  case 'solana':
469
540
  return await this.processSolanaPayment(params);
470
541
  case 'sui':
@@ -628,7 +699,35 @@ class Icpay {
628
699
  let canisterTransactionId;
629
700
  try {
630
701
  (0, utils_1.debugLog)(this.config.debug || false, 'notifying canister about ledger tx', { icpayCanisterId: this.icpayCanisterId, ledgerCanisterId, blockIndex });
631
- const notifyRes = await this.notifyLedgerTransaction(this.icpayCanisterId, ledgerCanisterId, BigInt(blockIndex));
702
+ // Attempt to decode accountCanisterId from packed memo (high 32 bits)
703
+ let acctFromMemo = undefined;
704
+ try {
705
+ if (memo && memo.length > 0) {
706
+ let big = 0n;
707
+ for (let i = 0; i < memo.length; i++) {
708
+ big |= BigInt(memo[i] & 0xff) << BigInt(8 * i);
709
+ }
710
+ const acct = Number(big >> 32n);
711
+ if (Number.isFinite(acct) && acct > 0)
712
+ acctFromMemo = acct;
713
+ }
714
+ }
715
+ catch { }
716
+ // Derive recipient principal for relay from request payload (IC address)
717
+ const recipientPrincipal = (() => {
718
+ const addrAny = request;
719
+ const icAddr = (addrAny?.recipientAddresses?.ic || addrAny?.recipientAddress || '').toString().trim();
720
+ // Heuristic: treat non-hex, non-empty as IC principal candidate
721
+ if (icAddr && !/^0x[a-fA-F0-9]{40}$/.test(icAddr))
722
+ return icAddr;
723
+ return undefined;
724
+ })();
725
+ const externalCostAmount = request.__externalCostAmount ?? request?.externalCostAmount ?? request?.metadata?.externalCostAmount;
726
+ const notifyRes = await this.notifyLedgerTransaction(this.icpayCanisterId, ledgerCanisterId, BigInt(blockIndex), {
727
+ accountCanisterId: acctFromMemo,
728
+ externalCostAmount,
729
+ recipientPrincipal
730
+ });
632
731
  if (typeof notifyRes === 'string') {
633
732
  const parsed = parseInt(notifyRes, 10);
634
733
  canisterTransactionId = Number.isFinite(parsed) ? parsed : undefined;
@@ -652,6 +751,7 @@ class Icpay {
652
751
  paymentIntentId: paymentIntentId,
653
752
  canisterTransactionId: (typeof canisterTransactionId === 'number' && Number.isFinite(canisterTransactionId)) ? String(canisterTransactionId) : undefined,
654
753
  ledgerCanisterId: ledgerCanisterId,
754
+ ledgerBlockIndex: blockIndex,
655
755
  amount: amount.toString(),
656
756
  metadata: metadata ?? request.metadata,
657
757
  });
@@ -828,9 +928,9 @@ class Icpay {
828
928
  // Prefer selectors from API; otherwise fallback to constants provided by backend
829
929
  const apiSelectors = params.request.__functionSelectors || {};
830
930
  const selector = {
831
- // API provides the overloaded signatures under these names
832
- payNative: apiSelectors.payNative || '0x4e47ff88', // payNative(bytes32,uint64,uint256)
833
- payERC20: apiSelectors.payERC20 || '0x87b9fed2', // payERC20(bytes32,uint64,address,uint256,uint256)
931
+ // Always use relay overloads; server maps payNative/payERC20 to relay selectors
932
+ payNative: apiSelectors.payNative || '0x8062dd66', // payNative(bytes32,uint64,uint256,address)
933
+ payERC20: apiSelectors.payERC20 || '0xc20b92c7', // payERC20(bytes32,uint64,address,uint256,uint256,address)
834
934
  };
835
935
  // Build EVM id bytes32 using shared helper
836
936
  const accountIdNum = BigInt(params.accountCanisterId || 0);
@@ -849,6 +949,7 @@ class Icpay {
849
949
  idHexLen: idHex?.length,
850
950
  selectorPayNative: selector.payNative,
851
951
  selectorPayERC20: selector.payERC20,
952
+ recipientAddress: params.request?.recipientAddress || null,
852
953
  });
853
954
  }
854
955
  catch { }
@@ -881,15 +982,21 @@ class Icpay {
881
982
  if (!owner)
882
983
  throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.WALLET_NOT_CONNECTED, message: 'EVM wallet not connected' });
883
984
  (0, utils_1.debugLog)(this.config.debug || false, 'evm from account', { owner });
985
+ const ZERO = '0x0000000000000000000000000000000000000000';
986
+ const reqAny = params.request;
987
+ const recipientPrimary = String(reqAny?.recipientAddress || '').trim();
988
+ const recipientFromMap = String((reqAny?.recipientAddresses || {})?.evm || '').trim();
989
+ const recipientCandidate = recipientPrimary || recipientFromMap;
990
+ const recipient = /^0x[a-fA-F0-9]{40}$/.test(recipientCandidate) ? recipientCandidate : ZERO;
884
991
  if (isNative) {
885
992
  const externalCostStr = params.request?.__externalCostAmount;
886
993
  const externalCost = externalCostStr != null && externalCostStr !== '' ? BigInt(String(externalCostStr)) : 0n;
887
994
  const extSel = selector.payNative;
888
995
  if (!extSel) {
889
- throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG, message: 'Missing payNative overload selector from API; update API/chain metadata.' });
996
+ throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG, message: 'Missing payNative selector from API; update API/chain metadata.' });
890
997
  }
891
- const data = extSel + idHex + toUint64(accountIdNum) + toUint256(externalCost);
892
- (0, utils_1.debugLog)(this.config.debug || false, 'evm native tx', { to: contractAddress, from: owner, dataLen: data.length, value: amountHex });
998
+ const data = extSel + idHex + toUint64(accountIdNum) + toUint256(externalCost) + toAddressPadded(recipient);
999
+ (0, utils_1.debugLog)(this.config.debug || false, 'evm native tx', { to: contractAddress, from: owner, dataLen: data.length, value: amountHex, recipient });
893
1000
  txHash = await eth.request({ method: 'eth_sendTransaction', params: [{ from: owner, to: contractAddress, data, value: amountHex }] });
894
1001
  }
895
1002
  else {
@@ -914,10 +1021,11 @@ class Icpay {
914
1021
  const externalCost = externalCostStr != null && externalCostStr !== '' ? BigInt(String(externalCostStr)) : 0n;
915
1022
  const extSel = selector.payERC20;
916
1023
  if (!extSel) {
917
- throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG, message: 'Missing payERC20 overload selector from API; update API/chain metadata.' });
1024
+ throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG, message: 'Missing payERC20 selector from API; update API/chain metadata.' });
918
1025
  }
919
- const data = extSel + idHex + toUint64(accountIdNum) + toAddressPadded(String(tokenAddress)) + toUint256(params.amount) + toUint256(externalCost);
920
- (0, utils_1.debugLog)(this.config.debug || false, 'evm erc20 pay', { to: contractAddress, from: owner, token: tokenAddress, dataLen: data.length });
1026
+ const base = idHex + toUint64(accountIdNum) + toAddressPadded(String(tokenAddress)) + toUint256(params.amount) + toUint256(externalCost);
1027
+ const data = extSel + base + toAddressPadded(recipient);
1028
+ (0, utils_1.debugLog)(this.config.debug || false, 'evm erc20 pay', { to: contractAddress, from: owner, token: tokenAddress, dataLen: data.length, recipient });
921
1029
  txHash = await eth.request({ method: 'eth_sendTransaction', params: [{ from: owner, to: contractAddress, data }] });
922
1030
  }
923
1031
  }
@@ -1082,7 +1190,8 @@ class Icpay {
1082
1190
  // Resolve ledgerCanisterId from symbol if needed (legacy). If tokenShortcode provided, no resolution required.
1083
1191
  let ledgerCanisterId = request.ledgerCanisterId;
1084
1192
  const tokenShortcode = request?.tokenShortcode;
1085
- if (!ledgerCanisterId && !tokenShortcode && !request.symbol) {
1193
+ const isOnrampFlow = (request?.onrampPayment === true) || (this?.config?.onrampPayment === true);
1194
+ if (!ledgerCanisterId && !tokenShortcode && !request.symbol && !isOnrampFlow) {
1086
1195
  const err = new errors_1.IcpayError({
1087
1196
  code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG,
1088
1197
  message: 'Provide either tokenShortcode or ledgerCanisterId (symbol is deprecated).',
@@ -1099,6 +1208,7 @@ class Icpay {
1099
1208
  let intentChainId;
1100
1209
  let accountCanisterId;
1101
1210
  let resolvedAmountStr = typeof request.amount === 'string' ? request.amount : (request.amount != null ? String(request.amount) : undefined);
1211
+ let intentResp;
1102
1212
  try {
1103
1213
  (0, utils_1.debugLog)(this.config.debug || false, 'creating payment intent');
1104
1214
  // Resolve expected sender principal:
@@ -1136,7 +1246,7 @@ class Icpay {
1136
1246
  catch { }
1137
1247
  }
1138
1248
  }
1139
- if (!expectedSenderPrincipal) {
1249
+ if (!expectedSenderPrincipal && !((request?.onrampPayment === true) || (this?.config?.onrampPayment === true))) {
1140
1250
  throw new errors_1.IcpayError({
1141
1251
  code: errors_1.ICPAY_ERROR_CODES.WALLET_NOT_CONNECTED,
1142
1252
  message: 'Wallet must be connected to create payment intent',
@@ -1148,7 +1258,19 @@ class Icpay {
1148
1258
  const onramp = (request.onrampPayment === true || this.config.onrampPayment === true) && this.config.onrampDisabled !== true ? true : false;
1149
1259
  const meta = request?.metadata || {};
1150
1260
  const isAtxp = Boolean(meta?.icpay_atxp_request) && typeof (meta?.atxp_request_id) === 'string';
1151
- let intentResp;
1261
+ // Resolve recipientAddress only for non-onramp flows
1262
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
1263
+ const reqAny = request;
1264
+ const addrObj = (reqAny?.recipientAddresses) || {};
1265
+ const candidateEvm = addrObj.evm ? addrObj.evm : undefined;
1266
+ const candidateIC = addrObj.ic ? addrObj.ic : undefined;
1267
+ const candidateSol = addrObj.sol ? addrObj.sol : undefined;
1268
+ let recipientAddress = undefined;
1269
+ if (!onramp) {
1270
+ // Choose a default to persist on the intent; EVM will override to ZERO if non-hex when building tx
1271
+ recipientAddress = (reqAny?.recipientAddress) || candidateEvm || candidateIC || candidateSol || ZERO_ADDRESS;
1272
+ (0, utils_1.debugLog)(this.config.debug || false, 'recipientAddress resolved for intent', { recipientAddress });
1273
+ }
1152
1274
  if (isAtxp) {
1153
1275
  // Route ATXP intents to the ATXP endpoint so they link to the request
1154
1276
  const atxpRequestId = String(meta.atxp_request_id);
@@ -1156,25 +1278,42 @@ class Icpay {
1156
1278
  intentResp = await this.publicApiClient.post(endpoint, {
1157
1279
  tokenShortcode: tokenShortcode || undefined,
1158
1280
  description: request.description,
1281
+ recipientAddress,
1282
+ recipientAddresses: request?.recipientAddresses || undefined,
1283
+ externalCostAmount: request?.externalCostAmount ?? request?.metadata?.externalCostAmount ?? undefined,
1159
1284
  });
1160
1285
  }
1161
1286
  else {
1162
- intentResp = await this.publicApiClient.post('/sdk/public/payments/intents', {
1163
- amount: (typeof request.amount === 'string' ? request.amount : (request.amount != null ? String(request.amount) : undefined)),
1164
- // Prefer tokenShortcode if provided
1165
- tokenShortcode: tokenShortcode || undefined,
1166
- // Legacy fields for backwards compatibility
1167
- symbol: tokenShortcode ? undefined : request.symbol,
1168
- ledgerCanisterId: tokenShortcode ? undefined : ledgerCanisterId,
1169
- description: request.description,
1170
- expectedSenderPrincipal,
1171
- metadata: request.metadata || {},
1172
- amountUsd: request.amountUsd,
1173
- // With tokenShortcode, backend derives chain. Keep legacy chainId for old flows.
1174
- chainId: tokenShortcode ? undefined : request.chainId,
1175
- onrampPayment: onramp || undefined,
1176
- widgetParams: request.widgetParams || undefined,
1177
- });
1287
+ if (onramp) {
1288
+ // Route onramp flows to the dedicated onramp endpoint without requiring token/ledger
1289
+ intentResp = await this.publicApiClient.post('/sdk/public/onramp/intents', {
1290
+ usdAmount: request.amountUsd,
1291
+ description: request.description,
1292
+ metadata: normalizeSdkMetadata(request.metadata || {}),
1293
+ widgetParams: request.widgetParams || undefined,
1294
+ recipientAddresses: request?.recipientAddresses || undefined,
1295
+ });
1296
+ }
1297
+ else {
1298
+ intentResp = await this.publicApiClient.post('/sdk/public/payments/intents', {
1299
+ amount: (typeof request.amount === 'string' ? request.amount : (request.amount != null ? String(request.amount) : undefined)),
1300
+ // Prefer tokenShortcode if provided
1301
+ tokenShortcode: tokenShortcode || undefined,
1302
+ // Legacy fields for backwards compatibility
1303
+ symbol: tokenShortcode ? undefined : request.symbol,
1304
+ ledgerCanisterId: tokenShortcode ? undefined : ledgerCanisterId,
1305
+ description: request.description,
1306
+ expectedSenderPrincipal,
1307
+ metadata: normalizeSdkMetadata(request.metadata || {}),
1308
+ amountUsd: request.amountUsd,
1309
+ // With tokenShortcode, backend derives chain. Keep legacy chainId for old flows.
1310
+ chainId: tokenShortcode ? undefined : request.chainId,
1311
+ widgetParams: request.widgetParams || undefined,
1312
+ recipientAddress,
1313
+ recipientAddresses: request?.recipientAddresses || undefined,
1314
+ externalCostAmount: request?.externalCostAmount ?? request?.metadata?.externalCostAmount ?? undefined,
1315
+ });
1316
+ }
1178
1317
  }
1179
1318
  paymentIntentId = intentResp?.paymentIntent?.id || null;
1180
1319
  paymentIntentCode = intentResp?.paymentIntent?.intentCode ?? null;
@@ -1195,15 +1334,28 @@ class Icpay {
1195
1334
  ledgerCanisterId = intentResp.paymentIntent.ledgerCanisterId;
1196
1335
  }
1197
1336
  (0, utils_1.debugLog)(this.config.debug || false, 'payment intent created', { paymentIntentId, paymentIntentCode, expectedSenderPrincipal, resolvedAmountStr });
1198
- // Emit transaction created event
1337
+ // Emit transaction created event only for non-onramp flows
1199
1338
  if (paymentIntentId) {
1200
- this.emit('icpay-sdk-transaction-created', {
1201
- paymentIntentId,
1202
- amount: resolvedAmountStr,
1203
- ledgerCanisterId,
1204
- expectedSenderPrincipal,
1205
- accountCanisterId,
1206
- });
1339
+ if (!isOnrampFlow) {
1340
+ this.emit('icpay-sdk-transaction-created', {
1341
+ paymentIntentId,
1342
+ amount: resolvedAmountStr,
1343
+ ledgerCanisterId,
1344
+ expectedSenderPrincipal,
1345
+ accountCanisterId,
1346
+ });
1347
+ }
1348
+ else {
1349
+ // Optional: emit an onramp-specific event for UI
1350
+ try {
1351
+ this.emit?.('icpay-sdk-onramp-intent-created', {
1352
+ paymentIntentId,
1353
+ amountUsd: request.amountUsd,
1354
+ onramp: intentResp?.onramp,
1355
+ });
1356
+ }
1357
+ catch { }
1358
+ }
1207
1359
  }
1208
1360
  request.__onramp = onrampData;
1209
1361
  request.__contractAddress = contractAddress;
@@ -1234,6 +1386,18 @@ class Icpay {
1234
1386
  this.emitError(err);
1235
1387
  throw err;
1236
1388
  }
1389
+ // If this is an onramp flow, do NOT proceed with chain processing. Return onramp payload instead.
1390
+ const onrampPayload = request.__onramp;
1391
+ if (isOnrampFlow && onrampPayload) {
1392
+ const early = {
1393
+ paymentIntentId,
1394
+ paymentIntentCode,
1395
+ onramp: onrampPayload,
1396
+ paymentIntent: intentResp?.paymentIntent || null,
1397
+ };
1398
+ this.emitMethodSuccess('createPayment', early);
1399
+ return early;
1400
+ }
1237
1401
  const amount = BigInt(resolvedAmountStr);
1238
1402
  // Build packed memo
1239
1403
  const acctIdNum = parseInt(accountCanisterId);
@@ -1376,7 +1540,7 @@ class Icpay {
1376
1540
  /**
1377
1541
  * Notify canister about ledger transaction using anonymous actor (no signature required)
1378
1542
  */
1379
- async notifyLedgerTransaction(canisterId, ledgerCanisterId, blockIndex) {
1543
+ async notifyLedgerTransaction(canisterId, ledgerCanisterId, blockIndex, opts) {
1380
1544
  this.emitMethodStart('notifyLedgerTransaction', { canisterId, ledgerCanisterId, blockIndex: blockIndex.toString() });
1381
1545
  // Create anonymous actor for canister notifications (no signature required)
1382
1546
  // Retry on transient certificate TrustError (clock skew)
@@ -1386,11 +1550,45 @@ class Icpay {
1386
1550
  try {
1387
1551
  const agent = new agent_1.HttpAgent({ host: this.icHost });
1388
1552
  const actor = agent_1.Actor.createActor(icpay_canister_backend_did_js_1.idlFactory, { agent, canisterId });
1389
- result = await actor.notify_ledger_transaction({
1390
- // Canister expects text for ledger_canister_id
1391
- ledger_canister_id: ledgerCanisterId,
1392
- block_index: blockIndex
1393
- });
1553
+ // Prefer v2 when available and we have required inputs
1554
+ const maybeV2 = actor?.notify_ledger_transaction_v2;
1555
+ const haveAcct = typeof opts?.accountCanisterId === 'number' && Number.isFinite(opts.accountCanisterId);
1556
+ if (maybeV2 && haveAcct) {
1557
+ try {
1558
+ (0, utils_1.debugLog)(this.config.debug || false, 'notify using v2', { ledgerCanisterId, blockIndex: blockIndex.toString(), accountCanisterId: opts?.accountCanisterId, hasExternalCost: opts?.externalCostAmount != null, hasRecipient: Boolean((opts?.recipientPrincipal || '').trim()) });
1559
+ }
1560
+ catch { }
1561
+ const externalCost = (() => {
1562
+ if (opts?.externalCostAmount == null)
1563
+ return [];
1564
+ try {
1565
+ const v = BigInt(String(opts.externalCostAmount));
1566
+ if (v < 0n)
1567
+ return [];
1568
+ return [v];
1569
+ }
1570
+ catch {
1571
+ return [];
1572
+ }
1573
+ })();
1574
+ const recipient = (() => {
1575
+ const s = (opts?.recipientPrincipal || '').trim();
1576
+ return s ? [s] : [];
1577
+ })();
1578
+ result = await actor.notify_ledger_transaction_v2({ ledger_canister_id: ledgerCanisterId, block_index: blockIndex }, BigInt(opts.accountCanisterId), externalCost, recipient);
1579
+ }
1580
+ else {
1581
+ try {
1582
+ (0, utils_1.debugLog)(this.config.debug || false, 'notify using v1 (fallback)', { ledgerCanisterId, blockIndex: blockIndex.toString(), haveAcct });
1583
+ }
1584
+ catch { }
1585
+ // Fallback to legacy notify
1586
+ result = await actor.notify_ledger_transaction({
1587
+ // Canister expects text for ledger_canister_id
1588
+ ledger_canister_id: ledgerCanisterId,
1589
+ block_index: blockIndex
1590
+ });
1591
+ }
1394
1592
  break;
1395
1593
  }
1396
1594
  catch (e) {
@@ -1747,7 +1945,8 @@ class Icpay {
1747
1945
  // If tokenShortcode provided, skip canister resolution; otherwise resolve from symbol if needed
1748
1946
  const tokenShortcode = request?.tokenShortcode;
1749
1947
  let ledgerCanisterId = request.ledgerCanisterId;
1750
- if (!ledgerCanisterId && !tokenShortcode && !request.symbol) {
1948
+ const isOnrampFlow = request?.onrampPayment === true || this?.config?.onrampPayment === true;
1949
+ if (!ledgerCanisterId && !tokenShortcode && !request.symbol && !isOnrampFlow) {
1751
1950
  const err = new errors_1.IcpayError({
1752
1951
  code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG,
1753
1952
  message: 'Provide either tokenShortcode or ledgerCanisterId (symbol is deprecated).',
@@ -1767,6 +1966,8 @@ class Icpay {
1767
1966
  onrampPayment: request.onrampPayment,
1768
1967
  widgetParams: request.widgetParams,
1769
1968
  chainId: tokenShortcode ? undefined : request.chainId,
1969
+ recipientAddress: request?.recipientAddress || '0x0000000000000000000000000000000000000000',
1970
+ recipientAddresses: request?.recipientAddresses,
1770
1971
  };
1771
1972
  const res = await this.createPayment(createTransactionRequest);
1772
1973
  this.emitMethodSuccess('createPaymentUsd', res);
@@ -1807,10 +2008,21 @@ class Icpay {
1807
2008
  symbol: tokenShortcode ? undefined : request.symbol,
1808
2009
  ledgerCanisterId: tokenShortcode ? undefined : ledgerCanisterId,
1809
2010
  description: request.description,
1810
- metadata: request.metadata,
2011
+ metadata: normalizeSdkMetadata(request.metadata || {}),
1811
2012
  chainId: tokenShortcode ? undefined : request.chainId,
1812
2013
  x402: true,
2014
+ recipientAddress: request?.recipientAddress || '0x0000000000000000000000000000000000000000',
1813
2015
  };
2016
+ // Include Solana payerPublicKey if available so server can build unsigned tx inline for Solana x402
2017
+ try {
2018
+ const w = globalThis?.window || globalThis;
2019
+ const sol = this.config?.solanaProvider || w?.solana || w?.phantom?.solana;
2020
+ const pk = sol?.publicKey?.toBase58?.() || sol?.publicKey || null;
2021
+ if (pk && typeof pk === 'string') {
2022
+ body.payerPublicKey = pk;
2023
+ }
2024
+ }
2025
+ catch { }
1814
2026
  try {
1815
2027
  const resp = await this.publicApiClient.post('/sdk/public/payments/intents/x402', body);
1816
2028
  // If backend indicates x402 is unavailable (failed + fallback), immediately switch to normal flow
@@ -1857,13 +2069,1316 @@ class Icpay {
1857
2069
  const acceptsArr = Array.isArray(data?.accepts) ? data.accepts : [];
1858
2070
  let requirement = acceptsArr.length > 0 ? acceptsArr[0] : null;
1859
2071
  if (requirement) {
2072
+ // Determine network once for error handling policy
2073
+ const isSol = typeof requirement?.network === 'string' && String(requirement.network).toLowerCase().startsWith('solana:');
1860
2074
  try {
1861
- const paymentHeader = await (0, builders_1.buildAndSignX402PaymentHeader)(requirement, {
1862
- x402Version: Number(data?.x402Version || 1),
1863
- debug: this.config?.debug || false,
1864
- provider: this.config?.evmProvider || (typeof globalThis?.ethereum !== 'undefined' ? globalThis.ethereum : undefined),
1865
- });
1866
- // Start verification stage while we wait for settlement to process
2075
+ const providerForHeader = isSol
2076
+ ? (this.config?.solanaProvider || globalThis?.solana || globalThis?.phantom?.solana)
2077
+ : (this.config?.evmProvider || globalThis?.ethereum);
2078
+ let paymentHeader = null;
2079
+ // IC x402: client-side settle via allowance + canister pull
2080
+ const isIc = typeof requirement?.network === 'string' && String(requirement.network).toLowerCase().startsWith('icp:');
2081
+ if (isIc) {
2082
+ // IC x402: client ensures allowance; then call API settle so services (controller) pulls and notifies
2083
+ if (!this.actorProvider) {
2084
+ throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.WALLET_PROVIDER_NOT_AVAILABLE, message: 'actorProvider required for IC x402' });
2085
+ }
2086
+ // Ensure allowance first
2087
+ const asset = String(requirement?.asset || requirement?.payTo || '').trim();
2088
+ const amountStr = String(requirement?.maxAmountRequired || '0');
2089
+ const amountBn = BigInt(amountStr);
2090
+ if (!asset || amountBn <= 0n) {
2091
+ throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.API_ERROR, message: 'Invalid x402 IC requirement (asset/amount)' });
2092
+ }
2093
+ // Approve spender (ICPay canister) for amount
2094
+ if (!this.icpayCanisterId) {
2095
+ // Prefer payTo from x402 requirement as ICPay canister id if present
2096
+ try {
2097
+ const maybe = String(requirement?.payTo || '');
2098
+ if (maybe) {
2099
+ // Validate shape by attempting to parse
2100
+ principal_1.Principal.fromText(maybe);
2101
+ this.icpayCanisterId = maybe;
2102
+ }
2103
+ }
2104
+ catch { }
2105
+ }
2106
+ if (!this.icpayCanisterId) {
2107
+ // Fallback to account lookup
2108
+ const acctInfo = await this.fetchAccountInfo();
2109
+ this.icpayCanisterId = acctInfo?.icpayCanisterId?.toString?.() || this.icpayCanisterId;
2110
+ }
2111
+ if (!this.icpayCanisterId) {
2112
+ throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG, message: 'Missing ICPay canister id for IC x402' });
2113
+ }
2114
+ const ledgerActor = this.actorProvider(asset, ledger_did_js_1.idlFactory);
2115
+ try {
2116
+ // Fetch transfer fee and approve amount + fee to avoid InsufficientAllowance
2117
+ let feeBn = 0n;
2118
+ try {
2119
+ const f = await ledgerActor.icrc1_fee();
2120
+ feeBn = typeof f === 'bigint' ? f : BigInt(f);
2121
+ }
2122
+ catch { }
2123
+ const approveAmount = amountBn + (feeBn > 0n ? feeBn : 0n);
2124
+ await ledgerActor.icrc2_approve({
2125
+ fee: [],
2126
+ memo: [],
2127
+ from_subaccount: [],
2128
+ created_at_time: [],
2129
+ amount: approveAmount,
2130
+ expected_allowance: [],
2131
+ expires_at: [],
2132
+ spender: { owner: principal_1.Principal.fromText(this.icpayCanisterId), subaccount: [] },
2133
+ });
2134
+ }
2135
+ catch (apprErr) {
2136
+ throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.TRANSACTION_FAILED, message: 'ICRC-2 approve failed', details: apprErr });
2137
+ }
2138
+ // Obtain payer principal if available
2139
+ let payerPrincipal = null;
2140
+ try {
2141
+ const p = this.wallet.getPrincipal();
2142
+ if (p)
2143
+ payerPrincipal = p.toText();
2144
+ else if (this.connectedWallet && typeof this.connectedWallet.getPrincipal === 'function') {
2145
+ const maybe = await this.connectedWallet.getPrincipal();
2146
+ if (typeof maybe === 'string')
2147
+ payerPrincipal = maybe;
2148
+ }
2149
+ else if (this.connectedWallet?.principal) {
2150
+ payerPrincipal = String(this.connectedWallet.principal);
2151
+ }
2152
+ }
2153
+ catch { }
2154
+ // Build memo from accountCanisterId and intentCode for matching on services
2155
+ let memoBytes = undefined;
2156
+ try {
2157
+ const extra = requirement?.extra || {};
2158
+ const accIdStr = String(extra?.accountCanisterId || '');
2159
+ const icIntentCodeStr = String(extra?.intentCode || '');
2160
+ const accIdNum = accIdStr ? parseInt(accIdStr, 10) : 0;
2161
+ const icIntentCodeNum = icIntentCodeStr ? parseInt(icIntentCodeStr, 10) : 0;
2162
+ if (Number.isFinite(accIdNum) && accIdNum > 0 && Number.isFinite(icIntentCodeNum) && icIntentCodeNum > 0) {
2163
+ const packed = this.createPackedMemo(accIdNum, icIntentCodeNum);
2164
+ memoBytes = Array.from(packed);
2165
+ }
2166
+ }
2167
+ catch { }
2168
+ // Call API to settle IC x402 via services (controller will pull + notify)
2169
+ const settleRespIc = await this.publicApiClient.post('/sdk/public/payments/x402/settle', {
2170
+ paymentIntentId,
2171
+ paymentHeader: null, // not used for IC allowance path
2172
+ paymentRequirements: requirement,
2173
+ payerPrincipal,
2174
+ memoBytes: memoBytes || null,
2175
+ });
2176
+ const statusIc = (settleRespIc?.status || settleRespIc?.paymentIntent?.status || 'completed').toString().toLowerCase();
2177
+ const amountIc = (settleRespIc?.paymentIntent?.amount && String(settleRespIc.paymentIntent.amount)) ||
2178
+ (typeof usdAmount === 'number' ? String(usdAmount) : request?.amount?.toString?.() || '0');
2179
+ const outIc = {
2180
+ transactionId: Number(settleRespIc?.canisterTxId || 0),
2181
+ status: statusIc === 'succeeded' ? 'completed' : statusIc,
2182
+ amount: amountIc,
2183
+ recipientCanister: this.icpayCanisterId,
2184
+ timestamp: new Date(),
2185
+ metadata: { ...(request.metadata || {}), icpay_x402: true, icpay_network: 'ic' },
2186
+ payment: settleRespIc || null,
2187
+ };
2188
+ const isTerminalIc = (() => {
2189
+ const s = String(outIc.status || '').toLowerCase();
2190
+ return s === 'completed' || s === 'succeeded' || s === 'failed' || s === 'canceled' || s === 'cancelled' || s === 'mismatched';
2191
+ })();
2192
+ if (isTerminalIc) {
2193
+ if (outIc.status === 'completed') {
2194
+ this.emit('icpay-sdk-transaction-completed', outIc);
2195
+ }
2196
+ else if (outIc.status === 'failed') {
2197
+ this.emit('icpay-sdk-transaction-failed', outIc);
2198
+ }
2199
+ else {
2200
+ this.emit('icpay-sdk-transaction-updated', outIc);
2201
+ }
2202
+ this.emitMethodSuccess('createPaymentX402Usd', outIc);
2203
+ return outIc;
2204
+ }
2205
+ try {
2206
+ this.emit('icpay-sdk-transaction-updated', outIc);
2207
+ }
2208
+ catch { }
2209
+ const waitedIc = await this.awaitIntentTerminal({
2210
+ paymentIntentId,
2211
+ ledgerCanisterId: asset,
2212
+ amount: amountIc,
2213
+ canisterTransactionId: String(settleRespIc?.canisterTxId || ''),
2214
+ metadata: { ...(request.metadata || {}), icpay_x402: true, icpay_network: 'ic' },
2215
+ });
2216
+ this.emitMethodSuccess('createPaymentX402Usd', waitedIc);
2217
+ return waitedIc;
2218
+ }
2219
+ if (!isSol) {
2220
+ paymentHeader = await (0, builders_1.buildAndSignX402PaymentHeader)(requirement, {
2221
+ x402Version: Number(data?.x402Version || 2),
2222
+ debug: this.config?.debug || false,
2223
+ provider: providerForHeader,
2224
+ });
2225
+ }
2226
+ if (isSol) {
2227
+ // Solana x402: prefer message-sign flow if server provided a signable message
2228
+ const signableMsgB64 = requirement?.extra?.signableMessageBase64;
2229
+ const signableFields = requirement?.extra?.signableFields || {};
2230
+ const wSolCtx = globalThis?.window || globalThis;
2231
+ const sol = providerForHeader;
2232
+ if (!sol)
2233
+ throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.WALLET_PROVIDER_NOT_AVAILABLE, message: 'Solana provider not available (window.solana)' });
2234
+ // Get public key (already connected from widget)
2235
+ let fromBase58 = null;
2236
+ try {
2237
+ fromBase58 = sol?.publicKey?.toBase58?.() || sol?.publicKey || null;
2238
+ }
2239
+ catch { }
2240
+ if (!fromBase58 && typeof sol.connect === 'function') {
2241
+ try {
2242
+ const con = await sol.connect({ onlyIfTrusted: false });
2243
+ fromBase58 = con?.publicKey?.toBase58?.() || con?.publicKey || null;
2244
+ }
2245
+ catch { }
2246
+ }
2247
+ if (!fromBase58)
2248
+ throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.WALLET_NOT_CONNECTED, message: 'Solana wallet not connected' });
2249
+ // No ICPay popups; only wallet UI should be shown for signing
2250
+ if (signableMsgB64) {
2251
+ // Sign the provided message and settle via header (services will submit)
2252
+ // Ensure explicit connect prompt before signing
2253
+ if (typeof sol?.connect === 'function') {
2254
+ try {
2255
+ await sol.connect({ onlyIfTrusted: false });
2256
+ }
2257
+ catch { }
2258
+ }
2259
+ let sigB64 = null;
2260
+ const msgBytes = u8FromBase64(signableMsgB64);
2261
+ const msgB58ForReq = base58Encode(msgBytes);
2262
+ // Attempts in order (strict):
2263
+ // 1) Wallet Standard: request colon form with Uint8Array
2264
+ if (!sigB64 && sol?.request) {
2265
+ try {
2266
+ try {
2267
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(request) params', { method: 'solana:signMessage', shape: 'object{message:Uint8Array}', len: msgBytes.length });
2268
+ }
2269
+ catch { }
2270
+ const r0 = await sol.request({ method: 'solana:signMessage', params: { message: msgBytes } });
2271
+ if (typeof r0 === 'string') {
2272
+ try {
2273
+ const b = base58Decode(r0);
2274
+ sigB64 = b64FromBytes(b);
2275
+ }
2276
+ catch {
2277
+ sigB64 = r0;
2278
+ }
2279
+ }
2280
+ else if (r0 && typeof r0.signature === 'string') {
2281
+ try {
2282
+ const b = base58Decode(r0.signature);
2283
+ sigB64 = b64FromBytes(b);
2284
+ }
2285
+ catch {
2286
+ sigB64 = r0.signature;
2287
+ }
2288
+ }
2289
+ else if (r0 && (r0.byteLength != null || ArrayBuffer.isView(r0))) {
2290
+ const b = r0 instanceof Uint8Array ? r0 : new Uint8Array(r0);
2291
+ if (b && b.length === 64)
2292
+ sigB64 = b64FromBytes(b);
2293
+ }
2294
+ else if (r0 && typeof r0 === 'object' && Array.isArray(r0.data)) {
2295
+ const b = Uint8Array.from(r0.data);
2296
+ if (b && b.length === 64)
2297
+ sigB64 = b64FromBytes(b);
2298
+ }
2299
+ }
2300
+ catch (e0) {
2301
+ try {
2302
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol solana:signMessage failed', { error: String(e0) });
2303
+ }
2304
+ catch { }
2305
+ }
2306
+ }
2307
+ // 2) Native: signMessage(Uint8Array)
2308
+ if (!sigB64 && typeof sol?.signMessage === 'function') {
2309
+ try {
2310
+ try {
2311
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(fn) Uint8Array');
2312
+ }
2313
+ catch { }
2314
+ const r2 = await sol.signMessage(msgBytes);
2315
+ if (r2 && (r2.byteLength != null || ArrayBuffer.isView(r2))) {
2316
+ const b = r2 instanceof Uint8Array ? r2 : new Uint8Array(r2);
2317
+ if (b && b.length === 64)
2318
+ sigB64 = b64FromBytes(b);
2319
+ }
2320
+ else if (typeof r2 === 'string') {
2321
+ try {
2322
+ const b = base58Decode(r2);
2323
+ sigB64 = b64FromBytes(b);
2324
+ }
2325
+ catch {
2326
+ sigB64 = r2;
2327
+ }
2328
+ }
2329
+ else if (r2 && typeof r2 === 'object' && typeof r2.signature === 'string') {
2330
+ const s = r2.signature;
2331
+ try {
2332
+ const b = base58Decode(s);
2333
+ sigB64 = b64FromBytes(b);
2334
+ }
2335
+ catch {
2336
+ sigB64 = s;
2337
+ }
2338
+ }
2339
+ }
2340
+ catch (e2) {
2341
+ try {
2342
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(fn) failed', { error: String(e2) });
2343
+ }
2344
+ catch { }
2345
+ }
2346
+ }
2347
+ // 3) Request: signMessage with Uint8Array payload (legacy method name)
2348
+ if (!sigB64 && sol?.request) {
2349
+ try {
2350
+ try {
2351
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(request) params', { method: 'signMessage', shape: 'object{message:Uint8Array}', len: msgBytes.length });
2352
+ }
2353
+ catch { }
2354
+ const r3 = await sol.request({ method: 'signMessage', params: { message: msgBytes } });
2355
+ if (typeof r3 === 'string') {
2356
+ try {
2357
+ const b = base58Decode(r3);
2358
+ sigB64 = b64FromBytes(b);
2359
+ }
2360
+ catch {
2361
+ sigB64 = r3;
2362
+ }
2363
+ }
2364
+ else if (r3 && typeof r3.signature === 'string') {
2365
+ try {
2366
+ const b = base58Decode(r3.signature);
2367
+ sigB64 = b64FromBytes(b);
2368
+ }
2369
+ catch {
2370
+ sigB64 = r3.signature;
2371
+ }
2372
+ }
2373
+ else if (r3 && (r3.byteLength != null || ArrayBuffer.isView(r3))) {
2374
+ const b = r3 instanceof Uint8Array ? r3 : new Uint8Array(r3);
2375
+ if (b && b.length === 64)
2376
+ sigB64 = b64FromBytes(b);
2377
+ }
2378
+ else if (r3 && typeof r3 === 'object' && Array.isArray(r3.data)) {
2379
+ const b = Uint8Array.from(r3.data);
2380
+ if (b && b.length === 64)
2381
+ sigB64 = b64FromBytes(b);
2382
+ }
2383
+ }
2384
+ catch (e3) {
2385
+ try {
2386
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(request Uint8Array) failed', { error: String(e3) });
2387
+ }
2388
+ catch { }
2389
+ }
2390
+ }
2391
+ // 4) Request: signMessage with base58
2392
+ if (!sigB64 && sol?.request) {
2393
+ try {
2394
+ try {
2395
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(request) params', { method: 'signMessage', shape: 'object{message:base58}', len: msgB58ForReq.length });
2396
+ }
2397
+ catch { }
2398
+ const r4 = await sol.request({ method: 'signMessage', params: { message: msgB58ForReq } });
2399
+ if (typeof r4 === 'string') {
2400
+ try {
2401
+ const b = base58Decode(r4);
2402
+ sigB64 = b64FromBytes(b);
2403
+ }
2404
+ catch {
2405
+ sigB64 = r4;
2406
+ }
2407
+ }
2408
+ else if (r4 && typeof r4.signature === 'string') {
2409
+ try {
2410
+ const b = base58Decode(r4.signature);
2411
+ sigB64 = b64FromBytes(b);
2412
+ }
2413
+ catch {
2414
+ sigB64 = r4.signature;
2415
+ }
2416
+ }
2417
+ else if (r4 && (r4.byteLength != null || ArrayBuffer.isView(r4))) {
2418
+ const b = r4 instanceof Uint8Array ? r4 : new Uint8Array(r4);
2419
+ if (b && b.length === 64)
2420
+ sigB64 = b64FromBytes(b);
2421
+ }
2422
+ else if (r4 && typeof r4 === 'object' && Array.isArray(r4.data)) {
2423
+ const b = Uint8Array.from(r4.data);
2424
+ if (b && b.length === 64)
2425
+ sigB64 = b64FromBytes(b);
2426
+ }
2427
+ }
2428
+ catch (e4) {
2429
+ try {
2430
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(request legacy) failed', { error: String(e4) });
2431
+ }
2432
+ catch { }
2433
+ }
2434
+ }
2435
+ if (sigB64) {
2436
+ // Build x402 header and settle
2437
+ const header = (0, builders_1.buildX402HeaderFromAuthorization)({
2438
+ x402Version: Number(data?.x402Version || 2),
2439
+ scheme: String(requirement?.scheme || 'exact'),
2440
+ network: String(requirement?.network || ''),
2441
+ from: String(fromBase58 || ''),
2442
+ to: String(requirement?.payTo || ''),
2443
+ value: String(requirement?.maxAmountRequired || '0'),
2444
+ validAfter: String(signableFields?.validAfter || '0'),
2445
+ validBefore: String(signableFields?.validBefore || '0'),
2446
+ nonce: String(signableFields?.nonceHex || ''),
2447
+ signature: String(sigB64),
2448
+ });
2449
+ const headerJson = JSON.stringify(header);
2450
+ const headerB64 = (() => {
2451
+ try {
2452
+ const Buf = globalThis.Buffer;
2453
+ return Buf ? Buf.from(headerJson, 'utf8').toString('base64') : globalThis?.btoa?.(headerJson) || '';
2454
+ }
2455
+ catch {
2456
+ return '';
2457
+ }
2458
+ })();
2459
+ const settleRespSol = await this.publicApiClient.post('/sdk/public/payments/x402/settle', {
2460
+ paymentIntentId,
2461
+ paymentHeader: headerB64,
2462
+ paymentRequirements: requirement,
2463
+ });
2464
+ try {
2465
+ (0, utils_1.debugLog)(this.config?.debug || false, 'x402 (sol) settle via header response', {
2466
+ ok: settleRespSol?.ok,
2467
+ status: settleRespSol?.status,
2468
+ paymentIntentId: settleRespSol?.paymentIntent?.id,
2469
+ paymentId: settleRespSol?.payment?.id,
2470
+ rawKeys: Object.keys(settleRespSol || {}),
2471
+ });
2472
+ }
2473
+ catch { }
2474
+ const statusSolHdr = (settleRespSol?.status || settleRespSol?.paymentIntent?.status || 'completed').toString().toLowerCase();
2475
+ const amountSolHdr = (settleRespSol?.paymentIntent?.amount && String(settleRespSol.paymentIntent.amount)) ||
2476
+ (typeof usdAmount === 'number' ? String(usdAmount) : request?.amount?.toString?.() || '0');
2477
+ const outSolHdr = {
2478
+ transactionId: Number(settleRespSol?.canisterTxId || 0),
2479
+ status: statusSolHdr === 'succeeded' ? 'completed' : statusSolHdr,
2480
+ amount: amountSolHdr,
2481
+ recipientCanister: ledgerCanisterId,
2482
+ timestamp: new Date(),
2483
+ metadata: { ...(request.metadata || {}), icpay_x402: true },
2484
+ payment: settleRespSol || null,
2485
+ };
2486
+ const isTerminalSolHdr = (() => {
2487
+ const s = String(outSolHdr.status || '').toLowerCase();
2488
+ return s === 'completed' || s === 'succeeded' || s === 'failed' || s === 'canceled' || s === 'cancelled' || s === 'mismatched';
2489
+ })();
2490
+ if (isTerminalSolHdr) {
2491
+ if (outSolHdr.status === 'completed') {
2492
+ this.emit('icpay-sdk-transaction-completed', outSolHdr);
2493
+ }
2494
+ else if (outSolHdr.status === 'failed') {
2495
+ this.emit('icpay-sdk-transaction-failed', outSolHdr);
2496
+ }
2497
+ else {
2498
+ this.emit('icpay-sdk-transaction-updated', outSolHdr);
2499
+ }
2500
+ this.emitMethodSuccess('createPaymentX402Usd', outSolHdr);
2501
+ return outSolHdr;
2502
+ }
2503
+ // Non-terminal: wait until terminal via notify loop
2504
+ try {
2505
+ this.emit('icpay-sdk-transaction-updated', outSolHdr);
2506
+ }
2507
+ catch { }
2508
+ const waitedSolHdr = await this.awaitIntentTerminal({
2509
+ paymentIntentId,
2510
+ ledgerCanisterId: ledgerCanisterId,
2511
+ amount: amountSolHdr,
2512
+ metadata: { ...(request.metadata || {}), icpay_x402: true },
2513
+ });
2514
+ this.emitMethodSuccess('createPaymentX402Usd', waitedSolHdr);
2515
+ return waitedSolHdr;
2516
+ }
2517
+ else {
2518
+ // Fallback: if API provided an unsigned transaction, try transaction-signing path
2519
+ const fallbackTx = requirement?.extra?.transactionBase64;
2520
+ if (!fallbackTx) {
2521
+ throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.TRANSACTION_FAILED, message: 'Wallet did not sign message' });
2522
+ }
2523
+ // Inject for transaction-signing fallback below
2524
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2525
+ const __fallbackTxBase64 = String(fallbackTx);
2526
+ // Reassign txBase64 by creating a new block scope later; use a marker in metadata to indicate fallback used
2527
+ try {
2528
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol x402 fallback to transaction signing');
2529
+ }
2530
+ catch { }
2531
+ // Use local variable for fallback path
2532
+ let signedTxB64 = null;
2533
+ let signerSigBase58 = null;
2534
+ const inlineMsgB58Fallback = requirement?.extra?.messageBase58 || undefined;
2535
+ // Ensure explicit connect to prompt wallet if needed
2536
+ if (typeof sol?.connect === 'function') {
2537
+ try {
2538
+ await sol.connect({ onlyIfTrusted: false });
2539
+ }
2540
+ catch { }
2541
+ }
2542
+ // Do NOT submit from wallet; only sign, then relay to backend
2543
+ // Try signAllTransactions (array) then signTransaction with message:base58 (prefer) then transaction:base64
2544
+ if (sol?.request) {
2545
+ try {
2546
+ let r = null;
2547
+ // 0) Try Wallet Standard signAllTransactions with Uint8Array
2548
+ try {
2549
+ const txBytes = u8FromBase64(__fallbackTxBase64);
2550
+ try {
2551
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signAllTransactions(request) params', { method: 'solana:signAllTransactions', shape: 'object{transactions:Uint8Array[]}', count: 1, txLen: txBytes.length });
2552
+ }
2553
+ catch { }
2554
+ r = await sol.request({ method: 'solana:signAllTransactions', params: { transactions: [txBytes] } });
2555
+ }
2556
+ catch { }
2557
+ // 0b) Legacy signAllTransactions with Uint8Array
2558
+ if (!r) {
2559
+ try {
2560
+ const txBytes = u8FromBase64(__fallbackTxBase64);
2561
+ try {
2562
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signAllTransactions(request) params', { method: 'signAllTransactions', shape: 'object{transactions:Uint8Array[]}', count: 1, txLen: txBytes.length });
2563
+ }
2564
+ catch { }
2565
+ r = await sol.request({ method: 'signAllTransactions', params: { transactions: [txBytes] } });
2566
+ }
2567
+ catch { }
2568
+ }
2569
+ // 0c) signAllTransactions with base64 strings
2570
+ if (!r) {
2571
+ try {
2572
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signAllTransactions(request) params', { method: 'solana:signAllTransactions', shape: 'object{transactions:base64[]}', count: 1, txLen: __fallbackTxBase64.length });
2573
+ }
2574
+ catch { }
2575
+ try {
2576
+ r = await sol.request({ method: 'solana:signAllTransactions', params: { transactions: [__fallbackTxBase64] } });
2577
+ }
2578
+ catch { }
2579
+ }
2580
+ if (!r) {
2581
+ try {
2582
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signAllTransactions(request) params', { method: 'signAllTransactions', shape: 'object{transactions:base64[]}', count: 1, txLen: __fallbackTxBase64.length });
2583
+ }
2584
+ catch { }
2585
+ try {
2586
+ r = await sol.request({ method: 'signAllTransactions', params: { transactions: [__fallbackTxBase64] } });
2587
+ }
2588
+ catch { }
2589
+ }
2590
+ if (r) {
2591
+ // Normalize array responses
2592
+ const arr = Array.isArray(r) ? r : (Array.isArray(r?.transactions) ? r.transactions : null);
2593
+ if (arr && arr.length > 0) {
2594
+ const first = arr[0];
2595
+ if (typeof first === 'string') {
2596
+ const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(first) && first.length % 4 === 0;
2597
+ if (looksBase64)
2598
+ signedTxB64 = first;
2599
+ }
2600
+ else if (first && (first.byteLength != null || ArrayBuffer.isView(first))) {
2601
+ const b = first instanceof Uint8Array ? first : new Uint8Array(first);
2602
+ // If it's a raw signature (64 bytes) treat as signature; otherwise treat as signed tx bytes
2603
+ if (b.length === 64)
2604
+ signerSigBase58 = base58Encode(b);
2605
+ else
2606
+ signedTxB64 = b64FromBytes(b);
2607
+ }
2608
+ }
2609
+ }
2610
+ if (signedTxB64 || signerSigBase58) {
2611
+ // short-circuit to relay below
2612
+ }
2613
+ // 1) Try message: base58 (string form first)
2614
+ if (inlineMsgB58Fallback) {
2615
+ try {
2616
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'message:string(base58)', msgLen: inlineMsgB58Fallback.length });
2617
+ }
2618
+ catch { }
2619
+ try {
2620
+ r = await sol.request({ method: 'signTransaction', params: inlineMsgB58Fallback });
2621
+ }
2622
+ catch { }
2623
+ let candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
2624
+ if (!signedTxB64 && !signerSigBase58) {
2625
+ if (typeof candidate === 'string') {
2626
+ const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
2627
+ if (looksBase64)
2628
+ signedTxB64 = candidate;
2629
+ else
2630
+ signerSigBase58 = candidate;
2631
+ }
2632
+ else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
2633
+ const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
2634
+ if (b.length === 64)
2635
+ signerSigBase58 = base58Encode(b);
2636
+ }
2637
+ }
2638
+ // 1b) Try message: base58 (array form)
2639
+ if (!signedTxB64 && !signerSigBase58) {
2640
+ try {
2641
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'message:[string]', msgLen: inlineMsgB58Fallback.length });
2642
+ }
2643
+ catch { }
2644
+ try {
2645
+ r = await sol.request({ method: 'signTransaction', params: [inlineMsgB58Fallback] });
2646
+ }
2647
+ catch { }
2648
+ candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
2649
+ if (typeof candidate === 'string') {
2650
+ const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
2651
+ if (looksBase64)
2652
+ signedTxB64 = candidate;
2653
+ else
2654
+ signerSigBase58 = candidate;
2655
+ }
2656
+ else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
2657
+ const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
2658
+ if (b.length === 64)
2659
+ signerSigBase58 = base58Encode(b);
2660
+ }
2661
+ }
2662
+ // 1c) Try message: base58 (object form)
2663
+ if (!signedTxB64 && !signerSigBase58) {
2664
+ try {
2665
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'message:object{message}', msgLen: inlineMsgB58Fallback.length });
2666
+ }
2667
+ catch { }
2668
+ try {
2669
+ r = await sol.request({ method: 'signTransaction', params: { message: inlineMsgB58Fallback } });
2670
+ }
2671
+ catch { }
2672
+ candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
2673
+ if (typeof candidate === 'string') {
2674
+ const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
2675
+ if (looksBase64)
2676
+ signedTxB64 = candidate;
2677
+ else
2678
+ signerSigBase58 = candidate;
2679
+ }
2680
+ else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
2681
+ const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
2682
+ if (b.length === 64)
2683
+ signerSigBase58 = base58Encode(b);
2684
+ }
2685
+ }
2686
+ }
2687
+ // 2) If still nothing, try transaction: base64
2688
+ if (!signedTxB64 && !signerSigBase58) {
2689
+ // Wallet Standard first
2690
+ try {
2691
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'solana:signTransaction', paramShape: 'object{transaction:base64}', txLen: __fallbackTxBase64.length });
2692
+ }
2693
+ catch { }
2694
+ try {
2695
+ r = await sol.request({ method: 'solana:signTransaction', params: { transaction: __fallbackTxBase64 } });
2696
+ }
2697
+ catch { }
2698
+ let candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
2699
+ if (typeof candidate === 'string') {
2700
+ const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
2701
+ if (looksBase64)
2702
+ signedTxB64 = candidate;
2703
+ else
2704
+ signerSigBase58 = candidate;
2705
+ }
2706
+ else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
2707
+ const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
2708
+ if (b.length === 64)
2709
+ signerSigBase58 = base58Encode(b);
2710
+ }
2711
+ }
2712
+ if (!signedTxB64 && !signerSigBase58) {
2713
+ // Legacy string form
2714
+ try {
2715
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'transaction:string(base64)', txLen: __fallbackTxBase64.length });
2716
+ }
2717
+ catch { }
2718
+ try {
2719
+ r = await sol.request({ method: 'signTransaction', params: __fallbackTxBase64 });
2720
+ }
2721
+ catch { }
2722
+ let candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
2723
+ if (typeof candidate === 'string') {
2724
+ const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
2725
+ if (looksBase64)
2726
+ signedTxB64 = candidate;
2727
+ else
2728
+ signerSigBase58 = candidate;
2729
+ }
2730
+ else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
2731
+ const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
2732
+ if (b.length === 64)
2733
+ signerSigBase58 = base58Encode(b);
2734
+ }
2735
+ }
2736
+ if (!signedTxB64 && !signerSigBase58) {
2737
+ // Legacy array form
2738
+ try {
2739
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'transaction:[string]', txLen: __fallbackTxBase64.length });
2740
+ }
2741
+ catch { }
2742
+ try {
2743
+ r = await sol.request({ method: 'signTransaction', params: [__fallbackTxBase64] });
2744
+ }
2745
+ catch { }
2746
+ const candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
2747
+ if (typeof candidate === 'string') {
2748
+ const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
2749
+ if (looksBase64)
2750
+ signedTxB64 = candidate;
2751
+ else
2752
+ signerSigBase58 = candidate;
2753
+ }
2754
+ else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
2755
+ const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
2756
+ if (b.length === 64)
2757
+ signerSigBase58 = base58Encode(b);
2758
+ }
2759
+ }
2760
+ if (!signedTxB64 && !signerSigBase58) {
2761
+ // Legacy object form
2762
+ try {
2763
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'transaction:object{transaction}', txLen: __fallbackTxBase64.length });
2764
+ }
2765
+ catch { }
2766
+ try {
2767
+ r = await sol.request({ method: 'signTransaction', params: { transaction: __fallbackTxBase64 } });
2768
+ }
2769
+ catch { }
2770
+ const candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
2771
+ if (typeof candidate === 'string') {
2772
+ const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
2773
+ if (looksBase64)
2774
+ signedTxB64 = candidate;
2775
+ else
2776
+ signerSigBase58 = candidate;
2777
+ }
2778
+ else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
2779
+ const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
2780
+ if (b.length === 64)
2781
+ signerSigBase58 = base58Encode(b);
2782
+ }
2783
+ }
2784
+ }
2785
+ catch { }
2786
+ }
2787
+ let settleResp;
2788
+ if (signedTxB64 && typeof signedTxB64 === 'string') {
2789
+ settleResp = await this.publicApiClient.post('/sdk/public/payments/x402/relay', {
2790
+ paymentIntentId,
2791
+ signedTransactionBase64: signedTxB64,
2792
+ rpcUrlPublic: requirement?.extra?.rpcUrlPublic || null,
2793
+ });
2794
+ }
2795
+ else if (signerSigBase58) {
2796
+ settleResp = await this.publicApiClient.post('/sdk/public/payments/x402/relay', {
2797
+ paymentIntentId,
2798
+ signatureBase58: signerSigBase58,
2799
+ transactionBase64: __fallbackTxBase64,
2800
+ payerPublicKey: fromBase58,
2801
+ rpcUrlPublic: requirement?.extra?.rpcUrlPublic || null,
2802
+ });
2803
+ }
2804
+ else {
2805
+ throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.TRANSACTION_FAILED, message: 'Wallet did not return a signed transaction' });
2806
+ }
2807
+ const statusSol = (settleResp?.status || settleResp?.paymentIntent?.status || 'completed').toString().toLowerCase();
2808
+ const amountSol = (settleResp?.paymentIntent?.amount && String(settleResp.paymentIntent.amount)) ||
2809
+ (typeof usdAmount === 'number' ? String(usdAmount) : request?.amount?.toString?.() || '0');
2810
+ const outSol = {
2811
+ transactionId: Number(settleResp?.canisterTxId || 0),
2812
+ status: statusSol === 'succeeded' ? 'completed' : statusSol,
2813
+ amount: amountSol,
2814
+ recipientCanister: ledgerCanisterId,
2815
+ timestamp: new Date(),
2816
+ metadata: { ...(request.metadata || {}), icpay_x402: true },
2817
+ payment: settleResp || null,
2818
+ };
2819
+ this.emitMethodSuccess('createPaymentX402Usd', outSol);
2820
+ return outSol;
2821
+ }
2822
+ }
2823
+ // Otherwise fall back to unsigned transaction flow if provided
2824
+ const inlineTx = requirement?.extra?.transactionBase64;
2825
+ const wSolCtx2 = globalThis?.window || globalThis;
2826
+ // fromBase58 already determined
2827
+ // Build signer-based transaction, user signs it, and server relays
2828
+ let txBase64;
2829
+ if (inlineTx) {
2830
+ txBase64 = String(inlineTx);
2831
+ }
2832
+ else {
2833
+ throw new errors_1.IcpayError({
2834
+ code: errors_1.ICPAY_ERROR_CODES.API_ERROR,
2835
+ message: 'X402 missing transactionBase64 in 402 response for Solana',
2836
+ details: { note: 'API must include extra.transactionBase64 to avoid prepare' }
2837
+ });
2838
+ }
2839
+ // Sign-only with wallet, then relay server-side (fee payer = relayer)
2840
+ // Prefer request-based signTransaction with base58 "message" (serialized tx bytes)
2841
+ let signedTxB64 = null;
2842
+ let signerSigBase58 = null;
2843
+ const inlineMsgB58 = requirement?.extra?.messageBase58;
2844
+ const msgB58 = inlineMsgB58 || undefined;
2845
+ try {
2846
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol x402 payload sizes', {
2847
+ txBase64Len: txBase64?.length || 0,
2848
+ hasInlineMsgB58: !!inlineMsgB58,
2849
+ msgB58Len: msgB58?.length || 0,
2850
+ });
2851
+ }
2852
+ catch { }
2853
+ // Do NOT submit from wallet; only sign, then relay to backend
2854
+ // For wallets that require request-based signing
2855
+ const preferTransactionSigning = true;
2856
+ // Try to sign using transaction MESSAGE first (wallets often expect base58 message)
2857
+ if (!signedTxB64 && sol?.request) {
2858
+ try {
2859
+ let r = null;
2860
+ // Some wallets expect bare string or array for message
2861
+ if (msgB58) {
2862
+ // Ensure a visible connect prompt if required by the wallet
2863
+ if (typeof sol?.connect === 'function') {
2864
+ try {
2865
+ await sol.connect({ onlyIfTrusted: false });
2866
+ }
2867
+ catch { }
2868
+ }
2869
+ // Prefer object form with Uint8Array message first
2870
+ try {
2871
+ const msgBytesU8 = (() => { try {
2872
+ return base58Decode(msgB58);
2873
+ }
2874
+ catch {
2875
+ return new Uint8Array();
2876
+ } })();
2877
+ if (msgBytesU8.length > 0) {
2878
+ try {
2879
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'object{message:Uint8Array}', msgLen: msgBytesU8.length });
2880
+ }
2881
+ catch { }
2882
+ r = await sol.request({ method: 'signTransaction', params: { message: msgBytesU8 } });
2883
+ }
2884
+ }
2885
+ catch { }
2886
+ try {
2887
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'message:string(base58)', msgLen: msgB58.length });
2888
+ }
2889
+ catch { }
2890
+ try {
2891
+ r = await sol.request({ method: 'signTransaction', params: msgB58 });
2892
+ }
2893
+ catch (eM1) {
2894
+ try {
2895
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(message:string) failed', { error: String(eM1) });
2896
+ }
2897
+ catch { }
2898
+ try {
2899
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'message:[string]', msgLen: msgB58.length });
2900
+ }
2901
+ catch { }
2902
+ try {
2903
+ r = await sol.request({ method: 'signTransaction', params: [msgB58] });
2904
+ }
2905
+ catch (eM2) {
2906
+ try {
2907
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(message:array) failed', { error: String(eM2) });
2908
+ }
2909
+ catch { }
2910
+ try {
2911
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'message:object', msgLen: msgB58.length });
2912
+ }
2913
+ catch { }
2914
+ r = await sol.request({ method: 'signTransaction', params: { message: msgB58 } });
2915
+ }
2916
+ }
2917
+ }
2918
+ let candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
2919
+ try {
2920
+ const rawKeys = r && typeof r === 'object' ? Object.keys(r || {}) : [];
2921
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) raw result', { hasResult: !!r, rawKeys });
2922
+ }
2923
+ catch { }
2924
+ if (typeof candidate === 'string') {
2925
+ const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
2926
+ if (looksBase64)
2927
+ signedTxB64 = candidate;
2928
+ else
2929
+ signerSigBase58 = candidate;
2930
+ }
2931
+ else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
2932
+ try {
2933
+ const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
2934
+ if (b.length === 64)
2935
+ signerSigBase58 = base58Encode(b);
2936
+ }
2937
+ catch { }
2938
+ }
2939
+ else if (candidate && typeof candidate === 'object' && Array.isArray(candidate.data)) {
2940
+ try {
2941
+ const b = Uint8Array.from(candidate.data);
2942
+ if (b.length === 64)
2943
+ signerSigBase58 = base58Encode(b);
2944
+ }
2945
+ catch { }
2946
+ }
2947
+ else if (r && typeof r === 'object') {
2948
+ const obj = r;
2949
+ if (typeof obj.signature === 'string') {
2950
+ signerSigBase58 = obj.signature;
2951
+ }
2952
+ else if (Array.isArray(obj.signatures) && typeof obj.signatures[0] === 'string') {
2953
+ signerSigBase58 = obj.signatures[0];
2954
+ }
2955
+ }
2956
+ }
2957
+ catch { }
2958
+ }
2959
+ // If message-based did not yield, try transaction (prefer Uint8Array), then base64 (some wallets accept this)
2960
+ if (!signedTxB64 && !signerSigBase58 && sol?.request) {
2961
+ try {
2962
+ let r = null;
2963
+ // Prefer Uint8Array object form first to trigger wallet UI
2964
+ try {
2965
+ const txBytesU8 = u8FromBase64(txBase64);
2966
+ try {
2967
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'object{transaction:Uint8Array}', txLen: txBytesU8.length });
2968
+ }
2969
+ catch { }
2970
+ r = await sol.request({ method: 'signTransaction', params: { transaction: txBytesU8 } });
2971
+ }
2972
+ catch { }
2973
+ if (!r) {
2974
+ try {
2975
+ const txBytesU8b = u8FromBase64(txBase64);
2976
+ try {
2977
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'solana:signTransaction', paramShape: 'object{transaction:Uint8Array}', txLen: txBytesU8b.length });
2978
+ }
2979
+ catch { }
2980
+ r = await sol.request({ method: 'solana:signTransaction', params: { transaction: txBytesU8b } });
2981
+ }
2982
+ catch { }
2983
+ }
2984
+ // Try string param, then array, then object
2985
+ try {
2986
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'transaction:string(base64)', txLen: txBase64.length });
2987
+ }
2988
+ catch { }
2989
+ try {
2990
+ r = await sol.request({ method: 'signTransaction', params: txBase64 });
2991
+ }
2992
+ catch (eT1) {
2993
+ try {
2994
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(transaction:string) failed', { error: String(eT1) });
2995
+ }
2996
+ catch { }
2997
+ try {
2998
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'transaction:[string]', txLen: txBase64.length });
2999
+ }
3000
+ catch { }
3001
+ try {
3002
+ r = await sol.request({ method: 'signTransaction', params: [txBase64] });
3003
+ }
3004
+ catch (eT2) {
3005
+ try {
3006
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(transaction:array) failed', { error: String(eT2) });
3007
+ }
3008
+ catch { }
3009
+ try {
3010
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'transaction:object', txLen: txBase64.length });
3011
+ }
3012
+ catch { }
3013
+ r = await sol.request({ method: 'signTransaction', params: { transaction: txBase64 } });
3014
+ }
3015
+ }
3016
+ let candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
3017
+ if (typeof candidate === 'string') {
3018
+ const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
3019
+ if (looksBase64)
3020
+ signedTxB64 = candidate;
3021
+ else
3022
+ signerSigBase58 = candidate;
3023
+ }
3024
+ else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
3025
+ try {
3026
+ const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
3027
+ if (b.length === 64)
3028
+ signerSigBase58 = base58Encode(b);
3029
+ }
3030
+ catch { }
3031
+ }
3032
+ else if (candidate && typeof candidate === 'object' && Array.isArray(candidate.data)) {
3033
+ try {
3034
+ const b = Uint8Array.from(candidate.data);
3035
+ if (b.length === 64)
3036
+ signerSigBase58 = base58Encode(b);
3037
+ }
3038
+ catch { }
3039
+ }
3040
+ }
3041
+ catch { }
3042
+ }
3043
+ // No hosted signer fallback; rely on wallet API only
3044
+ // Prefer signMessage path only if explicitly allowed (disabled by default)
3045
+ try {
3046
+ const signableMsgB64 = requirement?.extra?.signableMessageBase64;
3047
+ if (!preferTransactionSigning && !signedTxB64 && !signerSigBase58 && signableMsgB64) {
3048
+ const message = u8FromBase64(signableMsgB64);
3049
+ let sigResp = null;
3050
+ let signErr = null;
3051
+ try {
3052
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage attempts begin', { hasBuffer: !!globalThis.Buffer, msgLen: message.length });
3053
+ }
3054
+ catch { }
3055
+ // A) Direct signMessage with Buffer first (some wallets require Buffer)
3056
+ if (typeof sol?.signMessage === 'function') {
3057
+ try {
3058
+ const Buf = globalThis.Buffer;
3059
+ if (Buf) {
3060
+ try {
3061
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(Buffer)');
3062
+ }
3063
+ catch { }
3064
+ sigResp = await sol.signMessage(Buf.from(message));
3065
+ }
3066
+ }
3067
+ catch (eA) {
3068
+ signErr = eA;
3069
+ try {
3070
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(Buffer) failed', { error: String(eA) });
3071
+ }
3072
+ catch { }
3073
+ }
3074
+ }
3075
+ // B) Direct signMessage with Uint8Array
3076
+ if (!sigResp && typeof sol?.signMessage === 'function') {
3077
+ try {
3078
+ try {
3079
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(Uint8Array)');
3080
+ }
3081
+ catch { }
3082
+ ;
3083
+ sigResp = await sol.signMessage(message);
3084
+ }
3085
+ catch (eB) {
3086
+ signErr = eB;
3087
+ try {
3088
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(Uint8Array) failed', { error: String(eB) });
3089
+ }
3090
+ catch { }
3091
+ }
3092
+ }
3093
+ // C) Direct signer.signMessage if present
3094
+ if (!sigResp && sol?.signer && typeof sol.signer.signMessage === 'function') {
3095
+ try {
3096
+ try {
3097
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signer.signMessage(Uint8Array)');
3098
+ }
3099
+ catch { }
3100
+ ;
3101
+ sigResp = await sol.signer.signMessage(message);
3102
+ }
3103
+ catch (eC) {
3104
+ signErr = eC;
3105
+ try {
3106
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signer.signMessage failed', { error: String(eC) });
3107
+ }
3108
+ catch { }
3109
+ }
3110
+ }
3111
+ // D) Wallet-standard request with array-of-bytes
3112
+ if (!sigResp && sol?.request) {
3113
+ try {
3114
+ try {
3115
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol request signMessage(array-of-bytes)');
3116
+ }
3117
+ catch { }
3118
+ ;
3119
+ const arr = Array.from(message);
3120
+ sigResp = await sol.request({ method: 'signMessage', params: { message: arr, display: 'hex' } });
3121
+ }
3122
+ catch (eD) {
3123
+ signErr = eD;
3124
+ try {
3125
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol request signMessage(array-of-bytes) failed', { error: String(eD) });
3126
+ }
3127
+ catch { }
3128
+ }
3129
+ }
3130
+ // E) Wallet-standard request with base58 message
3131
+ if (!sigResp && sol?.request) {
3132
+ try {
3133
+ try {
3134
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol request signMessage(base58)');
3135
+ }
3136
+ catch { }
3137
+ ;
3138
+ const msgB58ForReq = base58Encode(message);
3139
+ sigResp = await sol.request({ method: 'signMessage', params: { message: msgB58ForReq, display: 'hex' } });
3140
+ }
3141
+ catch (eE) {
3142
+ signErr = eE;
3143
+ try {
3144
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol request signMessage(base58) failed', { error: String(eE) });
3145
+ }
3146
+ catch { }
3147
+ }
3148
+ }
3149
+ let messageSigB64 = null;
3150
+ if (sigResp) {
3151
+ // Normalize common shapes: Uint8Array, Buffer-like, base58/base64 string, or { signature }
3152
+ if (typeof sigResp === 'string') {
3153
+ // Could be base58 or base64; try base58 first
3154
+ try {
3155
+ const asBytes = base58Decode(sigResp);
3156
+ messageSigB64 = b64FromBytes(asBytes);
3157
+ }
3158
+ catch {
3159
+ // assume base64 already
3160
+ messageSigB64 = sigResp;
3161
+ }
3162
+ }
3163
+ else if (sigResp && (sigResp.byteLength != null || ArrayBuffer.isView(sigResp))) {
3164
+ try {
3165
+ const bytes = sigResp instanceof Uint8Array ? sigResp : new Uint8Array(sigResp);
3166
+ if (bytes && bytes.length === 64) {
3167
+ messageSigB64 = b64FromBytes(bytes);
3168
+ }
3169
+ }
3170
+ catch { }
3171
+ }
3172
+ else if (sigResp && typeof sigResp === 'object') {
3173
+ const obj = sigResp;
3174
+ if (typeof obj.signature === 'string') {
3175
+ try {
3176
+ const asBytes = base58Decode(obj.signature);
3177
+ messageSigB64 = b64FromBytes(asBytes);
3178
+ }
3179
+ catch {
3180
+ messageSigB64 = obj.signature;
3181
+ }
3182
+ }
3183
+ else if (Array.isArray(obj.data)) {
3184
+ try {
3185
+ const bytes = Uint8Array.from(obj.data);
3186
+ if (bytes && bytes.length === 64) {
3187
+ messageSigB64 = b64FromBytes(bytes);
3188
+ }
3189
+ }
3190
+ catch { }
3191
+ }
3192
+ }
3193
+ try {
3194
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage result normalized (message signature)', {
3195
+ hasSignature: !!messageSigB64,
3196
+ signatureLen: messageSigB64 ? messageSigB64.length : null,
3197
+ });
3198
+ }
3199
+ catch { }
3200
+ }
3201
+ else if (signErr) {
3202
+ try {
3203
+ (0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage failed (all attempts)', { error: String(signErr) });
3204
+ }
3205
+ catch { }
3206
+ }
3207
+ // If we obtained a message signature, construct x402 header and settle via API
3208
+ if (messageSigB64) {
3209
+ const fields = (requirement?.extra?.signableFields || {});
3210
+ const header = (0, builders_1.buildX402HeaderFromAuthorization)({
3211
+ x402Version: Number(requirement?.x402Version || (data?.x402Version || 2)),
3212
+ scheme: String(requirement?.scheme || 'exact'),
3213
+ network: String(requirement?.network || ''),
3214
+ from: String(fromBase58 || ''),
3215
+ to: String(requirement?.payTo || ''),
3216
+ value: String(requirement?.maxAmountRequired || '0'),
3217
+ validAfter: String(fields?.validAfter || '0'),
3218
+ validBefore: String(fields?.validBefore || '0'),
3219
+ nonce: String(fields?.nonceHex || ''),
3220
+ signature: String(messageSigB64),
3221
+ });
3222
+ const headerJson = JSON.stringify(header);
3223
+ let headerB64;
3224
+ try {
3225
+ const Buf = globalThis.Buffer;
3226
+ headerB64 = Buf ? Buf.from(headerJson, 'utf8').toString('base64') : globalThis?.btoa?.(headerJson) || '';
3227
+ }
3228
+ catch {
3229
+ headerB64 = '';
3230
+ }
3231
+ if (headerB64) {
3232
+ try {
3233
+ this.emitMethodStart('notifyLedgerTransaction', { paymentIntentId });
3234
+ }
3235
+ catch { }
3236
+ const settleRespSol = await this.publicApiClient.post('/sdk/public/payments/x402/settle', {
3237
+ paymentIntentId,
3238
+ paymentHeader: headerB64,
3239
+ paymentRequirements: requirement,
3240
+ });
3241
+ try {
3242
+ (0, utils_1.debugLog)(this.config?.debug || false, 'x402 (sol) settle via header response', {
3243
+ ok: settleRespSol?.ok,
3244
+ status: settleRespSol?.status,
3245
+ paymentIntentId: settleRespSol?.paymentIntent?.id,
3246
+ paymentId: settleRespSol?.payment?.id,
3247
+ rawKeys: Object.keys(settleRespSol || {}),
3248
+ });
3249
+ }
3250
+ catch { }
3251
+ const statusSolHdr = (settleRespSol?.status || settleRespSol?.paymentIntent?.status || 'completed').toString().toLowerCase();
3252
+ const amountSolHdr = (settleRespSol?.paymentIntent?.amount && String(settleRespSol.paymentIntent.amount)) ||
3253
+ (typeof usdAmount === 'number' ? String(usdAmount) : request?.amount?.toString?.() || '0');
3254
+ const outSolHdr = {
3255
+ transactionId: Number(settleRespSol?.canisterTxId || 0),
3256
+ status: statusSolHdr === 'succeeded' ? 'completed' : statusSolHdr,
3257
+ amount: amountSolHdr,
3258
+ recipientCanister: ledgerCanisterId,
3259
+ timestamp: new Date(),
3260
+ metadata: { ...(request.metadata || {}), icpay_x402: true },
3261
+ payment: settleRespSol || null,
3262
+ };
3263
+ const isTerminalSolHdr = (() => {
3264
+ const s = String(outSolHdr.status || '').toLowerCase();
3265
+ return s === 'completed' || s === 'succeeded' || s === 'failed' || s === 'canceled' || s === 'cancelled' || s === 'mismatched';
3266
+ })();
3267
+ if (isTerminalSolHdr) {
3268
+ if (outSolHdr.status === 'completed') {
3269
+ this.emit('icpay-sdk-transaction-completed', outSolHdr);
3270
+ }
3271
+ else if (outSolHdr.status === 'failed') {
3272
+ this.emit('icpay-sdk-transaction-failed', outSolHdr);
3273
+ }
3274
+ else {
3275
+ this.emit('icpay-sdk-transaction-updated', outSolHdr);
3276
+ }
3277
+ this.emitMethodSuccess('createPaymentX402Usd', outSolHdr);
3278
+ return outSolHdr;
3279
+ }
3280
+ // Non-terminal: wait until terminal via notify loop
3281
+ try {
3282
+ this.emit('icpay-sdk-transaction-updated', outSolHdr);
3283
+ }
3284
+ catch { }
3285
+ const waitedSolHdr = await this.awaitIntentTerminal({
3286
+ paymentIntentId,
3287
+ ledgerCanisterId: ledgerCanisterId,
3288
+ amount: amountSolHdr,
3289
+ metadata: { ...(request.metadata || {}), icpay_x402: true },
3290
+ });
3291
+ this.emitMethodSuccess('createPaymentX402Usd', waitedSolHdr);
3292
+ return waitedSolHdr;
3293
+ }
3294
+ }
3295
+ }
3296
+ }
3297
+ catch { }
3298
+ if (!signedTxB64 && !signerSigBase58) {
3299
+ throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.TRANSACTION_FAILED, message: 'Wallet did not return a signed transaction' });
3300
+ }
3301
+ let settleResp;
3302
+ if (signedTxB64 && typeof signedTxB64 === 'string') {
3303
+ settleResp = await this.publicApiClient.post('/sdk/public/payments/x402/relay', {
3304
+ paymentIntentId,
3305
+ signedTransactionBase64: signedTxB64,
3306
+ rpcUrlPublic: requirement?.extra?.rpcUrlPublic || null,
3307
+ });
3308
+ }
3309
+ else if (signerSigBase58) {
3310
+ // Relay signature + unsigned tx; server will attach and co-sign
3311
+ settleResp = await this.publicApiClient.post('/sdk/public/payments/x402/relay', {
3312
+ paymentIntentId,
3313
+ signatureBase58: signerSigBase58,
3314
+ transactionBase64: txBase64,
3315
+ payerPublicKey: fromBase58,
3316
+ rpcUrlPublic: requirement?.extra?.rpcUrlPublic || null,
3317
+ });
3318
+ }
3319
+ else {
3320
+ throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.TRANSACTION_FAILED, message: 'Wallet did not return a signed transaction' });
3321
+ }
3322
+ try {
3323
+ (0, utils_1.debugLog)(this.config?.debug || false, 'x402 (sol) settle response (relay via services)', {
3324
+ ok: settleResp?.ok,
3325
+ status: settleResp?.status,
3326
+ paymentIntentId: settleResp?.paymentIntent?.id,
3327
+ paymentId: settleResp?.payment?.id,
3328
+ rawKeys: Object.keys(settleResp || {}),
3329
+ });
3330
+ }
3331
+ catch { }
3332
+ // Move to "Payment confirmation" stage (after relayer submission)
3333
+ try {
3334
+ this.emitMethodSuccess('notifyLedgerTransaction', { paymentIntentId });
3335
+ }
3336
+ catch { }
3337
+ const statusSol = (settleResp?.status || settleResp?.paymentIntent?.status || 'completed').toString().toLowerCase();
3338
+ const amountSol = (settleResp?.paymentIntent?.amount && String(settleResp.paymentIntent.amount)) ||
3339
+ (typeof usdAmount === 'number' ? String(usdAmount) : request?.amount?.toString?.() || '0');
3340
+ const outSol = {
3341
+ transactionId: Number(settleResp?.canisterTxId || 0),
3342
+ status: statusSol === 'succeeded' ? 'completed' : statusSol,
3343
+ amount: amountSol,
3344
+ recipientCanister: ledgerCanisterId,
3345
+ timestamp: new Date(),
3346
+ metadata: { ...(request.metadata || {}), icpay_x402: true },
3347
+ payment: settleResp || null,
3348
+ };
3349
+ // Do not fallback to normal flow for Solana x402; surface failure
3350
+ const isTerminalSol = (() => {
3351
+ const s = String(outSol.status || '').toLowerCase();
3352
+ return s === 'completed' || s === 'succeeded' || s === 'failed' || s === 'canceled' || s === 'cancelled' || s === 'mismatched';
3353
+ })();
3354
+ if (isTerminalSol) {
3355
+ if (outSol.status === 'completed') {
3356
+ this.emit('icpay-sdk-transaction-completed', outSol);
3357
+ }
3358
+ else if (outSol.status === 'failed') {
3359
+ this.emit('icpay-sdk-transaction-failed', outSol);
3360
+ }
3361
+ else {
3362
+ this.emit('icpay-sdk-transaction-updated', outSol);
3363
+ }
3364
+ this.emitMethodSuccess('createPaymentX402Usd', outSol);
3365
+ return outSol;
3366
+ }
3367
+ // Non-terminal: wait until terminal via notify loop
3368
+ try {
3369
+ this.emit('icpay-sdk-transaction-updated', outSol);
3370
+ }
3371
+ catch { }
3372
+ const waitedSol = await this.awaitIntentTerminal({
3373
+ paymentIntentId,
3374
+ ledgerCanisterId: ledgerCanisterId,
3375
+ amount: amountSol,
3376
+ metadata: { ...(request.metadata || {}), icpay_x402: true },
3377
+ });
3378
+ this.emitMethodSuccess('createPaymentX402Usd', waitedSol);
3379
+ return waitedSol;
3380
+ }
3381
+ // EVM: server-side settlement
1867
3382
  try {
1868
3383
  this.emitMethodStart('notifyLedgerTransaction', { paymentIntentId });
1869
3384
  }
@@ -1908,7 +3423,6 @@ class Icpay {
1908
3423
  this.emit('icpay-sdk-transaction-failed', { ...out, reason: failMsg });
1909
3424
  }
1910
3425
  catch { }
1911
- // Initiate regular flow (non-x402) with the same request
1912
3426
  const fallback = await this.createPaymentUsd(request);
1913
3427
  this.emitMethodSuccess('createPaymentX402Usd', fallback);
1914
3428
  return fallback;
@@ -1944,8 +3458,12 @@ class Icpay {
1944
3458
  this.emitMethodSuccess('createPaymentX402Usd', waited);
1945
3459
  return waited;
1946
3460
  }
1947
- catch {
1948
- // Fall through to notify-based wait if settle endpoint not available
3461
+ catch (err) {
3462
+ // For Solana x402, do not silently fall back; surface the error so user can retry/sign
3463
+ if (isSol) {
3464
+ throw (err instanceof errors_1.IcpayError) ? err : new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.TRANSACTION_FAILED, message: 'X402 Solana flow failed before signing', details: err });
3465
+ }
3466
+ // Non-Solana: fall through to notify-based wait if settle endpoint not available
1949
3467
  }
1950
3468
  }
1951
3469
  // Fallback: wait until terminal via notify loop
@@ -2052,6 +3570,16 @@ class Icpay {
2052
3570
  body.transactionId = params.transactionId;
2053
3571
  if (params.orderId)
2054
3572
  body.orderId = params.orderId;
3573
+ if (params.ledgerCanisterId)
3574
+ body.ledgerCanisterId = params.ledgerCanisterId;
3575
+ if (params.ledgerBlockIndex != null)
3576
+ body.ledgerBlockIndex = params.ledgerBlockIndex;
3577
+ if (typeof params.accountCanisterId === 'number')
3578
+ body.accountCanisterId = params.accountCanisterId;
3579
+ if (params.externalCostAmount != null)
3580
+ body.externalCostAmount = params.externalCostAmount;
3581
+ if (typeof params.recipientPrincipal === 'string')
3582
+ body.recipientPrincipal = params.recipientPrincipal;
2055
3583
  const resp = await notifyClient.post(notifyPath, body);
2056
3584
  // If this is the last attempt, return whatever we got
2057
3585
  if (attempt === maxAttempts) {
@@ -2098,6 +3626,11 @@ class Icpay {
2098
3626
  transactionId: params.transactionId || (params?.metadata?.icpay_evm_tx_hash ? String(params.metadata.icpay_evm_tx_hash) : undefined),
2099
3627
  maxAttempts: 1,
2100
3628
  delayMs: 0,
3629
+ ledgerCanisterId: params.ledgerCanisterId,
3630
+ ledgerBlockIndex: params.ledgerBlockIndex,
3631
+ accountCanisterId: params.accountCanisterId,
3632
+ externalCostAmount: params.externalCostAmount,
3633
+ recipientPrincipal: params.recipientPrincipal,
2101
3634
  });
2102
3635
  const status = resp?.paymentIntent?.status || resp?.payment?.status || resp?.status || '';
2103
3636
  const norm = typeof status === 'string' ? status.toLowerCase() : '';