Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ Official SDK for Internet Computer payments with account-based authentication an

## Version History

### v1.3.2 (Latest)
### v1.3.37 (Latest)
- Added debug configuration option for development and troubleshooting
- All console.log statements now respect the debug setting
- Improved logging for transaction flow and API operations

### v1.3.2
- Initial release with user-based authentication

## Installation
Expand Down Expand Up @@ -283,11 +288,49 @@ interface IcpayConfig {
plugNPlayConfig?: Record<string, any>;
connectedWallet?: ConnectedWallet;
actorProvider?: (canisterId: string, idl: any) => ActorSubclass<any>;

// Debug configuration
debug?: boolean;
}
```
```

**Note:** Either `publishableKey` (public mode) or `secretKey` (private mode) must be provided.

### Debug Configuration

The SDK includes a debug mode that can be enabled to output detailed console logs for development and troubleshooting:

```typescript
const icpay = new Icpay({
publishableKey: 'pk_live_your_key_here',
debug: true // Enable debug logging
});
```

**Debug Mode Features:**
- Detailed logging of all SDK operations
- Transaction flow tracking
- API request/response logging
- Wallet connection debugging
- Balance checking logs

**Usage:**
- **Development**: Set `debug: true` to see detailed operation logs
- **Production**: Leave `debug` unset or set to `false` (default) to avoid console clutter
- **Troubleshooting**: Enable debug mode to diagnose issues with transactions or wallet connections

**Example Output:**
```
[ICPay SDK] constructor { config: {...} }
[ICPay SDK] sendFunds start { request: {...} }
[ICPay SDK] checking balance { ledgerCanisterId: "...", requiredAmount: "..." }
[ICPay SDK] balance ok { actualBalance: "..." }
[ICPay SDK] creating payment intent
[ICPay SDK] payment intent created { paymentIntentId: "...", paymentIntentCode: ... }
[ICPay SDK] sendFunds done { transactionId: ..., status: "completed" }
```

## API Reference

### Account Methods
Expand Down
37 changes: 37 additions & 0 deletions examples/debug-example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Icpay } from '../src';

async function debugExample() {
console.log('=== ICPay SDK Debug Configuration Example ===\n');

// Example 1: Debug disabled (default behavior)
console.log('1. Creating SDK with debug: false (default)');
const sdkNoDebug = new Icpay({
publishableKey: 'pk_test_example',
debug: false
});
console.log(' SDK created - no debug messages should appear above\n');

// Example 2: Debug enabled
console.log('2. Creating SDK with debug: true');
const sdkWithDebug = new Icpay({
publishableKey: 'pk_test_example',
debug: true
});
console.log(' SDK created - debug messages should appear above\n');

// Example 3: Debug not specified (should default to false)
console.log('3. Creating SDK without debug specified');
const sdkDefault = new Icpay({
publishableKey: 'pk_test_example'
});
console.log(' SDK created - no debug messages should appear above\n');

console.log('=== Debug Configuration Example Completed ===');
console.log('\nKey points:');
console.log('- When debug: false (or not set), no console.log messages are output');
console.log('- When debug: true, all SDK operations will log detailed information');
console.log('- This helps with development and troubleshooting without cluttering production logs');
}

// Run the example
debugExample().catch(console.error);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@icpay/sdk",
"version": "1.3.37",
"version": "1.3.38",
"description": "Official icpay SDK for Internet Computer payments",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
58 changes: 30 additions & 28 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { HttpAgent, Actor } from '@dfinity/agent';
import { idlFactory as icpayIdl } from './declarations/icpay_canister_backend/icpay_canister_backend.did.js';
import { idlFactory as ledgerIdl } from './declarations/icrc-ledger/ledger.did.js';
import { Principal } from '@dfinity/principal';
import { toAccountIdentifier } from './utils'; // We'll add this helper
import { toAccountIdentifier, debugLog } from './utils';

