@ic-pay/icpay-sdk 1.4.26 → 1.4.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -7
- package/dist/declarations/icpay_canister_backend/icpay_canister_backend.did +50 -17
- package/dist/declarations/icpay_canister_backend/icpay_canister_backend.did.d.ts +43 -1
- package/dist/declarations/icpay_canister_backend/icpay_canister_backend.did.js +56 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1761 -80
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/solana_index.iife.min.js +20 -0
- package/dist/x402/builders.d.ts.map +1 -1
- package/dist/x402/builders.js +266 -3
- package/dist/x402/builders.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -26,6 +26,120 @@ const principal_1 = require("@dfinity/principal");
|
|
|
26
26
|
const utils_1 = require("./utils");
|
|
27
27
|
const protected_1 = require("./protected");
|
|
28
28
|
const http_1 = require("./http");
|
|
29
|
+
// Minimal helpers to support Phantom's base58 "message" signing without extra deps
|
|
30
|
+
const B58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
31
|
+
function base58Encode(bytes) {
|
|
32
|
+
if (!bytes || bytes.length === 0)
|
|
33
|
+
return '';
|
|
34
|
+
// Count leading zeros
|
|
35
|
+
let zeros = 0;
|
|
36
|
+
while (zeros < bytes.length && bytes[zeros] === 0)
|
|
37
|
+
zeros++;
|
|
38
|
+
// Convert base-256 to base-58
|
|
39
|
+
const digits = [0];
|
|
40
|
+
for (let i = zeros; i < bytes.length; i++) {
|
|
41
|
+
let carry = bytes[i];
|
|
42
|
+
for (let j = 0; j < digits.length; j++) {
|
|
43
|
+
const x = digits[j] * 256 + carry;
|
|
44
|
+
digits[j] = x % 58;
|
|
45
|
+
carry = Math.floor(x / 58);
|
|
46
|
+
}
|
|
47
|
+
while (carry > 0) {
|
|
48
|
+
digits.push(carry % 58);
|
|
49
|
+
carry = Math.floor(carry / 58);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Leading zero bytes are represented by '1'
|
|
53
|
+
let out = '';
|
|
54
|
+
for (let k = 0; k < zeros; k++)
|
|
55
|
+
out += '1';
|
|
56
|
+
for (let q = digits.length - 1; q >= 0; q--)
|
|
57
|
+
out += B58_ALPHABET[digits[q]];
|
|
58
|
+
return out;
|
|
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
|
+
}
|
|
108
|
+
function u8FromBase64(b64) {
|
|
109
|
+
try {
|
|
110
|
+
const Buf = globalThis.Buffer;
|
|
111
|
+
if (Buf)
|
|
112
|
+
return new Uint8Array(Buf.from(b64, 'base64'));
|
|
113
|
+
}
|
|
114
|
+
catch { }
|
|
115
|
+
const bin = globalThis?.atob ? globalThis.atob(b64) : '';
|
|
116
|
+
const arr = new Uint8Array(bin.length);
|
|
117
|
+
for (let i = 0; i < bin.length; i++)
|
|
118
|
+
arr[i] = bin.charCodeAt(i);
|
|
119
|
+
return arr;
|
|
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
|
+
}
|
|
29
143
|
class Icpay {
|
|
30
144
|
constructor(config) {
|
|
31
145
|
this.privateApiClient = null;
|
|
@@ -41,7 +155,7 @@ class Icpay {
|
|
|
41
155
|
debug: false,
|
|
42
156
|
enableEvents: true,
|
|
43
157
|
...config,
|
|
44
|
-
onrampDisabled:
|
|
158
|
+
onrampDisabled: false,
|
|
45
159
|
};
|
|
46
160
|
(0, utils_1.debugLog)(this.config.debug || false, 'constructor', { config: this.config });
|
|
47
161
|
// Validate authentication configuration
|
|
@@ -421,12 +535,9 @@ class Icpay {
|
|
|
421
535
|
case 'evm':
|
|
422
536
|
case 'ethereum':
|
|
423
537
|
return await this.processEvmPayment(params);
|
|
538
|
+
case 'sol':
|
|
424
539
|
case 'solana':
|
|
425
|
-
|
|
426
|
-
code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG,
|
|
427
|
-
message: 'Solana payments are not implemented yet',
|
|
428
|
-
details: { chainType: params.chainType, chainId: params.chainId }
|
|
429
|
-
});
|
|
540
|
+
return await this.processSolanaPayment(params);
|
|
430
541
|
case 'sui':
|
|
431
542
|
throw new errors_1.IcpayError({
|
|
432
543
|
code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG,
|
|
@@ -588,7 +699,35 @@ class Icpay {
|
|
|
588
699
|
let canisterTransactionId;
|
|
589
700
|
try {
|
|
590
701
|
(0, utils_1.debugLog)(this.config.debug || false, 'notifying canister about ledger tx', { icpayCanisterId: this.icpayCanisterId, ledgerCanisterId, blockIndex });
|
|
591
|
-
|
|
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
|
+
});
|
|
592
731
|
if (typeof notifyRes === 'string') {
|
|
593
732
|
const parsed = parseInt(notifyRes, 10);
|
|
594
733
|
canisterTransactionId = Number.isFinite(parsed) ? parsed : undefined;
|
|
@@ -617,6 +756,117 @@ class Icpay {
|
|
|
617
756
|
});
|
|
618
757
|
return finalResponse;
|
|
619
758
|
}
|
|
759
|
+
async processSolanaPayment(params) {
|
|
760
|
+
if (!params.contractAddress) {
|
|
761
|
+
throw new errors_1.IcpayError({
|
|
762
|
+
code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG,
|
|
763
|
+
message: 'Missing Solana program address in payment intent',
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
const w = globalThis?.window || globalThis;
|
|
767
|
+
const sol = this.config?.solanaProvider || w?.solana;
|
|
768
|
+
if (!sol) {
|
|
769
|
+
throw new errors_1.IcpayError({
|
|
770
|
+
code: errors_1.ICPAY_ERROR_CODES.WALLET_PROVIDER_NOT_AVAILABLE,
|
|
771
|
+
message: 'Solana provider not available (window.solana)',
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
// Ensure connected & resolve payer pubkey
|
|
775
|
+
let payerKey = null;
|
|
776
|
+
try {
|
|
777
|
+
if (!sol.publicKey) {
|
|
778
|
+
await (sol.connect ? sol.connect() : sol.request?.({ method: 'connect' }));
|
|
779
|
+
}
|
|
780
|
+
const pk = sol.publicKey || sol?.wallet?.publicKey;
|
|
781
|
+
payerKey = pk ? String(pk) : null;
|
|
782
|
+
}
|
|
783
|
+
catch { }
|
|
784
|
+
if (!payerKey) {
|
|
785
|
+
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.WALLET_NOT_CONNECTED, message: 'Solana wallet not connected' });
|
|
786
|
+
}
|
|
787
|
+
// If the intent already provided an unsigned transaction, use it (no extra API roundtrip)
|
|
788
|
+
const prebuiltBase64 = params.request?.__transactionBase64;
|
|
789
|
+
if (typeof prebuiltBase64 === 'string' && prebuiltBase64.length > 0) {
|
|
790
|
+
let signature;
|
|
791
|
+
try {
|
|
792
|
+
if (sol?.request) {
|
|
793
|
+
// Treat as Phantom only if the selected provider itself reports isPhantom,
|
|
794
|
+
// or it is literally the same object as window.phantom.solana.
|
|
795
|
+
const isPhantom = !!(sol?.isPhantom ||
|
|
796
|
+
((w?.phantom?.solana) && (w.phantom.solana === sol)));
|
|
797
|
+
if (isPhantom) {
|
|
798
|
+
// Phantom expects base58-encoded serialized message under "message"
|
|
799
|
+
const msgB58 = base58Encode(u8FromBase64(prebuiltBase64));
|
|
800
|
+
(0, utils_1.debugLog)(this.config.debug || false, 'solana phantom request', { method: 'signAndSendTransaction', param: 'message(base58)' });
|
|
801
|
+
const r = await sol.request({ method: 'signAndSendTransaction', params: { message: msgB58 } });
|
|
802
|
+
signature = (r && (r.signature || r));
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
// Other wallets may accept base64 "transaction"
|
|
806
|
+
(0, utils_1.debugLog)(this.config.debug || false, 'solana generic request', { method: 'signAndSendTransaction', param: 'transaction(base64)' });
|
|
807
|
+
const r1 = await sol.request({ method: 'signAndSendTransaction', params: { transaction: prebuiltBase64 } });
|
|
808
|
+
signature = (r1 && (r1.signature || r1));
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
else if (typeof sol?.signAndSendTransaction === 'function') {
|
|
812
|
+
// Some providers (e.g., Backpack) expose direct signAndSendTransaction without request API.
|
|
813
|
+
// Try common parameter shapes in order.
|
|
814
|
+
const msgB58 = base58Encode(u8FromBase64(prebuiltBase64));
|
|
815
|
+
let r2 = null;
|
|
816
|
+
try {
|
|
817
|
+
(0, utils_1.debugLog)(this.config.debug || false, 'solana direct call', { fn: 'signAndSendTransaction', param: 'message(base58)' });
|
|
818
|
+
r2 = await sol.signAndSendTransaction({ message: msgB58 });
|
|
819
|
+
}
|
|
820
|
+
catch { }
|
|
821
|
+
if (!r2) {
|
|
822
|
+
try {
|
|
823
|
+
(0, utils_1.debugLog)(this.config.debug || false, 'solana direct call', { fn: 'signAndSendTransaction', param: 'transaction(base64)' });
|
|
824
|
+
r2 = await sol.signAndSendTransaction({ transaction: prebuiltBase64 });
|
|
825
|
+
}
|
|
826
|
+
catch { }
|
|
827
|
+
}
|
|
828
|
+
if (!r2) {
|
|
829
|
+
try {
|
|
830
|
+
(0, utils_1.debugLog)(this.config.debug || false, 'solana direct call', { fn: 'signAndSendTransaction', param: 'transaction(uint8array)' });
|
|
831
|
+
r2 = await sol.signAndSendTransaction(u8FromBase64(prebuiltBase64));
|
|
832
|
+
}
|
|
833
|
+
catch { }
|
|
834
|
+
}
|
|
835
|
+
signature = (r2 && (r2.signature || r2));
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
throw new Error('Unsupported Solana wallet interface');
|
|
839
|
+
}
|
|
840
|
+
if (!signature)
|
|
841
|
+
throw new Error('Missing Solana transaction signature');
|
|
842
|
+
}
|
|
843
|
+
catch (e) {
|
|
844
|
+
try {
|
|
845
|
+
(0, utils_1.debugLog)(this.config.debug || false, 'solana tx error (prebuilt)', { message: e?.message });
|
|
846
|
+
}
|
|
847
|
+
catch { }
|
|
848
|
+
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.TRANSACTION_FAILED, message: 'Solana transaction failed', details: e });
|
|
849
|
+
}
|
|
850
|
+
try {
|
|
851
|
+
this.emitMethodSuccess('notifyLedgerTransaction', { paymentIntentId: params.paymentIntentId });
|
|
852
|
+
}
|
|
853
|
+
catch { }
|
|
854
|
+
try {
|
|
855
|
+
await this.performNotifyPaymentIntent({ paymentIntentId: params.paymentIntentId, transactionId: signature, maxAttempts: 1 });
|
|
856
|
+
}
|
|
857
|
+
catch { }
|
|
858
|
+
const finalQuick = await this.awaitIntentTerminal({
|
|
859
|
+
paymentIntentId: params.paymentIntentId,
|
|
860
|
+
transactionId: signature,
|
|
861
|
+
ledgerCanisterId: params.ledgerCanisterId,
|
|
862
|
+
amount: params.amount.toString(),
|
|
863
|
+
metadata: { ...(params.metadata || {}), icpay_solana_tx_sig: signature },
|
|
864
|
+
});
|
|
865
|
+
return finalQuick;
|
|
866
|
+
}
|
|
867
|
+
// No prebuilt transaction available and builder fallback disabled
|
|
868
|
+
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.API_ERROR, message: 'Payment intent missing transactionBase64 for Solana' });
|
|
869
|
+
}
|
|
620
870
|
async processEvmPayment(params) {
|
|
621
871
|
const contractAddress = params.contractAddress;
|
|
622
872
|
if (!contractAddress) {
|
|
@@ -677,9 +927,9 @@ class Icpay {
|
|
|
677
927
|
// Prefer selectors from API; otherwise fallback to constants provided by backend
|
|
678
928
|
const apiSelectors = params.request.__functionSelectors || {};
|
|
679
929
|
const selector = {
|
|
680
|
-
//
|
|
681
|
-
payNative: apiSelectors.payNative || '
|
|
682
|
-
payERC20: apiSelectors.payERC20 || '
|
|
930
|
+
// Always use relay overloads; server maps payNative/payERC20 to relay selectors
|
|
931
|
+
payNative: apiSelectors.payNative || '0x8062dd66', // payNative(bytes32,uint64,uint256,address)
|
|
932
|
+
payERC20: apiSelectors.payERC20 || '0xc20b92c7', // payERC20(bytes32,uint64,address,uint256,uint256,address)
|
|
683
933
|
};
|
|
684
934
|
// Build EVM id bytes32 using shared helper
|
|
685
935
|
const accountIdNum = BigInt(params.accountCanisterId || 0);
|
|
@@ -698,6 +948,7 @@ class Icpay {
|
|
|
698
948
|
idHexLen: idHex?.length,
|
|
699
949
|
selectorPayNative: selector.payNative,
|
|
700
950
|
selectorPayERC20: selector.payERC20,
|
|
951
|
+
recipientAddress: params.request?.recipientAddress || null,
|
|
701
952
|
});
|
|
702
953
|
}
|
|
703
954
|
catch { }
|
|
@@ -730,15 +981,21 @@ class Icpay {
|
|
|
730
981
|
if (!owner)
|
|
731
982
|
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.WALLET_NOT_CONNECTED, message: 'EVM wallet not connected' });
|
|
732
983
|
(0, utils_1.debugLog)(this.config.debug || false, 'evm from account', { owner });
|
|
984
|
+
const ZERO = '0x0000000000000000000000000000000000000000';
|
|
985
|
+
const reqAny = params.request;
|
|
986
|
+
const recipientPrimary = String(reqAny?.recipientAddress || '').trim();
|
|
987
|
+
const recipientFromMap = String((reqAny?.recipientAddresses || {})?.evm || '').trim();
|
|
988
|
+
const recipientCandidate = recipientPrimary || recipientFromMap;
|
|
989
|
+
const recipient = /^0x[a-fA-F0-9]{40}$/.test(recipientCandidate) ? recipientCandidate : ZERO;
|
|
733
990
|
if (isNative) {
|
|
734
991
|
const externalCostStr = params.request?.__externalCostAmount;
|
|
735
992
|
const externalCost = externalCostStr != null && externalCostStr !== '' ? BigInt(String(externalCostStr)) : 0n;
|
|
736
993
|
const extSel = selector.payNative;
|
|
737
994
|
if (!extSel) {
|
|
738
|
-
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG, message: 'Missing payNative
|
|
995
|
+
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG, message: 'Missing payNative selector from API; update API/chain metadata.' });
|
|
739
996
|
}
|
|
740
|
-
const data = extSel + idHex + toUint64(accountIdNum) + toUint256(externalCost);
|
|
741
|
-
(0, utils_1.debugLog)(this.config.debug || false, 'evm native tx', { to: contractAddress, from: owner, dataLen: data.length, value: amountHex });
|
|
997
|
+
const data = extSel + idHex + toUint64(accountIdNum) + toUint256(externalCost) + toAddressPadded(recipient);
|
|
998
|
+
(0, utils_1.debugLog)(this.config.debug || false, 'evm native tx', { to: contractAddress, from: owner, dataLen: data.length, value: amountHex, recipient });
|
|
742
999
|
txHash = await eth.request({ method: 'eth_sendTransaction', params: [{ from: owner, to: contractAddress, data, value: amountHex }] });
|
|
743
1000
|
}
|
|
744
1001
|
else {
|
|
@@ -763,10 +1020,11 @@ class Icpay {
|
|
|
763
1020
|
const externalCost = externalCostStr != null && externalCostStr !== '' ? BigInt(String(externalCostStr)) : 0n;
|
|
764
1021
|
const extSel = selector.payERC20;
|
|
765
1022
|
if (!extSel) {
|
|
766
|
-
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG, message: 'Missing payERC20
|
|
1023
|
+
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG, message: 'Missing payERC20 selector from API; update API/chain metadata.' });
|
|
767
1024
|
}
|
|
768
|
-
const
|
|
769
|
-
|
|
1025
|
+
const base = idHex + toUint64(accountIdNum) + toAddressPadded(String(tokenAddress)) + toUint256(params.amount) + toUint256(externalCost);
|
|
1026
|
+
const data = extSel + base + toAddressPadded(recipient);
|
|
1027
|
+
(0, utils_1.debugLog)(this.config.debug || false, 'evm erc20 pay', { to: contractAddress, from: owner, token: tokenAddress, dataLen: data.length, recipient });
|
|
770
1028
|
txHash = await eth.request({ method: 'eth_sendTransaction', params: [{ from: owner, to: contractAddress, data }] });
|
|
771
1029
|
}
|
|
772
1030
|
}
|
|
@@ -931,7 +1189,8 @@ class Icpay {
|
|
|
931
1189
|
// Resolve ledgerCanisterId from symbol if needed (legacy). If tokenShortcode provided, no resolution required.
|
|
932
1190
|
let ledgerCanisterId = request.ledgerCanisterId;
|
|
933
1191
|
const tokenShortcode = request?.tokenShortcode;
|
|
934
|
-
|
|
1192
|
+
const isOnrampFlow = (request?.onrampPayment === true) || (this?.config?.onrampPayment === true);
|
|
1193
|
+
if (!ledgerCanisterId && !tokenShortcode && !request.symbol && !isOnrampFlow) {
|
|
935
1194
|
const err = new errors_1.IcpayError({
|
|
936
1195
|
code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG,
|
|
937
1196
|
message: 'Provide either tokenShortcode or ledgerCanisterId (symbol is deprecated).',
|
|
@@ -948,33 +1207,45 @@ class Icpay {
|
|
|
948
1207
|
let intentChainId;
|
|
949
1208
|
let accountCanisterId;
|
|
950
1209
|
let resolvedAmountStr = typeof request.amount === 'string' ? request.amount : (request.amount != null ? String(request.amount) : undefined);
|
|
1210
|
+
let intentResp;
|
|
951
1211
|
try {
|
|
952
1212
|
(0, utils_1.debugLog)(this.config.debug || false, 'creating payment intent');
|
|
953
|
-
//
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1213
|
+
// Resolve expected sender principal:
|
|
1214
|
+
// Start with any value explicitly provided on the request or via connectedWallet.
|
|
1215
|
+
let expectedSenderPrincipal = request.expectedSenderPrincipal ||
|
|
1216
|
+
this.connectedWallet?.owner ||
|
|
1217
|
+
this.connectedWallet?.principal?.toString();
|
|
1218
|
+
// If none yet and a Solana provider is present (e.g., Phantom), prefer its publicKey (base58).
|
|
1219
|
+
try {
|
|
1220
|
+
const solProv = this.config?.solanaProvider || globalThis?.solana;
|
|
1221
|
+
const solPk = solProv?.publicKey ? String(solProv.publicKey) : undefined;
|
|
1222
|
+
if (!expectedSenderPrincipal && solPk) {
|
|
1223
|
+
expectedSenderPrincipal = String(solPk);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
catch { }
|
|
1227
|
+
// Only if still missing, fall back to EVM accounts.
|
|
1228
|
+
if (!expectedSenderPrincipal) {
|
|
1229
|
+
const evm = this.config?.evmProvider || globalThis?.ethereum;
|
|
1230
|
+
if (evm?.request) {
|
|
1231
|
+
try {
|
|
1232
|
+
const accounts = await evm.request({ method: 'eth_accounts' });
|
|
1233
|
+
if (Array.isArray(accounts) && accounts[0]) {
|
|
1234
|
+
const lowerAccounts = accounts.map((a) => String(a).toLowerCase());
|
|
1235
|
+
const providedRaw = request?.expectedSenderPrincipal;
|
|
1236
|
+
if (providedRaw) {
|
|
1237
|
+
const provided = String(providedRaw).toLowerCase();
|
|
1238
|
+
expectedSenderPrincipal = lowerAccounts.includes(provided) ? accounts[lowerAccounts.indexOf(provided)] : accounts[0];
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1241
|
+
expectedSenderPrincipal = accounts[0];
|
|
1242
|
+
}
|
|
972
1243
|
}
|
|
973
1244
|
}
|
|
1245
|
+
catch { }
|
|
974
1246
|
}
|
|
975
|
-
catch { }
|
|
976
1247
|
}
|
|
977
|
-
if (!expectedSenderPrincipal) {
|
|
1248
|
+
if (!expectedSenderPrincipal && !((request?.onrampPayment === true) || (this?.config?.onrampPayment === true))) {
|
|
978
1249
|
throw new errors_1.IcpayError({
|
|
979
1250
|
code: errors_1.ICPAY_ERROR_CODES.WALLET_NOT_CONNECTED,
|
|
980
1251
|
message: 'Wallet must be connected to create payment intent',
|
|
@@ -986,7 +1257,19 @@ class Icpay {
|
|
|
986
1257
|
const onramp = (request.onrampPayment === true || this.config.onrampPayment === true) && this.config.onrampDisabled !== true ? true : false;
|
|
987
1258
|
const meta = request?.metadata || {};
|
|
988
1259
|
const isAtxp = Boolean(meta?.icpay_atxp_request) && typeof (meta?.atxp_request_id) === 'string';
|
|
989
|
-
|
|
1260
|
+
// Resolve recipientAddress only for non-onramp flows
|
|
1261
|
+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
1262
|
+
const reqAny = request;
|
|
1263
|
+
const addrObj = (reqAny?.recipientAddresses) || {};
|
|
1264
|
+
const candidateEvm = addrObj.evm ? addrObj.evm : undefined;
|
|
1265
|
+
const candidateIC = addrObj.ic ? addrObj.ic : undefined;
|
|
1266
|
+
const candidateSol = addrObj.sol ? addrObj.sol : undefined;
|
|
1267
|
+
let recipientAddress = undefined;
|
|
1268
|
+
if (!onramp) {
|
|
1269
|
+
// Choose a default to persist on the intent; EVM will override to ZERO if non-hex when building tx
|
|
1270
|
+
recipientAddress = (reqAny?.recipientAddress) || candidateEvm || candidateIC || candidateSol || ZERO_ADDRESS;
|
|
1271
|
+
(0, utils_1.debugLog)(this.config.debug || false, 'recipientAddress resolved for intent', { recipientAddress });
|
|
1272
|
+
}
|
|
990
1273
|
if (isAtxp) {
|
|
991
1274
|
// Route ATXP intents to the ATXP endpoint so they link to the request
|
|
992
1275
|
const atxpRequestId = String(meta.atxp_request_id);
|
|
@@ -994,25 +1277,42 @@ class Icpay {
|
|
|
994
1277
|
intentResp = await this.publicApiClient.post(endpoint, {
|
|
995
1278
|
tokenShortcode: tokenShortcode || undefined,
|
|
996
1279
|
description: request.description,
|
|
1280
|
+
recipientAddress,
|
|
1281
|
+
recipientAddresses: request?.recipientAddresses || undefined,
|
|
1282
|
+
externalCostAmount: request?.externalCostAmount ?? request?.metadata?.externalCostAmount ?? undefined,
|
|
997
1283
|
});
|
|
998
1284
|
}
|
|
999
1285
|
else {
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1286
|
+
if (onramp) {
|
|
1287
|
+
// Route onramp flows to the dedicated onramp endpoint without requiring token/ledger
|
|
1288
|
+
intentResp = await this.publicApiClient.post('/sdk/public/onramp/intents', {
|
|
1289
|
+
usdAmount: request.amountUsd,
|
|
1290
|
+
description: request.description,
|
|
1291
|
+
metadata: normalizeSdkMetadata(request.metadata || {}),
|
|
1292
|
+
widgetParams: request.widgetParams || undefined,
|
|
1293
|
+
recipientAddresses: request?.recipientAddresses || undefined,
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
intentResp = await this.publicApiClient.post('/sdk/public/payments/intents', {
|
|
1298
|
+
amount: (typeof request.amount === 'string' ? request.amount : (request.amount != null ? String(request.amount) : undefined)),
|
|
1299
|
+
// Prefer tokenShortcode if provided
|
|
1300
|
+
tokenShortcode: tokenShortcode || undefined,
|
|
1301
|
+
// Legacy fields for backwards compatibility
|
|
1302
|
+
symbol: tokenShortcode ? undefined : request.symbol,
|
|
1303
|
+
ledgerCanisterId: tokenShortcode ? undefined : ledgerCanisterId,
|
|
1304
|
+
description: request.description,
|
|
1305
|
+
expectedSenderPrincipal,
|
|
1306
|
+
metadata: normalizeSdkMetadata(request.metadata || {}),
|
|
1307
|
+
amountUsd: request.amountUsd,
|
|
1308
|
+
// With tokenShortcode, backend derives chain. Keep legacy chainId for old flows.
|
|
1309
|
+
chainId: tokenShortcode ? undefined : request.chainId,
|
|
1310
|
+
widgetParams: request.widgetParams || undefined,
|
|
1311
|
+
recipientAddress,
|
|
1312
|
+
recipientAddresses: request?.recipientAddresses || undefined,
|
|
1313
|
+
externalCostAmount: request?.externalCostAmount ?? request?.metadata?.externalCostAmount ?? undefined,
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1016
1316
|
}
|
|
1017
1317
|
paymentIntentId = intentResp?.paymentIntent?.id || null;
|
|
1018
1318
|
paymentIntentCode = intentResp?.paymentIntent?.intentCode ?? null;
|
|
@@ -1026,21 +1326,35 @@ class Icpay {
|
|
|
1026
1326
|
const rpcChainId = intentResp?.paymentIntent?.rpcChainId || null;
|
|
1027
1327
|
const functionSelectors = intentResp?.paymentIntent?.functionSelectors || null;
|
|
1028
1328
|
const externalCostAmount = intentResp?.paymentIntent?.externalCostAmount || null;
|
|
1329
|
+
const transactionBase64 = intentResp?.paymentIntent?.transactionBase64 || null;
|
|
1029
1330
|
accountCanisterId = intentResp?.paymentIntent?.accountCanisterId || null;
|
|
1030
1331
|
// Backfill ledgerCanisterId from intent if not provided in request (tokenShortcode flow)
|
|
1031
1332
|
if (!ledgerCanisterId && intentResp?.paymentIntent?.ledgerCanisterId) {
|
|
1032
1333
|
ledgerCanisterId = intentResp.paymentIntent.ledgerCanisterId;
|
|
1033
1334
|
}
|
|
1034
1335
|
(0, utils_1.debugLog)(this.config.debug || false, 'payment intent created', { paymentIntentId, paymentIntentCode, expectedSenderPrincipal, resolvedAmountStr });
|
|
1035
|
-
// Emit transaction created event
|
|
1336
|
+
// Emit transaction created event only for non-onramp flows
|
|
1036
1337
|
if (paymentIntentId) {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1338
|
+
if (!isOnrampFlow) {
|
|
1339
|
+
this.emit('icpay-sdk-transaction-created', {
|
|
1340
|
+
paymentIntentId,
|
|
1341
|
+
amount: resolvedAmountStr,
|
|
1342
|
+
ledgerCanisterId,
|
|
1343
|
+
expectedSenderPrincipal,
|
|
1344
|
+
accountCanisterId,
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
// Optional: emit an onramp-specific event for UI
|
|
1349
|
+
try {
|
|
1350
|
+
this.emit?.('icpay-sdk-onramp-intent-created', {
|
|
1351
|
+
paymentIntentId,
|
|
1352
|
+
amountUsd: request.amountUsd,
|
|
1353
|
+
onramp: intentResp?.onramp,
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
catch { }
|
|
1357
|
+
}
|
|
1044
1358
|
}
|
|
1045
1359
|
request.__onramp = onrampData;
|
|
1046
1360
|
request.__contractAddress = contractAddress;
|
|
@@ -1049,6 +1363,7 @@ class Icpay {
|
|
|
1049
1363
|
request.__functionSelectors = functionSelectors;
|
|
1050
1364
|
request.__rpcChainId = rpcChainId;
|
|
1051
1365
|
request.__externalCostAmount = externalCostAmount;
|
|
1366
|
+
request.__transactionBase64 = transactionBase64;
|
|
1052
1367
|
}
|
|
1053
1368
|
catch (e) {
|
|
1054
1369
|
// Do not proceed without a payment intent
|
|
@@ -1070,6 +1385,18 @@ class Icpay {
|
|
|
1070
1385
|
this.emitError(err);
|
|
1071
1386
|
throw err;
|
|
1072
1387
|
}
|
|
1388
|
+
// If this is an onramp flow, do NOT proceed with chain processing. Return onramp payload instead.
|
|
1389
|
+
const onrampPayload = request.__onramp;
|
|
1390
|
+
if (isOnrampFlow && onrampPayload) {
|
|
1391
|
+
const early = {
|
|
1392
|
+
paymentIntentId,
|
|
1393
|
+
paymentIntentCode,
|
|
1394
|
+
onramp: onrampPayload,
|
|
1395
|
+
paymentIntent: intentResp?.paymentIntent || null,
|
|
1396
|
+
};
|
|
1397
|
+
this.emitMethodSuccess('createPayment', early);
|
|
1398
|
+
return early;
|
|
1399
|
+
}
|
|
1073
1400
|
const amount = BigInt(resolvedAmountStr);
|
|
1074
1401
|
// Build packed memo
|
|
1075
1402
|
const acctIdNum = parseInt(accountCanisterId);
|
|
@@ -1212,7 +1539,7 @@ class Icpay {
|
|
|
1212
1539
|
/**
|
|
1213
1540
|
* Notify canister about ledger transaction using anonymous actor (no signature required)
|
|
1214
1541
|
*/
|
|
1215
|
-
async notifyLedgerTransaction(canisterId, ledgerCanisterId, blockIndex) {
|
|
1542
|
+
async notifyLedgerTransaction(canisterId, ledgerCanisterId, blockIndex, opts) {
|
|
1216
1543
|
this.emitMethodStart('notifyLedgerTransaction', { canisterId, ledgerCanisterId, blockIndex: blockIndex.toString() });
|
|
1217
1544
|
// Create anonymous actor for canister notifications (no signature required)
|
|
1218
1545
|
// Retry on transient certificate TrustError (clock skew)
|
|
@@ -1222,11 +1549,45 @@ class Icpay {
|
|
|
1222
1549
|
try {
|
|
1223
1550
|
const agent = new agent_1.HttpAgent({ host: this.icHost });
|
|
1224
1551
|
const actor = agent_1.Actor.createActor(icpay_canister_backend_did_js_1.idlFactory, { agent, canisterId });
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1552
|
+
// Prefer v2 when available and we have required inputs
|
|
1553
|
+
const maybeV2 = actor?.notify_ledger_transaction_v2;
|
|
1554
|
+
const haveAcct = typeof opts?.accountCanisterId === 'number' && Number.isFinite(opts.accountCanisterId);
|
|
1555
|
+
if (maybeV2 && haveAcct) {
|
|
1556
|
+
try {
|
|
1557
|
+
(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()) });
|
|
1558
|
+
}
|
|
1559
|
+
catch { }
|
|
1560
|
+
const externalCost = (() => {
|
|
1561
|
+
if (opts?.externalCostAmount == null)
|
|
1562
|
+
return [];
|
|
1563
|
+
try {
|
|
1564
|
+
const v = BigInt(String(opts.externalCostAmount));
|
|
1565
|
+
if (v < 0n)
|
|
1566
|
+
return [];
|
|
1567
|
+
return [v];
|
|
1568
|
+
}
|
|
1569
|
+
catch {
|
|
1570
|
+
return [];
|
|
1571
|
+
}
|
|
1572
|
+
})();
|
|
1573
|
+
const recipient = (() => {
|
|
1574
|
+
const s = (opts?.recipientPrincipal || '').trim();
|
|
1575
|
+
return s ? [s] : [];
|
|
1576
|
+
})();
|
|
1577
|
+
result = await actor.notify_ledger_transaction_v2({ ledger_canister_id: ledgerCanisterId, block_index: blockIndex }, BigInt(opts.accountCanisterId), externalCost, recipient);
|
|
1578
|
+
}
|
|
1579
|
+
else {
|
|
1580
|
+
try {
|
|
1581
|
+
(0, utils_1.debugLog)(this.config.debug || false, 'notify using v1 (fallback)', { ledgerCanisterId, blockIndex: blockIndex.toString(), haveAcct });
|
|
1582
|
+
}
|
|
1583
|
+
catch { }
|
|
1584
|
+
// Fallback to legacy notify
|
|
1585
|
+
result = await actor.notify_ledger_transaction({
|
|
1586
|
+
// Canister expects text for ledger_canister_id
|
|
1587
|
+
ledger_canister_id: ledgerCanisterId,
|
|
1588
|
+
block_index: blockIndex
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1230
1591
|
break;
|
|
1231
1592
|
}
|
|
1232
1593
|
catch (e) {
|
|
@@ -1583,7 +1944,8 @@ class Icpay {
|
|
|
1583
1944
|
// If tokenShortcode provided, skip canister resolution; otherwise resolve from symbol if needed
|
|
1584
1945
|
const tokenShortcode = request?.tokenShortcode;
|
|
1585
1946
|
let ledgerCanisterId = request.ledgerCanisterId;
|
|
1586
|
-
|
|
1947
|
+
const isOnrampFlow = request?.onrampPayment === true || this?.config?.onrampPayment === true;
|
|
1948
|
+
if (!ledgerCanisterId && !tokenShortcode && !request.symbol && !isOnrampFlow) {
|
|
1587
1949
|
const err = new errors_1.IcpayError({
|
|
1588
1950
|
code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG,
|
|
1589
1951
|
message: 'Provide either tokenShortcode or ledgerCanisterId (symbol is deprecated).',
|
|
@@ -1603,6 +1965,8 @@ class Icpay {
|
|
|
1603
1965
|
onrampPayment: request.onrampPayment,
|
|
1604
1966
|
widgetParams: request.widgetParams,
|
|
1605
1967
|
chainId: tokenShortcode ? undefined : request.chainId,
|
|
1968
|
+
recipientAddress: request?.recipientAddress || '0x0000000000000000000000000000000000000000',
|
|
1969
|
+
recipientAddresses: request?.recipientAddresses,
|
|
1606
1970
|
};
|
|
1607
1971
|
const res = await this.createPayment(createTransactionRequest);
|
|
1608
1972
|
this.emitMethodSuccess('createPaymentUsd', res);
|
|
@@ -1643,10 +2007,21 @@ class Icpay {
|
|
|
1643
2007
|
symbol: tokenShortcode ? undefined : request.symbol,
|
|
1644
2008
|
ledgerCanisterId: tokenShortcode ? undefined : ledgerCanisterId,
|
|
1645
2009
|
description: request.description,
|
|
1646
|
-
metadata: request.metadata,
|
|
2010
|
+
metadata: normalizeSdkMetadata(request.metadata || {}),
|
|
1647
2011
|
chainId: tokenShortcode ? undefined : request.chainId,
|
|
1648
2012
|
x402: true,
|
|
2013
|
+
recipientAddress: request?.recipientAddress || '0x0000000000000000000000000000000000000000',
|
|
1649
2014
|
};
|
|
2015
|
+
// Include Solana payerPublicKey if available so server can build unsigned tx inline for Solana x402
|
|
2016
|
+
try {
|
|
2017
|
+
const w = globalThis?.window || globalThis;
|
|
2018
|
+
const sol = this.config?.solanaProvider || w?.solana || w?.phantom?.solana;
|
|
2019
|
+
const pk = sol?.publicKey?.toBase58?.() || sol?.publicKey || null;
|
|
2020
|
+
if (pk && typeof pk === 'string') {
|
|
2021
|
+
body.payerPublicKey = pk;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
catch { }
|
|
1650
2025
|
try {
|
|
1651
2026
|
const resp = await this.publicApiClient.post('/sdk/public/payments/intents/x402', body);
|
|
1652
2027
|
// If backend indicates x402 is unavailable (failed + fallback), immediately switch to normal flow
|
|
@@ -1693,13 +2068,1316 @@ class Icpay {
|
|
|
1693
2068
|
const acceptsArr = Array.isArray(data?.accepts) ? data.accepts : [];
|
|
1694
2069
|
let requirement = acceptsArr.length > 0 ? acceptsArr[0] : null;
|
|
1695
2070
|
if (requirement) {
|
|
2071
|
+
// Determine network once for error handling policy
|
|
2072
|
+
const isSol = typeof requirement?.network === 'string' && String(requirement.network).toLowerCase().startsWith('solana:');
|
|
1696
2073
|
try {
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
2074
|
+
const providerForHeader = isSol
|
|
2075
|
+
? (this.config?.solanaProvider || globalThis?.solana || globalThis?.phantom?.solana)
|
|
2076
|
+
: (this.config?.evmProvider || globalThis?.ethereum);
|
|
2077
|
+
let paymentHeader = null;
|
|
2078
|
+
// IC x402: client-side settle via allowance + canister pull
|
|
2079
|
+
const isIc = typeof requirement?.network === 'string' && String(requirement.network).toLowerCase().startsWith('icp:');
|
|
2080
|
+
if (isIc) {
|
|
2081
|
+
// IC x402: client ensures allowance; then call API settle so services (controller) pulls and notifies
|
|
2082
|
+
if (!this.actorProvider) {
|
|
2083
|
+
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.WALLET_PROVIDER_NOT_AVAILABLE, message: 'actorProvider required for IC x402' });
|
|
2084
|
+
}
|
|
2085
|
+
// Ensure allowance first
|
|
2086
|
+
const asset = String(requirement?.asset || requirement?.payTo || '').trim();
|
|
2087
|
+
const amountStr = String(requirement?.maxAmountRequired || '0');
|
|
2088
|
+
const amountBn = BigInt(amountStr);
|
|
2089
|
+
if (!asset || amountBn <= 0n) {
|
|
2090
|
+
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.API_ERROR, message: 'Invalid x402 IC requirement (asset/amount)' });
|
|
2091
|
+
}
|
|
2092
|
+
// Approve spender (ICPay canister) for amount
|
|
2093
|
+
if (!this.icpayCanisterId) {
|
|
2094
|
+
// Prefer payTo from x402 requirement as ICPay canister id if present
|
|
2095
|
+
try {
|
|
2096
|
+
const maybe = String(requirement?.payTo || '');
|
|
2097
|
+
if (maybe) {
|
|
2098
|
+
// Validate shape by attempting to parse
|
|
2099
|
+
principal_1.Principal.fromText(maybe);
|
|
2100
|
+
this.icpayCanisterId = maybe;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
catch { }
|
|
2104
|
+
}
|
|
2105
|
+
if (!this.icpayCanisterId) {
|
|
2106
|
+
// Fallback to account lookup
|
|
2107
|
+
const acctInfo = await this.fetchAccountInfo();
|
|
2108
|
+
this.icpayCanisterId = acctInfo?.icpayCanisterId?.toString?.() || this.icpayCanisterId;
|
|
2109
|
+
}
|
|
2110
|
+
if (!this.icpayCanisterId) {
|
|
2111
|
+
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.INVALID_CONFIG, message: 'Missing ICPay canister id for IC x402' });
|
|
2112
|
+
}
|
|
2113
|
+
const ledgerActor = this.actorProvider(asset, ledger_did_js_1.idlFactory);
|
|
2114
|
+
try {
|
|
2115
|
+
// Fetch transfer fee and approve amount + fee to avoid InsufficientAllowance
|
|
2116
|
+
let feeBn = 0n;
|
|
2117
|
+
try {
|
|
2118
|
+
const f = await ledgerActor.icrc1_fee();
|
|
2119
|
+
feeBn = typeof f === 'bigint' ? f : BigInt(f);
|
|
2120
|
+
}
|
|
2121
|
+
catch { }
|
|
2122
|
+
const approveAmount = amountBn + (feeBn > 0n ? feeBn : 0n);
|
|
2123
|
+
await ledgerActor.icrc2_approve({
|
|
2124
|
+
fee: [],
|
|
2125
|
+
memo: [],
|
|
2126
|
+
from_subaccount: [],
|
|
2127
|
+
created_at_time: [],
|
|
2128
|
+
amount: approveAmount,
|
|
2129
|
+
expected_allowance: [],
|
|
2130
|
+
expires_at: [],
|
|
2131
|
+
spender: { owner: principal_1.Principal.fromText(this.icpayCanisterId), subaccount: [] },
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
catch (apprErr) {
|
|
2135
|
+
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.TRANSACTION_FAILED, message: 'ICRC-2 approve failed', details: apprErr });
|
|
2136
|
+
}
|
|
2137
|
+
// Obtain payer principal if available
|
|
2138
|
+
let payerPrincipal = null;
|
|
2139
|
+
try {
|
|
2140
|
+
const p = this.wallet.getPrincipal();
|
|
2141
|
+
if (p)
|
|
2142
|
+
payerPrincipal = p.toText();
|
|
2143
|
+
else if (this.connectedWallet && typeof this.connectedWallet.getPrincipal === 'function') {
|
|
2144
|
+
const maybe = await this.connectedWallet.getPrincipal();
|
|
2145
|
+
if (typeof maybe === 'string')
|
|
2146
|
+
payerPrincipal = maybe;
|
|
2147
|
+
}
|
|
2148
|
+
else if (this.connectedWallet?.principal) {
|
|
2149
|
+
payerPrincipal = String(this.connectedWallet.principal);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
catch { }
|
|
2153
|
+
// Build memo from accountCanisterId and intentCode for matching on services
|
|
2154
|
+
let memoBytes = undefined;
|
|
2155
|
+
try {
|
|
2156
|
+
const extra = requirement?.extra || {};
|
|
2157
|
+
const accIdStr = String(extra?.accountCanisterId || '');
|
|
2158
|
+
const icIntentCodeStr = String(extra?.intentCode || '');
|
|
2159
|
+
const accIdNum = accIdStr ? parseInt(accIdStr, 10) : 0;
|
|
2160
|
+
const icIntentCodeNum = icIntentCodeStr ? parseInt(icIntentCodeStr, 10) : 0;
|
|
2161
|
+
if (Number.isFinite(accIdNum) && accIdNum > 0 && Number.isFinite(icIntentCodeNum) && icIntentCodeNum > 0) {
|
|
2162
|
+
const packed = this.createPackedMemo(accIdNum, icIntentCodeNum);
|
|
2163
|
+
memoBytes = Array.from(packed);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
catch { }
|
|
2167
|
+
// Call API to settle IC x402 via services (controller will pull + notify)
|
|
2168
|
+
const settleRespIc = await this.publicApiClient.post('/sdk/public/payments/x402/settle', {
|
|
2169
|
+
paymentIntentId,
|
|
2170
|
+
paymentHeader: null, // not used for IC allowance path
|
|
2171
|
+
paymentRequirements: requirement,
|
|
2172
|
+
payerPrincipal,
|
|
2173
|
+
memoBytes: memoBytes || null,
|
|
2174
|
+
});
|
|
2175
|
+
const statusIc = (settleRespIc?.status || settleRespIc?.paymentIntent?.status || 'completed').toString().toLowerCase();
|
|
2176
|
+
const amountIc = (settleRespIc?.paymentIntent?.amount && String(settleRespIc.paymentIntent.amount)) ||
|
|
2177
|
+
(typeof usdAmount === 'number' ? String(usdAmount) : request?.amount?.toString?.() || '0');
|
|
2178
|
+
const outIc = {
|
|
2179
|
+
transactionId: Number(settleRespIc?.canisterTxId || 0),
|
|
2180
|
+
status: statusIc === 'succeeded' ? 'completed' : statusIc,
|
|
2181
|
+
amount: amountIc,
|
|
2182
|
+
recipientCanister: this.icpayCanisterId,
|
|
2183
|
+
timestamp: new Date(),
|
|
2184
|
+
metadata: { ...(request.metadata || {}), icpay_x402: true, icpay_network: 'ic' },
|
|
2185
|
+
payment: settleRespIc || null,
|
|
2186
|
+
};
|
|
2187
|
+
const isTerminalIc = (() => {
|
|
2188
|
+
const s = String(outIc.status || '').toLowerCase();
|
|
2189
|
+
return s === 'completed' || s === 'succeeded' || s === 'failed' || s === 'canceled' || s === 'cancelled' || s === 'mismatched';
|
|
2190
|
+
})();
|
|
2191
|
+
if (isTerminalIc) {
|
|
2192
|
+
if (outIc.status === 'completed') {
|
|
2193
|
+
this.emit('icpay-sdk-transaction-completed', outIc);
|
|
2194
|
+
}
|
|
2195
|
+
else if (outIc.status === 'failed') {
|
|
2196
|
+
this.emit('icpay-sdk-transaction-failed', outIc);
|
|
2197
|
+
}
|
|
2198
|
+
else {
|
|
2199
|
+
this.emit('icpay-sdk-transaction-updated', outIc);
|
|
2200
|
+
}
|
|
2201
|
+
this.emitMethodSuccess('createPaymentX402Usd', outIc);
|
|
2202
|
+
return outIc;
|
|
2203
|
+
}
|
|
2204
|
+
try {
|
|
2205
|
+
this.emit('icpay-sdk-transaction-updated', outIc);
|
|
2206
|
+
}
|
|
2207
|
+
catch { }
|
|
2208
|
+
const waitedIc = await this.awaitIntentTerminal({
|
|
2209
|
+
paymentIntentId,
|
|
2210
|
+
ledgerCanisterId: asset,
|
|
2211
|
+
amount: amountIc,
|
|
2212
|
+
canisterTransactionId: String(settleRespIc?.canisterTxId || ''),
|
|
2213
|
+
metadata: { ...(request.metadata || {}), icpay_x402: true, icpay_network: 'ic' },
|
|
2214
|
+
});
|
|
2215
|
+
this.emitMethodSuccess('createPaymentX402Usd', waitedIc);
|
|
2216
|
+
return waitedIc;
|
|
2217
|
+
}
|
|
2218
|
+
if (!isSol) {
|
|
2219
|
+
paymentHeader = await (0, builders_1.buildAndSignX402PaymentHeader)(requirement, {
|
|
2220
|
+
x402Version: Number(data?.x402Version || 2),
|
|
2221
|
+
debug: this.config?.debug || false,
|
|
2222
|
+
provider: providerForHeader,
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
if (isSol) {
|
|
2226
|
+
// Solana x402: prefer message-sign flow if server provided a signable message
|
|
2227
|
+
const signableMsgB64 = requirement?.extra?.signableMessageBase64;
|
|
2228
|
+
const signableFields = requirement?.extra?.signableFields || {};
|
|
2229
|
+
const wSolCtx = globalThis?.window || globalThis;
|
|
2230
|
+
const sol = providerForHeader;
|
|
2231
|
+
if (!sol)
|
|
2232
|
+
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.WALLET_PROVIDER_NOT_AVAILABLE, message: 'Solana provider not available (window.solana)' });
|
|
2233
|
+
// Get public key (already connected from widget)
|
|
2234
|
+
let fromBase58 = null;
|
|
2235
|
+
try {
|
|
2236
|
+
fromBase58 = sol?.publicKey?.toBase58?.() || sol?.publicKey || null;
|
|
2237
|
+
}
|
|
2238
|
+
catch { }
|
|
2239
|
+
if (!fromBase58 && typeof sol.connect === 'function') {
|
|
2240
|
+
try {
|
|
2241
|
+
const con = await sol.connect({ onlyIfTrusted: false });
|
|
2242
|
+
fromBase58 = con?.publicKey?.toBase58?.() || con?.publicKey || null;
|
|
2243
|
+
}
|
|
2244
|
+
catch { }
|
|
2245
|
+
}
|
|
2246
|
+
if (!fromBase58)
|
|
2247
|
+
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.WALLET_NOT_CONNECTED, message: 'Solana wallet not connected' });
|
|
2248
|
+
// No ICPay popups; only wallet UI should be shown for signing
|
|
2249
|
+
if (signableMsgB64) {
|
|
2250
|
+
// Sign the provided message and settle via header (services will submit)
|
|
2251
|
+
// Ensure explicit connect prompt before signing
|
|
2252
|
+
if (typeof sol?.connect === 'function') {
|
|
2253
|
+
try {
|
|
2254
|
+
await sol.connect({ onlyIfTrusted: false });
|
|
2255
|
+
}
|
|
2256
|
+
catch { }
|
|
2257
|
+
}
|
|
2258
|
+
let sigB64 = null;
|
|
2259
|
+
const msgBytes = u8FromBase64(signableMsgB64);
|
|
2260
|
+
const msgB58ForReq = base58Encode(msgBytes);
|
|
2261
|
+
// Attempts in order (strict):
|
|
2262
|
+
// 1) Wallet Standard: request colon form with Uint8Array
|
|
2263
|
+
if (!sigB64 && sol?.request) {
|
|
2264
|
+
try {
|
|
2265
|
+
try {
|
|
2266
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(request) params', { method: 'solana:signMessage', shape: 'object{message:Uint8Array}', len: msgBytes.length });
|
|
2267
|
+
}
|
|
2268
|
+
catch { }
|
|
2269
|
+
const r0 = await sol.request({ method: 'solana:signMessage', params: { message: msgBytes } });
|
|
2270
|
+
if (typeof r0 === 'string') {
|
|
2271
|
+
try {
|
|
2272
|
+
const b = base58Decode(r0);
|
|
2273
|
+
sigB64 = b64FromBytes(b);
|
|
2274
|
+
}
|
|
2275
|
+
catch {
|
|
2276
|
+
sigB64 = r0;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
else if (r0 && typeof r0.signature === 'string') {
|
|
2280
|
+
try {
|
|
2281
|
+
const b = base58Decode(r0.signature);
|
|
2282
|
+
sigB64 = b64FromBytes(b);
|
|
2283
|
+
}
|
|
2284
|
+
catch {
|
|
2285
|
+
sigB64 = r0.signature;
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
else if (r0 && (r0.byteLength != null || ArrayBuffer.isView(r0))) {
|
|
2289
|
+
const b = r0 instanceof Uint8Array ? r0 : new Uint8Array(r0);
|
|
2290
|
+
if (b && b.length === 64)
|
|
2291
|
+
sigB64 = b64FromBytes(b);
|
|
2292
|
+
}
|
|
2293
|
+
else if (r0 && typeof r0 === 'object' && Array.isArray(r0.data)) {
|
|
2294
|
+
const b = Uint8Array.from(r0.data);
|
|
2295
|
+
if (b && b.length === 64)
|
|
2296
|
+
sigB64 = b64FromBytes(b);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
catch (e0) {
|
|
2300
|
+
try {
|
|
2301
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol solana:signMessage failed', { error: String(e0) });
|
|
2302
|
+
}
|
|
2303
|
+
catch { }
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
// 2) Native: signMessage(Uint8Array)
|
|
2307
|
+
if (!sigB64 && typeof sol?.signMessage === 'function') {
|
|
2308
|
+
try {
|
|
2309
|
+
try {
|
|
2310
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(fn) Uint8Array');
|
|
2311
|
+
}
|
|
2312
|
+
catch { }
|
|
2313
|
+
const r2 = await sol.signMessage(msgBytes);
|
|
2314
|
+
if (r2 && (r2.byteLength != null || ArrayBuffer.isView(r2))) {
|
|
2315
|
+
const b = r2 instanceof Uint8Array ? r2 : new Uint8Array(r2);
|
|
2316
|
+
if (b && b.length === 64)
|
|
2317
|
+
sigB64 = b64FromBytes(b);
|
|
2318
|
+
}
|
|
2319
|
+
else if (typeof r2 === 'string') {
|
|
2320
|
+
try {
|
|
2321
|
+
const b = base58Decode(r2);
|
|
2322
|
+
sigB64 = b64FromBytes(b);
|
|
2323
|
+
}
|
|
2324
|
+
catch {
|
|
2325
|
+
sigB64 = r2;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
else if (r2 && typeof r2 === 'object' && typeof r2.signature === 'string') {
|
|
2329
|
+
const s = r2.signature;
|
|
2330
|
+
try {
|
|
2331
|
+
const b = base58Decode(s);
|
|
2332
|
+
sigB64 = b64FromBytes(b);
|
|
2333
|
+
}
|
|
2334
|
+
catch {
|
|
2335
|
+
sigB64 = s;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
catch (e2) {
|
|
2340
|
+
try {
|
|
2341
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(fn) failed', { error: String(e2) });
|
|
2342
|
+
}
|
|
2343
|
+
catch { }
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
// 3) Request: signMessage with Uint8Array payload (legacy method name)
|
|
2347
|
+
if (!sigB64 && sol?.request) {
|
|
2348
|
+
try {
|
|
2349
|
+
try {
|
|
2350
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(request) params', { method: 'signMessage', shape: 'object{message:Uint8Array}', len: msgBytes.length });
|
|
2351
|
+
}
|
|
2352
|
+
catch { }
|
|
2353
|
+
const r3 = await sol.request({ method: 'signMessage', params: { message: msgBytes } });
|
|
2354
|
+
if (typeof r3 === 'string') {
|
|
2355
|
+
try {
|
|
2356
|
+
const b = base58Decode(r3);
|
|
2357
|
+
sigB64 = b64FromBytes(b);
|
|
2358
|
+
}
|
|
2359
|
+
catch {
|
|
2360
|
+
sigB64 = r3;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
else if (r3 && typeof r3.signature === 'string') {
|
|
2364
|
+
try {
|
|
2365
|
+
const b = base58Decode(r3.signature);
|
|
2366
|
+
sigB64 = b64FromBytes(b);
|
|
2367
|
+
}
|
|
2368
|
+
catch {
|
|
2369
|
+
sigB64 = r3.signature;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
else if (r3 && (r3.byteLength != null || ArrayBuffer.isView(r3))) {
|
|
2373
|
+
const b = r3 instanceof Uint8Array ? r3 : new Uint8Array(r3);
|
|
2374
|
+
if (b && b.length === 64)
|
|
2375
|
+
sigB64 = b64FromBytes(b);
|
|
2376
|
+
}
|
|
2377
|
+
else if (r3 && typeof r3 === 'object' && Array.isArray(r3.data)) {
|
|
2378
|
+
const b = Uint8Array.from(r3.data);
|
|
2379
|
+
if (b && b.length === 64)
|
|
2380
|
+
sigB64 = b64FromBytes(b);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
catch (e3) {
|
|
2384
|
+
try {
|
|
2385
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(request Uint8Array) failed', { error: String(e3) });
|
|
2386
|
+
}
|
|
2387
|
+
catch { }
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
// 4) Request: signMessage with base58
|
|
2391
|
+
if (!sigB64 && sol?.request) {
|
|
2392
|
+
try {
|
|
2393
|
+
try {
|
|
2394
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(request) params', { method: 'signMessage', shape: 'object{message:base58}', len: msgB58ForReq.length });
|
|
2395
|
+
}
|
|
2396
|
+
catch { }
|
|
2397
|
+
const r4 = await sol.request({ method: 'signMessage', params: { message: msgB58ForReq } });
|
|
2398
|
+
if (typeof r4 === 'string') {
|
|
2399
|
+
try {
|
|
2400
|
+
const b = base58Decode(r4);
|
|
2401
|
+
sigB64 = b64FromBytes(b);
|
|
2402
|
+
}
|
|
2403
|
+
catch {
|
|
2404
|
+
sigB64 = r4;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
else if (r4 && typeof r4.signature === 'string') {
|
|
2408
|
+
try {
|
|
2409
|
+
const b = base58Decode(r4.signature);
|
|
2410
|
+
sigB64 = b64FromBytes(b);
|
|
2411
|
+
}
|
|
2412
|
+
catch {
|
|
2413
|
+
sigB64 = r4.signature;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
else if (r4 && (r4.byteLength != null || ArrayBuffer.isView(r4))) {
|
|
2417
|
+
const b = r4 instanceof Uint8Array ? r4 : new Uint8Array(r4);
|
|
2418
|
+
if (b && b.length === 64)
|
|
2419
|
+
sigB64 = b64FromBytes(b);
|
|
2420
|
+
}
|
|
2421
|
+
else if (r4 && typeof r4 === 'object' && Array.isArray(r4.data)) {
|
|
2422
|
+
const b = Uint8Array.from(r4.data);
|
|
2423
|
+
if (b && b.length === 64)
|
|
2424
|
+
sigB64 = b64FromBytes(b);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
catch (e4) {
|
|
2428
|
+
try {
|
|
2429
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(request legacy) failed', { error: String(e4) });
|
|
2430
|
+
}
|
|
2431
|
+
catch { }
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
if (sigB64) {
|
|
2435
|
+
// Build x402 header and settle
|
|
2436
|
+
const header = (0, builders_1.buildX402HeaderFromAuthorization)({
|
|
2437
|
+
x402Version: Number(data?.x402Version || 2),
|
|
2438
|
+
scheme: String(requirement?.scheme || 'exact'),
|
|
2439
|
+
network: String(requirement?.network || ''),
|
|
2440
|
+
from: String(fromBase58 || ''),
|
|
2441
|
+
to: String(requirement?.payTo || ''),
|
|
2442
|
+
value: String(requirement?.maxAmountRequired || '0'),
|
|
2443
|
+
validAfter: String(signableFields?.validAfter || '0'),
|
|
2444
|
+
validBefore: String(signableFields?.validBefore || '0'),
|
|
2445
|
+
nonce: String(signableFields?.nonceHex || ''),
|
|
2446
|
+
signature: String(sigB64),
|
|
2447
|
+
});
|
|
2448
|
+
const headerJson = JSON.stringify(header);
|
|
2449
|
+
const headerB64 = (() => {
|
|
2450
|
+
try {
|
|
2451
|
+
const Buf = globalThis.Buffer;
|
|
2452
|
+
return Buf ? Buf.from(headerJson, 'utf8').toString('base64') : globalThis?.btoa?.(headerJson) || '';
|
|
2453
|
+
}
|
|
2454
|
+
catch {
|
|
2455
|
+
return '';
|
|
2456
|
+
}
|
|
2457
|
+
})();
|
|
2458
|
+
const settleRespSol = await this.publicApiClient.post('/sdk/public/payments/x402/settle', {
|
|
2459
|
+
paymentIntentId,
|
|
2460
|
+
paymentHeader: headerB64,
|
|
2461
|
+
paymentRequirements: requirement,
|
|
2462
|
+
});
|
|
2463
|
+
try {
|
|
2464
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'x402 (sol) settle via header response', {
|
|
2465
|
+
ok: settleRespSol?.ok,
|
|
2466
|
+
status: settleRespSol?.status,
|
|
2467
|
+
paymentIntentId: settleRespSol?.paymentIntent?.id,
|
|
2468
|
+
paymentId: settleRespSol?.payment?.id,
|
|
2469
|
+
rawKeys: Object.keys(settleRespSol || {}),
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
catch { }
|
|
2473
|
+
const statusSolHdr = (settleRespSol?.status || settleRespSol?.paymentIntent?.status || 'completed').toString().toLowerCase();
|
|
2474
|
+
const amountSolHdr = (settleRespSol?.paymentIntent?.amount && String(settleRespSol.paymentIntent.amount)) ||
|
|
2475
|
+
(typeof usdAmount === 'number' ? String(usdAmount) : request?.amount?.toString?.() || '0');
|
|
2476
|
+
const outSolHdr = {
|
|
2477
|
+
transactionId: Number(settleRespSol?.canisterTxId || 0),
|
|
2478
|
+
status: statusSolHdr === 'succeeded' ? 'completed' : statusSolHdr,
|
|
2479
|
+
amount: amountSolHdr,
|
|
2480
|
+
recipientCanister: ledgerCanisterId,
|
|
2481
|
+
timestamp: new Date(),
|
|
2482
|
+
metadata: { ...(request.metadata || {}), icpay_x402: true },
|
|
2483
|
+
payment: settleRespSol || null,
|
|
2484
|
+
};
|
|
2485
|
+
const isTerminalSolHdr = (() => {
|
|
2486
|
+
const s = String(outSolHdr.status || '').toLowerCase();
|
|
2487
|
+
return s === 'completed' || s === 'succeeded' || s === 'failed' || s === 'canceled' || s === 'cancelled' || s === 'mismatched';
|
|
2488
|
+
})();
|
|
2489
|
+
if (isTerminalSolHdr) {
|
|
2490
|
+
if (outSolHdr.status === 'completed') {
|
|
2491
|
+
this.emit('icpay-sdk-transaction-completed', outSolHdr);
|
|
2492
|
+
}
|
|
2493
|
+
else if (outSolHdr.status === 'failed') {
|
|
2494
|
+
this.emit('icpay-sdk-transaction-failed', outSolHdr);
|
|
2495
|
+
}
|
|
2496
|
+
else {
|
|
2497
|
+
this.emit('icpay-sdk-transaction-updated', outSolHdr);
|
|
2498
|
+
}
|
|
2499
|
+
this.emitMethodSuccess('createPaymentX402Usd', outSolHdr);
|
|
2500
|
+
return outSolHdr;
|
|
2501
|
+
}
|
|
2502
|
+
// Non-terminal: wait until terminal via notify loop
|
|
2503
|
+
try {
|
|
2504
|
+
this.emit('icpay-sdk-transaction-updated', outSolHdr);
|
|
2505
|
+
}
|
|
2506
|
+
catch { }
|
|
2507
|
+
const waitedSolHdr = await this.awaitIntentTerminal({
|
|
2508
|
+
paymentIntentId,
|
|
2509
|
+
ledgerCanisterId: ledgerCanisterId,
|
|
2510
|
+
amount: amountSolHdr,
|
|
2511
|
+
metadata: { ...(request.metadata || {}), icpay_x402: true },
|
|
2512
|
+
});
|
|
2513
|
+
this.emitMethodSuccess('createPaymentX402Usd', waitedSolHdr);
|
|
2514
|
+
return waitedSolHdr;
|
|
2515
|
+
}
|
|
2516
|
+
else {
|
|
2517
|
+
// Fallback: if API provided an unsigned transaction, try transaction-signing path
|
|
2518
|
+
const fallbackTx = requirement?.extra?.transactionBase64;
|
|
2519
|
+
if (!fallbackTx) {
|
|
2520
|
+
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.TRANSACTION_FAILED, message: 'Wallet did not sign message' });
|
|
2521
|
+
}
|
|
2522
|
+
// Inject for transaction-signing fallback below
|
|
2523
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
2524
|
+
const __fallbackTxBase64 = String(fallbackTx);
|
|
2525
|
+
// Reassign txBase64 by creating a new block scope later; use a marker in metadata to indicate fallback used
|
|
2526
|
+
try {
|
|
2527
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol x402 fallback to transaction signing');
|
|
2528
|
+
}
|
|
2529
|
+
catch { }
|
|
2530
|
+
// Use local variable for fallback path
|
|
2531
|
+
let signedTxB64 = null;
|
|
2532
|
+
let signerSigBase58 = null;
|
|
2533
|
+
const inlineMsgB58Fallback = requirement?.extra?.messageBase58 || undefined;
|
|
2534
|
+
// Ensure explicit connect to prompt wallet if needed
|
|
2535
|
+
if (typeof sol?.connect === 'function') {
|
|
2536
|
+
try {
|
|
2537
|
+
await sol.connect({ onlyIfTrusted: false });
|
|
2538
|
+
}
|
|
2539
|
+
catch { }
|
|
2540
|
+
}
|
|
2541
|
+
// Do NOT submit from wallet; only sign, then relay to backend
|
|
2542
|
+
// Try signAllTransactions (array) then signTransaction with message:base58 (prefer) then transaction:base64
|
|
2543
|
+
if (sol?.request) {
|
|
2544
|
+
try {
|
|
2545
|
+
let r = null;
|
|
2546
|
+
// 0) Try Wallet Standard signAllTransactions with Uint8Array
|
|
2547
|
+
try {
|
|
2548
|
+
const txBytes = u8FromBase64(__fallbackTxBase64);
|
|
2549
|
+
try {
|
|
2550
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signAllTransactions(request) params', { method: 'solana:signAllTransactions', shape: 'object{transactions:Uint8Array[]}', count: 1, txLen: txBytes.length });
|
|
2551
|
+
}
|
|
2552
|
+
catch { }
|
|
2553
|
+
r = await sol.request({ method: 'solana:signAllTransactions', params: { transactions: [txBytes] } });
|
|
2554
|
+
}
|
|
2555
|
+
catch { }
|
|
2556
|
+
// 0b) Legacy signAllTransactions with Uint8Array
|
|
2557
|
+
if (!r) {
|
|
2558
|
+
try {
|
|
2559
|
+
const txBytes = u8FromBase64(__fallbackTxBase64);
|
|
2560
|
+
try {
|
|
2561
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signAllTransactions(request) params', { method: 'signAllTransactions', shape: 'object{transactions:Uint8Array[]}', count: 1, txLen: txBytes.length });
|
|
2562
|
+
}
|
|
2563
|
+
catch { }
|
|
2564
|
+
r = await sol.request({ method: 'signAllTransactions', params: { transactions: [txBytes] } });
|
|
2565
|
+
}
|
|
2566
|
+
catch { }
|
|
2567
|
+
}
|
|
2568
|
+
// 0c) signAllTransactions with base64 strings
|
|
2569
|
+
if (!r) {
|
|
2570
|
+
try {
|
|
2571
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signAllTransactions(request) params', { method: 'solana:signAllTransactions', shape: 'object{transactions:base64[]}', count: 1, txLen: __fallbackTxBase64.length });
|
|
2572
|
+
}
|
|
2573
|
+
catch { }
|
|
2574
|
+
try {
|
|
2575
|
+
r = await sol.request({ method: 'solana:signAllTransactions', params: { transactions: [__fallbackTxBase64] } });
|
|
2576
|
+
}
|
|
2577
|
+
catch { }
|
|
2578
|
+
}
|
|
2579
|
+
if (!r) {
|
|
2580
|
+
try {
|
|
2581
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signAllTransactions(request) params', { method: 'signAllTransactions', shape: 'object{transactions:base64[]}', count: 1, txLen: __fallbackTxBase64.length });
|
|
2582
|
+
}
|
|
2583
|
+
catch { }
|
|
2584
|
+
try {
|
|
2585
|
+
r = await sol.request({ method: 'signAllTransactions', params: { transactions: [__fallbackTxBase64] } });
|
|
2586
|
+
}
|
|
2587
|
+
catch { }
|
|
2588
|
+
}
|
|
2589
|
+
if (r) {
|
|
2590
|
+
// Normalize array responses
|
|
2591
|
+
const arr = Array.isArray(r) ? r : (Array.isArray(r?.transactions) ? r.transactions : null);
|
|
2592
|
+
if (arr && arr.length > 0) {
|
|
2593
|
+
const first = arr[0];
|
|
2594
|
+
if (typeof first === 'string') {
|
|
2595
|
+
const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(first) && first.length % 4 === 0;
|
|
2596
|
+
if (looksBase64)
|
|
2597
|
+
signedTxB64 = first;
|
|
2598
|
+
}
|
|
2599
|
+
else if (first && (first.byteLength != null || ArrayBuffer.isView(first))) {
|
|
2600
|
+
const b = first instanceof Uint8Array ? first : new Uint8Array(first);
|
|
2601
|
+
// If it's a raw signature (64 bytes) treat as signature; otherwise treat as signed tx bytes
|
|
2602
|
+
if (b.length === 64)
|
|
2603
|
+
signerSigBase58 = base58Encode(b);
|
|
2604
|
+
else
|
|
2605
|
+
signedTxB64 = b64FromBytes(b);
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
if (signedTxB64 || signerSigBase58) {
|
|
2610
|
+
// short-circuit to relay below
|
|
2611
|
+
}
|
|
2612
|
+
// 1) Try message: base58 (string form first)
|
|
2613
|
+
if (inlineMsgB58Fallback) {
|
|
2614
|
+
try {
|
|
2615
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'message:string(base58)', msgLen: inlineMsgB58Fallback.length });
|
|
2616
|
+
}
|
|
2617
|
+
catch { }
|
|
2618
|
+
try {
|
|
2619
|
+
r = await sol.request({ method: 'signTransaction', params: inlineMsgB58Fallback });
|
|
2620
|
+
}
|
|
2621
|
+
catch { }
|
|
2622
|
+
let candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
|
|
2623
|
+
if (!signedTxB64 && !signerSigBase58) {
|
|
2624
|
+
if (typeof candidate === 'string') {
|
|
2625
|
+
const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
|
|
2626
|
+
if (looksBase64)
|
|
2627
|
+
signedTxB64 = candidate;
|
|
2628
|
+
else
|
|
2629
|
+
signerSigBase58 = candidate;
|
|
2630
|
+
}
|
|
2631
|
+
else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
|
|
2632
|
+
const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
|
|
2633
|
+
if (b.length === 64)
|
|
2634
|
+
signerSigBase58 = base58Encode(b);
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
// 1b) Try message: base58 (array form)
|
|
2638
|
+
if (!signedTxB64 && !signerSigBase58) {
|
|
2639
|
+
try {
|
|
2640
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'message:[string]', msgLen: inlineMsgB58Fallback.length });
|
|
2641
|
+
}
|
|
2642
|
+
catch { }
|
|
2643
|
+
try {
|
|
2644
|
+
r = await sol.request({ method: 'signTransaction', params: [inlineMsgB58Fallback] });
|
|
2645
|
+
}
|
|
2646
|
+
catch { }
|
|
2647
|
+
candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
|
|
2648
|
+
if (typeof candidate === 'string') {
|
|
2649
|
+
const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
|
|
2650
|
+
if (looksBase64)
|
|
2651
|
+
signedTxB64 = candidate;
|
|
2652
|
+
else
|
|
2653
|
+
signerSigBase58 = candidate;
|
|
2654
|
+
}
|
|
2655
|
+
else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
|
|
2656
|
+
const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
|
|
2657
|
+
if (b.length === 64)
|
|
2658
|
+
signerSigBase58 = base58Encode(b);
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
// 1c) Try message: base58 (object form)
|
|
2662
|
+
if (!signedTxB64 && !signerSigBase58) {
|
|
2663
|
+
try {
|
|
2664
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'message:object{message}', msgLen: inlineMsgB58Fallback.length });
|
|
2665
|
+
}
|
|
2666
|
+
catch { }
|
|
2667
|
+
try {
|
|
2668
|
+
r = await sol.request({ method: 'signTransaction', params: { message: inlineMsgB58Fallback } });
|
|
2669
|
+
}
|
|
2670
|
+
catch { }
|
|
2671
|
+
candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
|
|
2672
|
+
if (typeof candidate === 'string') {
|
|
2673
|
+
const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
|
|
2674
|
+
if (looksBase64)
|
|
2675
|
+
signedTxB64 = candidate;
|
|
2676
|
+
else
|
|
2677
|
+
signerSigBase58 = candidate;
|
|
2678
|
+
}
|
|
2679
|
+
else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
|
|
2680
|
+
const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
|
|
2681
|
+
if (b.length === 64)
|
|
2682
|
+
signerSigBase58 = base58Encode(b);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
// 2) If still nothing, try transaction: base64
|
|
2687
|
+
if (!signedTxB64 && !signerSigBase58) {
|
|
2688
|
+
// Wallet Standard first
|
|
2689
|
+
try {
|
|
2690
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'solana:signTransaction', paramShape: 'object{transaction:base64}', txLen: __fallbackTxBase64.length });
|
|
2691
|
+
}
|
|
2692
|
+
catch { }
|
|
2693
|
+
try {
|
|
2694
|
+
r = await sol.request({ method: 'solana:signTransaction', params: { transaction: __fallbackTxBase64 } });
|
|
2695
|
+
}
|
|
2696
|
+
catch { }
|
|
2697
|
+
let candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
|
|
2698
|
+
if (typeof candidate === 'string') {
|
|
2699
|
+
const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
|
|
2700
|
+
if (looksBase64)
|
|
2701
|
+
signedTxB64 = candidate;
|
|
2702
|
+
else
|
|
2703
|
+
signerSigBase58 = candidate;
|
|
2704
|
+
}
|
|
2705
|
+
else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
|
|
2706
|
+
const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
|
|
2707
|
+
if (b.length === 64)
|
|
2708
|
+
signerSigBase58 = base58Encode(b);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
if (!signedTxB64 && !signerSigBase58) {
|
|
2712
|
+
// Legacy string form
|
|
2713
|
+
try {
|
|
2714
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'transaction:string(base64)', txLen: __fallbackTxBase64.length });
|
|
2715
|
+
}
|
|
2716
|
+
catch { }
|
|
2717
|
+
try {
|
|
2718
|
+
r = await sol.request({ method: 'signTransaction', params: __fallbackTxBase64 });
|
|
2719
|
+
}
|
|
2720
|
+
catch { }
|
|
2721
|
+
let candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
|
|
2722
|
+
if (typeof candidate === 'string') {
|
|
2723
|
+
const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
|
|
2724
|
+
if (looksBase64)
|
|
2725
|
+
signedTxB64 = candidate;
|
|
2726
|
+
else
|
|
2727
|
+
signerSigBase58 = candidate;
|
|
2728
|
+
}
|
|
2729
|
+
else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
|
|
2730
|
+
const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
|
|
2731
|
+
if (b.length === 64)
|
|
2732
|
+
signerSigBase58 = base58Encode(b);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
if (!signedTxB64 && !signerSigBase58) {
|
|
2736
|
+
// Legacy array form
|
|
2737
|
+
try {
|
|
2738
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'transaction:[string]', txLen: __fallbackTxBase64.length });
|
|
2739
|
+
}
|
|
2740
|
+
catch { }
|
|
2741
|
+
try {
|
|
2742
|
+
r = await sol.request({ method: 'signTransaction', params: [__fallbackTxBase64] });
|
|
2743
|
+
}
|
|
2744
|
+
catch { }
|
|
2745
|
+
const candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
|
|
2746
|
+
if (typeof candidate === 'string') {
|
|
2747
|
+
const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
|
|
2748
|
+
if (looksBase64)
|
|
2749
|
+
signedTxB64 = candidate;
|
|
2750
|
+
else
|
|
2751
|
+
signerSigBase58 = candidate;
|
|
2752
|
+
}
|
|
2753
|
+
else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
|
|
2754
|
+
const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
|
|
2755
|
+
if (b.length === 64)
|
|
2756
|
+
signerSigBase58 = base58Encode(b);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
if (!signedTxB64 && !signerSigBase58) {
|
|
2760
|
+
// Legacy object form
|
|
2761
|
+
try {
|
|
2762
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'transaction:object{transaction}', txLen: __fallbackTxBase64.length });
|
|
2763
|
+
}
|
|
2764
|
+
catch { }
|
|
2765
|
+
try {
|
|
2766
|
+
r = await sol.request({ method: 'signTransaction', params: { transaction: __fallbackTxBase64 } });
|
|
2767
|
+
}
|
|
2768
|
+
catch { }
|
|
2769
|
+
const candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
|
|
2770
|
+
if (typeof candidate === 'string') {
|
|
2771
|
+
const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
|
|
2772
|
+
if (looksBase64)
|
|
2773
|
+
signedTxB64 = candidate;
|
|
2774
|
+
else
|
|
2775
|
+
signerSigBase58 = candidate;
|
|
2776
|
+
}
|
|
2777
|
+
else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
|
|
2778
|
+
const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
|
|
2779
|
+
if (b.length === 64)
|
|
2780
|
+
signerSigBase58 = base58Encode(b);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
catch { }
|
|
2785
|
+
}
|
|
2786
|
+
let settleResp;
|
|
2787
|
+
if (signedTxB64 && typeof signedTxB64 === 'string') {
|
|
2788
|
+
settleResp = await this.publicApiClient.post('/sdk/public/payments/x402/relay', {
|
|
2789
|
+
paymentIntentId,
|
|
2790
|
+
signedTransactionBase64: signedTxB64,
|
|
2791
|
+
rpcUrlPublic: requirement?.extra?.rpcUrlPublic || null,
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
else if (signerSigBase58) {
|
|
2795
|
+
settleResp = await this.publicApiClient.post('/sdk/public/payments/x402/relay', {
|
|
2796
|
+
paymentIntentId,
|
|
2797
|
+
signatureBase58: signerSigBase58,
|
|
2798
|
+
transactionBase64: __fallbackTxBase64,
|
|
2799
|
+
payerPublicKey: fromBase58,
|
|
2800
|
+
rpcUrlPublic: requirement?.extra?.rpcUrlPublic || null,
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
else {
|
|
2804
|
+
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.TRANSACTION_FAILED, message: 'Wallet did not return a signed transaction' });
|
|
2805
|
+
}
|
|
2806
|
+
const statusSol = (settleResp?.status || settleResp?.paymentIntent?.status || 'completed').toString().toLowerCase();
|
|
2807
|
+
const amountSol = (settleResp?.paymentIntent?.amount && String(settleResp.paymentIntent.amount)) ||
|
|
2808
|
+
(typeof usdAmount === 'number' ? String(usdAmount) : request?.amount?.toString?.() || '0');
|
|
2809
|
+
const outSol = {
|
|
2810
|
+
transactionId: Number(settleResp?.canisterTxId || 0),
|
|
2811
|
+
status: statusSol === 'succeeded' ? 'completed' : statusSol,
|
|
2812
|
+
amount: amountSol,
|
|
2813
|
+
recipientCanister: ledgerCanisterId,
|
|
2814
|
+
timestamp: new Date(),
|
|
2815
|
+
metadata: { ...(request.metadata || {}), icpay_x402: true },
|
|
2816
|
+
payment: settleResp || null,
|
|
2817
|
+
};
|
|
2818
|
+
this.emitMethodSuccess('createPaymentX402Usd', outSol);
|
|
2819
|
+
return outSol;
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
// Otherwise fall back to unsigned transaction flow if provided
|
|
2823
|
+
const inlineTx = requirement?.extra?.transactionBase64;
|
|
2824
|
+
const wSolCtx2 = globalThis?.window || globalThis;
|
|
2825
|
+
// fromBase58 already determined
|
|
2826
|
+
// Build signer-based transaction, user signs it, and server relays
|
|
2827
|
+
let txBase64;
|
|
2828
|
+
if (inlineTx) {
|
|
2829
|
+
txBase64 = String(inlineTx);
|
|
2830
|
+
}
|
|
2831
|
+
else {
|
|
2832
|
+
throw new errors_1.IcpayError({
|
|
2833
|
+
code: errors_1.ICPAY_ERROR_CODES.API_ERROR,
|
|
2834
|
+
message: 'X402 missing transactionBase64 in 402 response for Solana',
|
|
2835
|
+
details: { note: 'API must include extra.transactionBase64 to avoid prepare' }
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
// Sign-only with wallet, then relay server-side (fee payer = relayer)
|
|
2839
|
+
// Prefer request-based signTransaction with base58 "message" (serialized tx bytes)
|
|
2840
|
+
let signedTxB64 = null;
|
|
2841
|
+
let signerSigBase58 = null;
|
|
2842
|
+
const inlineMsgB58 = requirement?.extra?.messageBase58;
|
|
2843
|
+
const msgB58 = inlineMsgB58 || undefined;
|
|
2844
|
+
try {
|
|
2845
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol x402 payload sizes', {
|
|
2846
|
+
txBase64Len: txBase64?.length || 0,
|
|
2847
|
+
hasInlineMsgB58: !!inlineMsgB58,
|
|
2848
|
+
msgB58Len: msgB58?.length || 0,
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
catch { }
|
|
2852
|
+
// Do NOT submit from wallet; only sign, then relay to backend
|
|
2853
|
+
// For wallets that require request-based signing
|
|
2854
|
+
const preferTransactionSigning = true;
|
|
2855
|
+
// Try to sign using transaction MESSAGE first (wallets often expect base58 message)
|
|
2856
|
+
if (!signedTxB64 && sol?.request) {
|
|
2857
|
+
try {
|
|
2858
|
+
let r = null;
|
|
2859
|
+
// Some wallets expect bare string or array for message
|
|
2860
|
+
if (msgB58) {
|
|
2861
|
+
// Ensure a visible connect prompt if required by the wallet
|
|
2862
|
+
if (typeof sol?.connect === 'function') {
|
|
2863
|
+
try {
|
|
2864
|
+
await sol.connect({ onlyIfTrusted: false });
|
|
2865
|
+
}
|
|
2866
|
+
catch { }
|
|
2867
|
+
}
|
|
2868
|
+
// Prefer object form with Uint8Array message first
|
|
2869
|
+
try {
|
|
2870
|
+
const msgBytesU8 = (() => { try {
|
|
2871
|
+
return base58Decode(msgB58);
|
|
2872
|
+
}
|
|
2873
|
+
catch {
|
|
2874
|
+
return new Uint8Array();
|
|
2875
|
+
} })();
|
|
2876
|
+
if (msgBytesU8.length > 0) {
|
|
2877
|
+
try {
|
|
2878
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'object{message:Uint8Array}', msgLen: msgBytesU8.length });
|
|
2879
|
+
}
|
|
2880
|
+
catch { }
|
|
2881
|
+
r = await sol.request({ method: 'signTransaction', params: { message: msgBytesU8 } });
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
catch { }
|
|
2885
|
+
try {
|
|
2886
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'message:string(base58)', msgLen: msgB58.length });
|
|
2887
|
+
}
|
|
2888
|
+
catch { }
|
|
2889
|
+
try {
|
|
2890
|
+
r = await sol.request({ method: 'signTransaction', params: msgB58 });
|
|
2891
|
+
}
|
|
2892
|
+
catch (eM1) {
|
|
2893
|
+
try {
|
|
2894
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(message:string) failed', { error: String(eM1) });
|
|
2895
|
+
}
|
|
2896
|
+
catch { }
|
|
2897
|
+
try {
|
|
2898
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'message:[string]', msgLen: msgB58.length });
|
|
2899
|
+
}
|
|
2900
|
+
catch { }
|
|
2901
|
+
try {
|
|
2902
|
+
r = await sol.request({ method: 'signTransaction', params: [msgB58] });
|
|
2903
|
+
}
|
|
2904
|
+
catch (eM2) {
|
|
2905
|
+
try {
|
|
2906
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(message:array) failed', { error: String(eM2) });
|
|
2907
|
+
}
|
|
2908
|
+
catch { }
|
|
2909
|
+
try {
|
|
2910
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'message:object', msgLen: msgB58.length });
|
|
2911
|
+
}
|
|
2912
|
+
catch { }
|
|
2913
|
+
r = await sol.request({ method: 'signTransaction', params: { message: msgB58 } });
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
let candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
|
|
2918
|
+
try {
|
|
2919
|
+
const rawKeys = r && typeof r === 'object' ? Object.keys(r || {}) : [];
|
|
2920
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) raw result', { hasResult: !!r, rawKeys });
|
|
2921
|
+
}
|
|
2922
|
+
catch { }
|
|
2923
|
+
if (typeof candidate === 'string') {
|
|
2924
|
+
const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
|
|
2925
|
+
if (looksBase64)
|
|
2926
|
+
signedTxB64 = candidate;
|
|
2927
|
+
else
|
|
2928
|
+
signerSigBase58 = candidate;
|
|
2929
|
+
}
|
|
2930
|
+
else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
|
|
2931
|
+
try {
|
|
2932
|
+
const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
|
|
2933
|
+
if (b.length === 64)
|
|
2934
|
+
signerSigBase58 = base58Encode(b);
|
|
2935
|
+
}
|
|
2936
|
+
catch { }
|
|
2937
|
+
}
|
|
2938
|
+
else if (candidate && typeof candidate === 'object' && Array.isArray(candidate.data)) {
|
|
2939
|
+
try {
|
|
2940
|
+
const b = Uint8Array.from(candidate.data);
|
|
2941
|
+
if (b.length === 64)
|
|
2942
|
+
signerSigBase58 = base58Encode(b);
|
|
2943
|
+
}
|
|
2944
|
+
catch { }
|
|
2945
|
+
}
|
|
2946
|
+
else if (r && typeof r === 'object') {
|
|
2947
|
+
const obj = r;
|
|
2948
|
+
if (typeof obj.signature === 'string') {
|
|
2949
|
+
signerSigBase58 = obj.signature;
|
|
2950
|
+
}
|
|
2951
|
+
else if (Array.isArray(obj.signatures) && typeof obj.signatures[0] === 'string') {
|
|
2952
|
+
signerSigBase58 = obj.signatures[0];
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
catch { }
|
|
2957
|
+
}
|
|
2958
|
+
// If message-based did not yield, try transaction (prefer Uint8Array), then base64 (some wallets accept this)
|
|
2959
|
+
if (!signedTxB64 && !signerSigBase58 && sol?.request) {
|
|
2960
|
+
try {
|
|
2961
|
+
let r = null;
|
|
2962
|
+
// Prefer Uint8Array object form first to trigger wallet UI
|
|
2963
|
+
try {
|
|
2964
|
+
const txBytesU8 = u8FromBase64(txBase64);
|
|
2965
|
+
try {
|
|
2966
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'object{transaction:Uint8Array}', txLen: txBytesU8.length });
|
|
2967
|
+
}
|
|
2968
|
+
catch { }
|
|
2969
|
+
r = await sol.request({ method: 'signTransaction', params: { transaction: txBytesU8 } });
|
|
2970
|
+
}
|
|
2971
|
+
catch { }
|
|
2972
|
+
if (!r) {
|
|
2973
|
+
try {
|
|
2974
|
+
const txBytesU8b = u8FromBase64(txBase64);
|
|
2975
|
+
try {
|
|
2976
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'solana:signTransaction', paramShape: 'object{transaction:Uint8Array}', txLen: txBytesU8b.length });
|
|
2977
|
+
}
|
|
2978
|
+
catch { }
|
|
2979
|
+
r = await sol.request({ method: 'solana:signTransaction', params: { transaction: txBytesU8b } });
|
|
2980
|
+
}
|
|
2981
|
+
catch { }
|
|
2982
|
+
}
|
|
2983
|
+
// Try string param, then array, then object
|
|
2984
|
+
try {
|
|
2985
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'transaction:string(base64)', txLen: txBase64.length });
|
|
2986
|
+
}
|
|
2987
|
+
catch { }
|
|
2988
|
+
try {
|
|
2989
|
+
r = await sol.request({ method: 'signTransaction', params: txBase64 });
|
|
2990
|
+
}
|
|
2991
|
+
catch (eT1) {
|
|
2992
|
+
try {
|
|
2993
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(transaction:string) failed', { error: String(eT1) });
|
|
2994
|
+
}
|
|
2995
|
+
catch { }
|
|
2996
|
+
try {
|
|
2997
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'transaction:[string]', txLen: txBase64.length });
|
|
2998
|
+
}
|
|
2999
|
+
catch { }
|
|
3000
|
+
try {
|
|
3001
|
+
r = await sol.request({ method: 'signTransaction', params: [txBase64] });
|
|
3002
|
+
}
|
|
3003
|
+
catch (eT2) {
|
|
3004
|
+
try {
|
|
3005
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(transaction:array) failed', { error: String(eT2) });
|
|
3006
|
+
}
|
|
3007
|
+
catch { }
|
|
3008
|
+
try {
|
|
3009
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signTransaction(request) params', { method: 'signTransaction', paramShape: 'transaction:object', txLen: txBase64.length });
|
|
3010
|
+
}
|
|
3011
|
+
catch { }
|
|
3012
|
+
r = await sol.request({ method: 'signTransaction', params: { transaction: txBase64 } });
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
let candidate = (r?.signedTransaction || r?.transaction || r?.signedMessage || r);
|
|
3016
|
+
if (typeof candidate === 'string') {
|
|
3017
|
+
const looksBase64 = /^[A-Za-z0-9+/=]+$/.test(candidate) && candidate.length % 4 === 0;
|
|
3018
|
+
if (looksBase64)
|
|
3019
|
+
signedTxB64 = candidate;
|
|
3020
|
+
else
|
|
3021
|
+
signerSigBase58 = candidate;
|
|
3022
|
+
}
|
|
3023
|
+
else if (candidate && (candidate.byteLength != null || ArrayBuffer.isView(candidate))) {
|
|
3024
|
+
try {
|
|
3025
|
+
const b = candidate instanceof Uint8Array ? candidate : new Uint8Array(candidate);
|
|
3026
|
+
if (b.length === 64)
|
|
3027
|
+
signerSigBase58 = base58Encode(b);
|
|
3028
|
+
}
|
|
3029
|
+
catch { }
|
|
3030
|
+
}
|
|
3031
|
+
else if (candidate && typeof candidate === 'object' && Array.isArray(candidate.data)) {
|
|
3032
|
+
try {
|
|
3033
|
+
const b = Uint8Array.from(candidate.data);
|
|
3034
|
+
if (b.length === 64)
|
|
3035
|
+
signerSigBase58 = base58Encode(b);
|
|
3036
|
+
}
|
|
3037
|
+
catch { }
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
catch { }
|
|
3041
|
+
}
|
|
3042
|
+
// No hosted signer fallback; rely on wallet API only
|
|
3043
|
+
// Prefer signMessage path only if explicitly allowed (disabled by default)
|
|
3044
|
+
try {
|
|
3045
|
+
const signableMsgB64 = requirement?.extra?.signableMessageBase64;
|
|
3046
|
+
if (!preferTransactionSigning && !signedTxB64 && !signerSigBase58 && signableMsgB64) {
|
|
3047
|
+
const message = u8FromBase64(signableMsgB64);
|
|
3048
|
+
let sigResp = null;
|
|
3049
|
+
let signErr = null;
|
|
3050
|
+
try {
|
|
3051
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage attempts begin', { hasBuffer: !!globalThis.Buffer, msgLen: message.length });
|
|
3052
|
+
}
|
|
3053
|
+
catch { }
|
|
3054
|
+
// A) Direct signMessage with Buffer first (some wallets require Buffer)
|
|
3055
|
+
if (typeof sol?.signMessage === 'function') {
|
|
3056
|
+
try {
|
|
3057
|
+
const Buf = globalThis.Buffer;
|
|
3058
|
+
if (Buf) {
|
|
3059
|
+
try {
|
|
3060
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(Buffer)');
|
|
3061
|
+
}
|
|
3062
|
+
catch { }
|
|
3063
|
+
sigResp = await sol.signMessage(Buf.from(message));
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
catch (eA) {
|
|
3067
|
+
signErr = eA;
|
|
3068
|
+
try {
|
|
3069
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(Buffer) failed', { error: String(eA) });
|
|
3070
|
+
}
|
|
3071
|
+
catch { }
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
// B) Direct signMessage with Uint8Array
|
|
3075
|
+
if (!sigResp && typeof sol?.signMessage === 'function') {
|
|
3076
|
+
try {
|
|
3077
|
+
try {
|
|
3078
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(Uint8Array)');
|
|
3079
|
+
}
|
|
3080
|
+
catch { }
|
|
3081
|
+
;
|
|
3082
|
+
sigResp = await sol.signMessage(message);
|
|
3083
|
+
}
|
|
3084
|
+
catch (eB) {
|
|
3085
|
+
signErr = eB;
|
|
3086
|
+
try {
|
|
3087
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage(Uint8Array) failed', { error: String(eB) });
|
|
3088
|
+
}
|
|
3089
|
+
catch { }
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
// C) Direct signer.signMessage if present
|
|
3093
|
+
if (!sigResp && sol?.signer && typeof sol.signer.signMessage === 'function') {
|
|
3094
|
+
try {
|
|
3095
|
+
try {
|
|
3096
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signer.signMessage(Uint8Array)');
|
|
3097
|
+
}
|
|
3098
|
+
catch { }
|
|
3099
|
+
;
|
|
3100
|
+
sigResp = await sol.signer.signMessage(message);
|
|
3101
|
+
}
|
|
3102
|
+
catch (eC) {
|
|
3103
|
+
signErr = eC;
|
|
3104
|
+
try {
|
|
3105
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signer.signMessage failed', { error: String(eC) });
|
|
3106
|
+
}
|
|
3107
|
+
catch { }
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
// D) Wallet-standard request with array-of-bytes
|
|
3111
|
+
if (!sigResp && sol?.request) {
|
|
3112
|
+
try {
|
|
3113
|
+
try {
|
|
3114
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol request signMessage(array-of-bytes)');
|
|
3115
|
+
}
|
|
3116
|
+
catch { }
|
|
3117
|
+
;
|
|
3118
|
+
const arr = Array.from(message);
|
|
3119
|
+
sigResp = await sol.request({ method: 'signMessage', params: { message: arr, display: 'hex' } });
|
|
3120
|
+
}
|
|
3121
|
+
catch (eD) {
|
|
3122
|
+
signErr = eD;
|
|
3123
|
+
try {
|
|
3124
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol request signMessage(array-of-bytes) failed', { error: String(eD) });
|
|
3125
|
+
}
|
|
3126
|
+
catch { }
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
// E) Wallet-standard request with base58 message
|
|
3130
|
+
if (!sigResp && sol?.request) {
|
|
3131
|
+
try {
|
|
3132
|
+
try {
|
|
3133
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol request signMessage(base58)');
|
|
3134
|
+
}
|
|
3135
|
+
catch { }
|
|
3136
|
+
;
|
|
3137
|
+
const msgB58ForReq = base58Encode(message);
|
|
3138
|
+
sigResp = await sol.request({ method: 'signMessage', params: { message: msgB58ForReq, display: 'hex' } });
|
|
3139
|
+
}
|
|
3140
|
+
catch (eE) {
|
|
3141
|
+
signErr = eE;
|
|
3142
|
+
try {
|
|
3143
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol request signMessage(base58) failed', { error: String(eE) });
|
|
3144
|
+
}
|
|
3145
|
+
catch { }
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
let messageSigB64 = null;
|
|
3149
|
+
if (sigResp) {
|
|
3150
|
+
// Normalize common shapes: Uint8Array, Buffer-like, base58/base64 string, or { signature }
|
|
3151
|
+
if (typeof sigResp === 'string') {
|
|
3152
|
+
// Could be base58 or base64; try base58 first
|
|
3153
|
+
try {
|
|
3154
|
+
const asBytes = base58Decode(sigResp);
|
|
3155
|
+
messageSigB64 = b64FromBytes(asBytes);
|
|
3156
|
+
}
|
|
3157
|
+
catch {
|
|
3158
|
+
// assume base64 already
|
|
3159
|
+
messageSigB64 = sigResp;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
else if (sigResp && (sigResp.byteLength != null || ArrayBuffer.isView(sigResp))) {
|
|
3163
|
+
try {
|
|
3164
|
+
const bytes = sigResp instanceof Uint8Array ? sigResp : new Uint8Array(sigResp);
|
|
3165
|
+
if (bytes && bytes.length === 64) {
|
|
3166
|
+
messageSigB64 = b64FromBytes(bytes);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
catch { }
|
|
3170
|
+
}
|
|
3171
|
+
else if (sigResp && typeof sigResp === 'object') {
|
|
3172
|
+
const obj = sigResp;
|
|
3173
|
+
if (typeof obj.signature === 'string') {
|
|
3174
|
+
try {
|
|
3175
|
+
const asBytes = base58Decode(obj.signature);
|
|
3176
|
+
messageSigB64 = b64FromBytes(asBytes);
|
|
3177
|
+
}
|
|
3178
|
+
catch {
|
|
3179
|
+
messageSigB64 = obj.signature;
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
else if (Array.isArray(obj.data)) {
|
|
3183
|
+
try {
|
|
3184
|
+
const bytes = Uint8Array.from(obj.data);
|
|
3185
|
+
if (bytes && bytes.length === 64) {
|
|
3186
|
+
messageSigB64 = b64FromBytes(bytes);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
catch { }
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
try {
|
|
3193
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage result normalized (message signature)', {
|
|
3194
|
+
hasSignature: !!messageSigB64,
|
|
3195
|
+
signatureLen: messageSigB64 ? messageSigB64.length : null,
|
|
3196
|
+
});
|
|
3197
|
+
}
|
|
3198
|
+
catch { }
|
|
3199
|
+
}
|
|
3200
|
+
else if (signErr) {
|
|
3201
|
+
try {
|
|
3202
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'sol signMessage failed (all attempts)', { error: String(signErr) });
|
|
3203
|
+
}
|
|
3204
|
+
catch { }
|
|
3205
|
+
}
|
|
3206
|
+
// If we obtained a message signature, construct x402 header and settle via API
|
|
3207
|
+
if (messageSigB64) {
|
|
3208
|
+
const fields = (requirement?.extra?.signableFields || {});
|
|
3209
|
+
const header = (0, builders_1.buildX402HeaderFromAuthorization)({
|
|
3210
|
+
x402Version: Number(requirement?.x402Version || (data?.x402Version || 2)),
|
|
3211
|
+
scheme: String(requirement?.scheme || 'exact'),
|
|
3212
|
+
network: String(requirement?.network || ''),
|
|
3213
|
+
from: String(fromBase58 || ''),
|
|
3214
|
+
to: String(requirement?.payTo || ''),
|
|
3215
|
+
value: String(requirement?.maxAmountRequired || '0'),
|
|
3216
|
+
validAfter: String(fields?.validAfter || '0'),
|
|
3217
|
+
validBefore: String(fields?.validBefore || '0'),
|
|
3218
|
+
nonce: String(fields?.nonceHex || ''),
|
|
3219
|
+
signature: String(messageSigB64),
|
|
3220
|
+
});
|
|
3221
|
+
const headerJson = JSON.stringify(header);
|
|
3222
|
+
let headerB64;
|
|
3223
|
+
try {
|
|
3224
|
+
const Buf = globalThis.Buffer;
|
|
3225
|
+
headerB64 = Buf ? Buf.from(headerJson, 'utf8').toString('base64') : globalThis?.btoa?.(headerJson) || '';
|
|
3226
|
+
}
|
|
3227
|
+
catch {
|
|
3228
|
+
headerB64 = '';
|
|
3229
|
+
}
|
|
3230
|
+
if (headerB64) {
|
|
3231
|
+
try {
|
|
3232
|
+
this.emitMethodStart('notifyLedgerTransaction', { paymentIntentId });
|
|
3233
|
+
}
|
|
3234
|
+
catch { }
|
|
3235
|
+
const settleRespSol = await this.publicApiClient.post('/sdk/public/payments/x402/settle', {
|
|
3236
|
+
paymentIntentId,
|
|
3237
|
+
paymentHeader: headerB64,
|
|
3238
|
+
paymentRequirements: requirement,
|
|
3239
|
+
});
|
|
3240
|
+
try {
|
|
3241
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'x402 (sol) settle via header response', {
|
|
3242
|
+
ok: settleRespSol?.ok,
|
|
3243
|
+
status: settleRespSol?.status,
|
|
3244
|
+
paymentIntentId: settleRespSol?.paymentIntent?.id,
|
|
3245
|
+
paymentId: settleRespSol?.payment?.id,
|
|
3246
|
+
rawKeys: Object.keys(settleRespSol || {}),
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
catch { }
|
|
3250
|
+
const statusSolHdr = (settleRespSol?.status || settleRespSol?.paymentIntent?.status || 'completed').toString().toLowerCase();
|
|
3251
|
+
const amountSolHdr = (settleRespSol?.paymentIntent?.amount && String(settleRespSol.paymentIntent.amount)) ||
|
|
3252
|
+
(typeof usdAmount === 'number' ? String(usdAmount) : request?.amount?.toString?.() || '0');
|
|
3253
|
+
const outSolHdr = {
|
|
3254
|
+
transactionId: Number(settleRespSol?.canisterTxId || 0),
|
|
3255
|
+
status: statusSolHdr === 'succeeded' ? 'completed' : statusSolHdr,
|
|
3256
|
+
amount: amountSolHdr,
|
|
3257
|
+
recipientCanister: ledgerCanisterId,
|
|
3258
|
+
timestamp: new Date(),
|
|
3259
|
+
metadata: { ...(request.metadata || {}), icpay_x402: true },
|
|
3260
|
+
payment: settleRespSol || null,
|
|
3261
|
+
};
|
|
3262
|
+
const isTerminalSolHdr = (() => {
|
|
3263
|
+
const s = String(outSolHdr.status || '').toLowerCase();
|
|
3264
|
+
return s === 'completed' || s === 'succeeded' || s === 'failed' || s === 'canceled' || s === 'cancelled' || s === 'mismatched';
|
|
3265
|
+
})();
|
|
3266
|
+
if (isTerminalSolHdr) {
|
|
3267
|
+
if (outSolHdr.status === 'completed') {
|
|
3268
|
+
this.emit('icpay-sdk-transaction-completed', outSolHdr);
|
|
3269
|
+
}
|
|
3270
|
+
else if (outSolHdr.status === 'failed') {
|
|
3271
|
+
this.emit('icpay-sdk-transaction-failed', outSolHdr);
|
|
3272
|
+
}
|
|
3273
|
+
else {
|
|
3274
|
+
this.emit('icpay-sdk-transaction-updated', outSolHdr);
|
|
3275
|
+
}
|
|
3276
|
+
this.emitMethodSuccess('createPaymentX402Usd', outSolHdr);
|
|
3277
|
+
return outSolHdr;
|
|
3278
|
+
}
|
|
3279
|
+
// Non-terminal: wait until terminal via notify loop
|
|
3280
|
+
try {
|
|
3281
|
+
this.emit('icpay-sdk-transaction-updated', outSolHdr);
|
|
3282
|
+
}
|
|
3283
|
+
catch { }
|
|
3284
|
+
const waitedSolHdr = await this.awaitIntentTerminal({
|
|
3285
|
+
paymentIntentId,
|
|
3286
|
+
ledgerCanisterId: ledgerCanisterId,
|
|
3287
|
+
amount: amountSolHdr,
|
|
3288
|
+
metadata: { ...(request.metadata || {}), icpay_x402: true },
|
|
3289
|
+
});
|
|
3290
|
+
this.emitMethodSuccess('createPaymentX402Usd', waitedSolHdr);
|
|
3291
|
+
return waitedSolHdr;
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
catch { }
|
|
3297
|
+
if (!signedTxB64 && !signerSigBase58) {
|
|
3298
|
+
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.TRANSACTION_FAILED, message: 'Wallet did not return a signed transaction' });
|
|
3299
|
+
}
|
|
3300
|
+
let settleResp;
|
|
3301
|
+
if (signedTxB64 && typeof signedTxB64 === 'string') {
|
|
3302
|
+
settleResp = await this.publicApiClient.post('/sdk/public/payments/x402/relay', {
|
|
3303
|
+
paymentIntentId,
|
|
3304
|
+
signedTransactionBase64: signedTxB64,
|
|
3305
|
+
rpcUrlPublic: requirement?.extra?.rpcUrlPublic || null,
|
|
3306
|
+
});
|
|
3307
|
+
}
|
|
3308
|
+
else if (signerSigBase58) {
|
|
3309
|
+
// Relay signature + unsigned tx; server will attach and co-sign
|
|
3310
|
+
settleResp = await this.publicApiClient.post('/sdk/public/payments/x402/relay', {
|
|
3311
|
+
paymentIntentId,
|
|
3312
|
+
signatureBase58: signerSigBase58,
|
|
3313
|
+
transactionBase64: txBase64,
|
|
3314
|
+
payerPublicKey: fromBase58,
|
|
3315
|
+
rpcUrlPublic: requirement?.extra?.rpcUrlPublic || null,
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
else {
|
|
3319
|
+
throw new errors_1.IcpayError({ code: errors_1.ICPAY_ERROR_CODES.TRANSACTION_FAILED, message: 'Wallet did not return a signed transaction' });
|
|
3320
|
+
}
|
|
3321
|
+
try {
|
|
3322
|
+
(0, utils_1.debugLog)(this.config?.debug || false, 'x402 (sol) settle response (relay via services)', {
|
|
3323
|
+
ok: settleResp?.ok,
|
|
3324
|
+
status: settleResp?.status,
|
|
3325
|
+
paymentIntentId: settleResp?.paymentIntent?.id,
|
|
3326
|
+
paymentId: settleResp?.payment?.id,
|
|
3327
|
+
rawKeys: Object.keys(settleResp || {}),
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
3330
|
+
catch { }
|
|
3331
|
+
// Move to "Payment confirmation" stage (after relayer submission)
|
|
3332
|
+
try {
|
|
3333
|
+
this.emitMethodSuccess('notifyLedgerTransaction', { paymentIntentId });
|
|
3334
|
+
}
|
|
3335
|
+
catch { }
|
|
3336
|
+
const statusSol = (settleResp?.status || settleResp?.paymentIntent?.status || 'completed').toString().toLowerCase();
|
|
3337
|
+
const amountSol = (settleResp?.paymentIntent?.amount && String(settleResp.paymentIntent.amount)) ||
|
|
3338
|
+
(typeof usdAmount === 'number' ? String(usdAmount) : request?.amount?.toString?.() || '0');
|
|
3339
|
+
const outSol = {
|
|
3340
|
+
transactionId: Number(settleResp?.canisterTxId || 0),
|
|
3341
|
+
status: statusSol === 'succeeded' ? 'completed' : statusSol,
|
|
3342
|
+
amount: amountSol,
|
|
3343
|
+
recipientCanister: ledgerCanisterId,
|
|
3344
|
+
timestamp: new Date(),
|
|
3345
|
+
metadata: { ...(request.metadata || {}), icpay_x402: true },
|
|
3346
|
+
payment: settleResp || null,
|
|
3347
|
+
};
|
|
3348
|
+
// Do not fallback to normal flow for Solana x402; surface failure
|
|
3349
|
+
const isTerminalSol = (() => {
|
|
3350
|
+
const s = String(outSol.status || '').toLowerCase();
|
|
3351
|
+
return s === 'completed' || s === 'succeeded' || s === 'failed' || s === 'canceled' || s === 'cancelled' || s === 'mismatched';
|
|
3352
|
+
})();
|
|
3353
|
+
if (isTerminalSol) {
|
|
3354
|
+
if (outSol.status === 'completed') {
|
|
3355
|
+
this.emit('icpay-sdk-transaction-completed', outSol);
|
|
3356
|
+
}
|
|
3357
|
+
else if (outSol.status === 'failed') {
|
|
3358
|
+
this.emit('icpay-sdk-transaction-failed', outSol);
|
|
3359
|
+
}
|
|
3360
|
+
else {
|
|
3361
|
+
this.emit('icpay-sdk-transaction-updated', outSol);
|
|
3362
|
+
}
|
|
3363
|
+
this.emitMethodSuccess('createPaymentX402Usd', outSol);
|
|
3364
|
+
return outSol;
|
|
3365
|
+
}
|
|
3366
|
+
// Non-terminal: wait until terminal via notify loop
|
|
3367
|
+
try {
|
|
3368
|
+
this.emit('icpay-sdk-transaction-updated', outSol);
|
|
3369
|
+
}
|
|
3370
|
+
catch { }
|
|
3371
|
+
const waitedSol = await this.awaitIntentTerminal({
|
|
3372
|
+
paymentIntentId,
|
|
3373
|
+
ledgerCanisterId: ledgerCanisterId,
|
|
3374
|
+
amount: amountSol,
|
|
3375
|
+
metadata: { ...(request.metadata || {}), icpay_x402: true },
|
|
3376
|
+
});
|
|
3377
|
+
this.emitMethodSuccess('createPaymentX402Usd', waitedSol);
|
|
3378
|
+
return waitedSol;
|
|
3379
|
+
}
|
|
3380
|
+
// EVM: server-side settlement
|
|
1703
3381
|
try {
|
|
1704
3382
|
this.emitMethodStart('notifyLedgerTransaction', { paymentIntentId });
|
|
1705
3383
|
}
|
|
@@ -1744,7 +3422,6 @@ class Icpay {
|
|
|
1744
3422
|
this.emit('icpay-sdk-transaction-failed', { ...out, reason: failMsg });
|
|
1745
3423
|
}
|
|
1746
3424
|
catch { }
|
|
1747
|
-
// Initiate regular flow (non-x402) with the same request
|
|
1748
3425
|
const fallback = await this.createPaymentUsd(request);
|
|
1749
3426
|
this.emitMethodSuccess('createPaymentX402Usd', fallback);
|
|
1750
3427
|
return fallback;
|
|
@@ -1780,8 +3457,12 @@ class Icpay {
|
|
|
1780
3457
|
this.emitMethodSuccess('createPaymentX402Usd', waited);
|
|
1781
3458
|
return waited;
|
|
1782
3459
|
}
|
|
1783
|
-
catch {
|
|
1784
|
-
//
|
|
3460
|
+
catch (err) {
|
|
3461
|
+
// For Solana x402, do not silently fall back; surface the error so user can retry/sign
|
|
3462
|
+
if (isSol) {
|
|
3463
|
+
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 });
|
|
3464
|
+
}
|
|
3465
|
+
// Non-Solana: fall through to notify-based wait if settle endpoint not available
|
|
1785
3466
|
}
|
|
1786
3467
|
}
|
|
1787
3468
|
// Fallback: wait until terminal via notify loop
|