export class Icpay {
private config: IcpayConfig;
Expand All @@ -38,13 +38,15 @@ export class Icpay {
private verifiedLedgersCache: { data: VerifiedLedger[] | null; timestamp: number } = { data: null, timestamp: 0 };

constructor(config: IcpayConfig) {
console.log('[ICPay SDK] constructor', { config });
this.config = {
environment: 'production',
apiUrl: 'https://api.icpay.org',
debug: false,
...config
};

debugLog(this.config.debug || false, 'constructor', { config: this.config });

// Validate authentication configuration
if (!this.config.publishableKey && !this.config.secretKey) {
throw new Error('Either publishableKey or secretKey must be provided');
Expand All @@ -53,12 +55,12 @@ export class Icpay {
this.icHost = config.icHost || 'https://ic0.app';
this.connectedWallet = config.connectedWallet || null;
this.actorProvider = config.actorProvider;
console.log('[ICPay SDK] constructor', { connectedWallet: this.connectedWallet, actorProvider: this.actorProvider });
debugLog(this.config.debug || false, 'constructor', { connectedWallet: this.connectedWallet, actorProvider: this.actorProvider });

// Initialize wallet with connected wallet if provided
this.wallet = new IcpayWallet({ connectedWallet: this.connectedWallet });

console.log('[ICPay SDK] constructor', { connectedWallet: this.connectedWallet });
debugLog(this.config.debug || false, 'constructor', { connectedWallet: this.connectedWallet });

// Create public API client (always available)
this.publicApiClient = axios.create({
Expand All @@ -69,7 +71,7 @@ export class Icpay {
}
});

console.log('[ICPay SDK] publicApiClient', this.publicApiClient);
debugLog(this.config.debug || false, 'publicApiClient created', this.publicApiClient);

// Create private API client (only if secret key is provided)
if (this.config.secretKey) {
Expand All @@ -84,7 +86,7 @@ export class Icpay {
});
}

console.log('[ICPay SDK] privateApiClient', this.privateApiClient);
debugLog(this.config.debug || false, 'privateApiClient created', this.privateApiClient);
}

/**
Expand Down Expand Up @@ -419,14 +421,14 @@ export class Icpay {
*/
async sendFunds(request: CreateTransactionRequest): Promise<TransactionResponse> {
try {
console.log('[ICPay SDK] sendFunds start', { request });
debugLog(this.config.debug || false, 'sendFunds start', { request });
// Fetch account info to get accountCanisterId if not provided
let accountCanisterId = request.accountCanisterId;
if (!accountCanisterId) {
console.log('[ICPay SDK] fetching account info for accountCanisterId');
debugLog(this.config.debug || false, 'fetching account info for accountCanisterId');
const accountInfo = await this.getAccountInfo();
accountCanisterId = accountInfo.accountCanisterId.toString();
console.log('[ICPay SDK] accountCanisterId resolved', { accountCanisterId });
debugLog(this.config.debug || false, 'accountCanisterId resolved', { accountCanisterId });
}

// Always use icpayCanisterId as toPrincipal
Expand All @@ -444,7 +446,7 @@ export class Icpay {

// Check balance before sending
const requiredAmount = amount;
console.log('[ICPay SDK] checking balance', { ledgerCanisterId, requiredAmount: requiredAmount.toString() });
debugLog(this.config.debug || false, 'checking balance', { ledgerCanisterId, requiredAmount: requiredAmount.toString() });

// Helper function to make amounts human-readable
const formatAmount = (amount: bigint, decimals: number = 8, symbol: string = '') => {
Expand All @@ -469,7 +471,7 @@ export class Icpay {
ledgerCanisterId
});
}
console.log('[ICPay SDK] balance ok', { actualBalance: actualBalance.toString() });
debugLog(this.config.debug || false, 'balance ok', { actualBalance: actualBalance.toString() });
} catch (balanceError) {
// If we can't fetch the specific ledger balance, fall back to the old logic
throw new IcpayError({
Expand All @@ -483,15 +485,15 @@ export class Icpay {
let paymentIntentId: string | null = null;
let paymentIntentCode: number | null = null;
try {
console.log('[ICPay SDK] creating payment intent');
debugLog(this.config.debug || false, 'creating payment intent');
const intentResp = await this.publicApiClient.post('/sdk/public/payments/intents', {
amount: request.amount,
ledgerCanisterId,
metadata: request.metadata || {},
});
paymentIntentId = intentResp.data?.paymentIntent?.id || null;
paymentIntentCode = intentResp.data?.paymentIntent?.intentCode ?? null;
console.log('[ICPay SDK] payment intent created', { paymentIntentId, paymentIntentCode });
debugLog(this.config.debug || false, 'payment intent created', { paymentIntentId, paymentIntentCode });
} catch (e) {
// Do not proceed without a payment intent
// Throw a standardized error so integrators can handle it consistently
Expand All @@ -509,17 +511,17 @@ export class Icpay {
const acctIdNum = parseInt(accountCanisterId);
if (!isNaN(acctIdNum) && paymentIntentCode != null) {
memo = this.createPackedMemo(acctIdNum, Number(paymentIntentCode));
console.log('[ICPay SDK] built packed memo', { accountCanisterId: acctIdNum, paymentIntentCode });
debugLog(this.config.debug || false, 'built packed memo', { accountCanisterId: acctIdNum, paymentIntentCode });
} else if (!isNaN(acctIdNum)) {
memo = this.createMemoWithAccountCanisterId(acctIdNum);
console.log('[ICPay SDK] built legacy memo', { accountCanisterId: acctIdNum });
debugLog(this.config.debug || false, 'built legacy memo', { accountCanisterId: acctIdNum });
}
} catch {}

let transferResult;
if (ledgerCanisterId === 'ryjl3-tyaaa-aaaaa-aaaba-cai') {
// ICP Ledger: use ICRC-1 transfer (ICP ledger supports ICRC-1)
console.log('[ICPay SDK] sending ICRC-1 transfer (ICP)');
debugLog(this.config.debug || false, 'sending ICRC-1 transfer (ICP)');
transferResult = await this.sendFundsToLedger(
ledgerCanisterId,
toPrincipal,
Expand All @@ -529,7 +531,7 @@ export class Icpay {
);
} else {
// ICRC-1 ledgers: use principal directly
console.log('[ICPay SDK] sending ICRC-1 transfer');
debugLog(this.config.debug || false, 'sending ICRC-1 transfer');
transferResult = await this.sendFundsToLedger(
ledgerCanisterId,
toPrincipal,
Expand All @@ -541,13 +543,13 @@ export class Icpay {

// Assume transferResult returns a block index or transaction id
const blockIndex = transferResult?.Ok?.toString() || transferResult?.blockIndex?.toString() || `temp-${Date.now()}`;
console.log('[ICPay SDK] transfer result', { blockIndex });
debugLog(this.config.debug || false, 'transfer result', { blockIndex });

// First, notify the canister about the ledger transaction
let canisterTransactionId: number;
let notifyStatus: any = null;
try {
console.log('[ICPay SDK] notifying canister about ledger tx');
debugLog(this.config.debug || false, 'notifying canister about ledger tx');
const notifyRes: any = await this.notifyLedgerTransaction(
this.icpayCanisterId!,
ledgerCanisterId,
Expand All @@ -560,10 +562,10 @@ export class Icpay {
canisterTransactionId = parseInt(notifyRes.id, 10);
notifyStatus = notifyRes;
}
console.log('[ICPay SDK] canister notified', { canisterTransactionId });
debugLog(this.config.debug || false, 'canister notified', { canisterTransactionId });
} catch (notifyError) {
canisterTransactionId = parseInt(blockIndex, 10);
console.log('[ICPay SDK] notify failed, using blockIndex as tx id', { canisterTransactionId });
debugLog(this.config.debug || false, 'notify failed, using blockIndex as tx id', { canisterTransactionId });
}

// Poll for transaction status until completed
Expand All @@ -573,12 +575,12 @@ export class Icpay {
status = { status: notifyStatus.status };
} else {
try {
console.log('[ICPay SDK] polling transaction status (public)', { canisterTransactionId });
debugLog(this.config.debug || false, 'polling transaction status (public)', { canisterTransactionId });
status = await this.pollTransactionStatus(this.icpayCanisterId!, canisterTransactionId, accountCanisterId as string, Number(blockIndex), 2000, 30);
console.log('[ICPay SDK] poll done', { status });
debugLog(this.config.debug || false, 'poll done', { status });
} catch (e) {
status = { status: 'pending' };
console.log('[ICPay SDK] poll failed, falling back to pending');
debugLog(this.config.debug || false, 'poll failed, falling back to pending');
}
}

Expand Down Expand Up @@ -614,7 +616,7 @@ export class Icpay {
const notifyDelayMs = 1000;
for (let attempt = 1; attempt <= maxNotifyAttempts; attempt++) {
try {
console.log('[ICPay SDK] notifying API about completion', { attempt, notifyPath, paymentIntentId, canisterTransactionId });
debugLog(this.config.debug || false, 'notifying API about completion', { attempt, notifyPath, paymentIntentId, canisterTransactionId });
const resp = await notifyClient.post(notifyPath, {
paymentIntentId,
canisterTxId: canisterTransactionId,
Expand All @@ -624,7 +626,7 @@ export class Icpay {
} catch (e: any) {
const status = e?.response?.status;
const data = e?.response?.data;
console.log('[ICPay SDK] API notify attempt failed', { attempt, status, data });
debugLog(this.config.debug || false, 'API notify attempt failed', { attempt, status, data });
// Proactively trigger a transaction sync if we get not found
try {
await this.triggerTransactionSync(canisterTransactionId);
Expand All @@ -635,7 +637,7 @@ export class Icpay {
}
}
if (!publicNotify) {
console.log('[ICPay SDK] API notify failed after retries (non-fatal)');
debugLog(this.config.debug || false, 'API notify failed after retries (non-fatal)');
}
}

Expand All @@ -650,7 +652,7 @@ export class Icpay {
payment: publicNotify
};

console.log('[ICPay SDK] sendFunds done', response);
debugLog(this.config.debug || false, 'sendFunds done', response);
return response;
} catch (error) {
// eslint-disable-next-line no-console
Expand Down
6 changes: 6 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ export interface IcpayConfig {
* (canisterId: string, idl: any) => ActorSubclass<any>
*/
actorProvider?: (canisterId: string, idl: any) => ActorSubclass<any>;

/**
* Optional: Enable debug logging
* When set to true, all console.log messages will be output
*/
debug?: boolean;
}

export interface ConnectedWallet {
Expand Down
17 changes: 17 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,20 @@ export function toAccountIdentifier(principal: Principal, subaccount?: Uint8Arra
const hash = createHash('sha224').update(data).digest();
return hash;
}

/**
* Debug logger utility
* Only outputs console.log messages when debug is enabled
* @param debug - Whether debug mode is enabled
* @param message - The message to log
* @param data - Optional data to log
*/
export function debugLog(debug: boolean, message: string, data?: any): void {
if (debug) {
if (data !== undefined) {
console.log(`[ICPay SDK] ${message}`, data);
} else {
console.log(`[ICPay SDK] ${message}`);
}
}
}