Skip to content

Commit 464f7db

Browse files
committed
feat: Add fiat amount support and escrow management features
### SDK Changes: - Add convertFiatToCrypto method to CoinPayClient for real-time fiat conversion - Update createEscrow to accept amountFiat and fiatCurrency parameters - Add authenticateEscrow function for token-based escrow authentication - Expand FiatCurrency enum to include JPY, CHF, CNY, INR, BRL ### CLI Changes: - Add --amount-fiat and --fiat flags to 'coinpay escrow create' - Add --amount-fiat and --fiat flags to 'coinpay wallet send' - Add 'coinpay escrow auth' command for escrow management - Show fiat-to-crypto conversion details during transactions - Display both release and beneficiary tokens in escrow create output - Add manage URL hints to help users with escrow management ### Features: - Real-time fiat conversion using rates API - Enhanced escrow creation with fiat amounts (e.g., --amount-fiat 50 --fiat USD) - Token-based authentication showing user role and available actions - Improved user experience with conversion feedback ### Tests: - Comprehensive test coverage for new SDK functions - CLI integration tests for fiat conversion workflows - Edge case handling for invalid parameters All tests passing ✅
1 parent 08e7e9f commit 464f7db

File tree

8 files changed

+668
-13
lines changed

8 files changed

+668
-13
lines changed

packages/sdk/bin/coinpay.js

Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ ${colors.cyan}Commands:${colors.reset}
216216
refund <id> Refund funds to depositor
217217
dispute <id> Open a dispute
218218
events <id> Get escrow audit log
219+
auth <id> Authenticate with escrow token
219220
220221
${colors.bright}webhook${colors.reset}
221222
logs <business-id> Get webhook logs
@@ -226,11 +227,22 @@ ${colors.cyan}Wallet Options:${colors.reset}
226227
--chains <BTC,ETH,...> Chains to derive (default: BTC,ETH,SOL,POL,BCH)
227228
--chain <chain> Single chain for operations
228229
--to <address> Recipient address
229-
--amount <amount> Amount to send
230+
--amount <amount> Amount to send (crypto)
231+
--amount-fiat <amount> Amount to send (fiat, requires --fiat)
232+
--fiat <currency> Fiat currency (USD, EUR, GBP, CAD, AUD, JPY, CHF, CNY, INR, BRL)
230233
--password <pass> Wallet encryption password
231234
--wallet-file <path> Custom wallet file (default: ~/.coinpay-wallet.gpg)
232235
--no-save Don't save wallet locally after create/import
233236
237+
${colors.cyan}Escrow Options:${colors.reset}
238+
--chain <chain> Blockchain (BTC, ETH, SOL, POL, BCH, etc.)
239+
--amount <amount> Crypto amount to escrow
240+
--amount-fiat <amount> Fiat amount to escrow (alternative to --amount)
241+
--fiat <currency> Fiat currency (required with --amount-fiat)
242+
--depositor <address> Depositor wallet address
243+
--beneficiary <address> Beneficiary wallet address
244+
--token <token> Release or beneficiary token (for auth/release/refund)
245+
234246
${colors.cyan}Swap Options:${colors.reset}
235247
--from <coin> Source coin (e.g., BTC)
236248
--to <coin> Destination coin (e.g., ETH)
@@ -249,6 +261,18 @@ ${colors.cyan}Examples:${colors.reset}
249261
250262
# Send transaction (auto-decrypts for signing)
251263
coinpay wallet send --chain ETH --to 0x123... --amount 0.1
264+
265+
# Send transaction with fiat amount
266+
coinpay wallet send --chain SOL --to abc123... --amount-fiat 10 --fiat USD
267+
268+
# Create escrow with crypto amount
269+
coinpay escrow create --chain SOL --amount 0.5 --depositor abc... --beneficiary def...
270+
271+
# Create escrow with fiat amount
272+
coinpay escrow create --chain SOL --amount-fiat 50 --fiat USD --depositor abc... --beneficiary def...
273+
274+
# Authenticate with escrow token
275+
coinpay escrow auth escr_123 --token rel_abc456
252276
253277
# Swap BTC to ETH
254278
coinpay swap quote --from BTC --to ETH --amount 0.1
@@ -1046,11 +1070,19 @@ async function handleWallet(subcommand, args, flags) {
10461070
const chain = (flags.chain || '').toUpperCase();
10471071
const to = flags.to;
10481072
const amount = flags.amount;
1073+
const amountFiat = flags['amount-fiat'] ? parseFloat(flags['amount-fiat']) : undefined;
1074+
const fiatCurrency = flags.fiat;
10491075
const priority = flags.priority || 'medium';
10501076

1051-
if (!chain || !to || !amount) {
1052-
print.error('Required: --chain, --to, --amount');
1077+
if (!chain || !to || (!amount && !amountFiat)) {
1078+
print.error('Required: --chain, --to, --amount (or --amount-fiat --fiat)');
10531079
print.info('Usage: coinpay wallet send --chain ETH --to 0x123... --amount 0.1');
1080+
print.info(' or: coinpay wallet send --chain SOL --to abc123... --amount-fiat 10 --fiat USD');
1081+
return;
1082+
}
1083+
1084+
if (amountFiat && !fiatCurrency) {
1085+
print.error('--fiat is required when using --amount-fiat');
10541086
return;
10551087
}
10561088

@@ -1065,12 +1097,23 @@ async function handleWallet(subcommand, args, flags) {
10651097
}
10661098

10671099
try {
1100+
let finalAmount = amount;
1101+
1102+
// Convert fiat to crypto if needed
1103+
if (amountFiat && fiatCurrency) {
1104+
print.info(`Converting ${fiatCurrency} ${amountFiat.toFixed(2)} to ${chain}...`);
1105+
const apiClient = createClient();
1106+
const conversion = await apiClient.convertFiatToCrypto(amountFiat, fiatCurrency, chain);
1107+
finalAmount = conversion.cryptoAmount.toString();
1108+
print.success(`Converting ${fiatCurrency} ${amountFiat.toFixed(2)}${conversion.cryptoAmount.toFixed(6)} ${chain} (rate: 1 ${chain} = ${fiatCurrency} ${conversion.rate.toFixed(2)})`);
1109+
}
1110+
10681111
const mnemonic = await getDecryptedMnemonic(flags);
10691112
const wallet = await WalletClient.fromSeed(mnemonic, { baseUrl });
10701113

1071-
print.info(`Sending ${amount} ${chain} to ${to}...`);
1114+
print.info(`Sending ${finalAmount} ${chain} to ${to}...`);
10721115

1073-
const result = await wallet.send({ chain, to, amount, priority });
1116+
const result = await wallet.send({ chain, to, amount: finalAmount, priority });
10741117

10751118
print.success('Transaction sent!');
10761119
if (result.tx_hash) {
@@ -1369,20 +1412,36 @@ async function handleEscrow(subcommand, args, flags) {
13691412
switch (subcommand) {
13701413
case 'create': {
13711414
const chain = flags.chain || flags.blockchain;
1372-
const amount = parseFloat(flags.amount);
1415+
const amount = flags.amount ? parseFloat(flags.amount) : undefined;
1416+
const amountFiat = flags['amount-fiat'] ? parseFloat(flags['amount-fiat']) : undefined;
1417+
const fiatCurrency = flags.fiat;
13731418
const depositor = flags.depositor || flags['depositor-address'];
13741419
const beneficiary = flags.beneficiary || flags['beneficiary-address'];
13751420

1376-
if (!chain || !amount || !depositor || !beneficiary) {
1377-
print.error('Required: --chain, --amount, --depositor, --beneficiary');
1421+
if (!chain || (!amount && !amountFiat) || !depositor || !beneficiary) {
1422+
print.error('Required: --chain, --amount (or --amount-fiat --fiat), --depositor, --beneficiary');
13781423
process.exit(1);
13791424
}
13801425

1381-
print.info(`Creating escrow: ${amount} ${chain}`);
1426+
if (amountFiat && !fiatCurrency) {
1427+
print.error('--fiat is required when using --amount-fiat');
1428+
process.exit(1);
1429+
}
1430+
1431+
// Show conversion if using fiat
1432+
let finalAmount = amount;
1433+
if (amountFiat && fiatCurrency) {
1434+
print.info(`Converting ${fiatCurrency} ${amountFiat.toFixed(2)} to ${chain}...`);
1435+
const conversion = await client.convertFiatToCrypto(amountFiat, fiatCurrency, chain);
1436+
finalAmount = conversion.cryptoAmount;
1437+
print.success(`Converting ${fiatCurrency} ${amountFiat.toFixed(2)}${finalAmount.toFixed(6)} ${chain} (rate: 1 ${chain} = ${fiatCurrency} ${conversion.rate.toFixed(2)})`);
1438+
}
1439+
1440+
print.info(`Creating escrow: ${finalAmount} ${chain}`);
13821441

13831442
const escrow = await client.createEscrow({
13841443
chain,
1385-
amount,
1444+
amount: finalAmount,
13861445
depositorAddress: depositor,
13871446
beneficiaryAddress: beneficiary,
13881447
metadata: flags.metadata ? JSON.parse(flags.metadata) : undefined,
@@ -1393,7 +1452,9 @@ async function handleEscrow(subcommand, args, flags) {
13931452
print.info(` Deposit to: ${escrow.escrowAddress}`);
13941453
print.info(` Status: ${escrow.status}`);
13951454
print.warn(` Release Token: ${escrow.releaseToken}`);
1455+
print.warn(` Beneficiary Token: ${escrow.beneficiaryToken}`);
13961456
print.warn(' ⚠️ Save these tokens!');
1457+
print.info(` Manage: coinpay escrow auth ${escrow.id} --token <token>`);
13971458

13981459
if (flags.json) print.json(escrow);
13991460
break;
@@ -1476,9 +1537,42 @@ async function handleEscrow(subcommand, args, flags) {
14761537
break;
14771538
}
14781539

1540+
case 'auth': {
1541+
const id = args[0];
1542+
const token = flags.token;
1543+
if (!id || !token) {
1544+
print.error('Required: <id> --token <token>');
1545+
process.exit(1);
1546+
}
1547+
1548+
const auth = await client.authenticateEscrow(id, token);
1549+
print.success(`Authenticated as: ${auth.role}`);
1550+
print.info(`Escrow Details:`);
1551+
print.info(` ID: ${auth.escrow.id}`);
1552+
print.info(` Status: ${auth.escrow.status}`);
1553+
print.info(` Chain: ${auth.escrow.chain}`);
1554+
print.info(` Amount: ${auth.escrow.amount}`);
1555+
print.info(` Depositor: ${auth.escrow.depositorAddress}`);
1556+
print.info(` Beneficiary: ${auth.escrow.beneficiaryAddress}`);
1557+
1558+
// Show available actions based on role and status
1559+
if (auth.escrow.status === 'funded') {
1560+
if (auth.role === 'depositor') {
1561+
print.info(`Available actions: release, refund, dispute`);
1562+
} else if (auth.role === 'beneficiary') {
1563+
print.info(`Available actions: dispute`);
1564+
}
1565+
} else if (auth.escrow.status === 'pending') {
1566+
print.info(`Waiting for deposit to: ${auth.escrow.escrowAddress}`);
1567+
}
1568+
1569+
if (flags.json) print.json(auth);
1570+
break;
1571+
}
1572+
14791573
default:
14801574
print.error(`Unknown escrow command: ${subcommand}`);
1481-
print.info('Available: create, get, list, release, refund, dispute, events');
1575+
print.info('Available: create, get, list, release, refund, dispute, events, auth');
14821576
process.exit(1);
14831577
}
14841578
}

packages/sdk/src/client.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,34 @@ export class CoinPayClient {
306306
return this.request(`/rates?crypto=${cryptocurrency}&fiat=${fiatCurrency}`);
307307
}
308308

309+
/**
310+
* Convert fiat amount to cryptocurrency amount
311+
* @param {number} fiatAmount - Fiat amount to convert
312+
* @param {string} fiatCurrency - Fiat currency code (USD, EUR, etc.)
313+
* @param {string} cryptoCurrency - Cryptocurrency code (BTC, ETH, SOL, etc.)
314+
* @returns {Promise<Object>} { cryptoAmount, rate, fiat, crypto }
315+
*
316+
* @example
317+
* const result = await client.convertFiatToCrypto(50, 'USD', 'SOL');
318+
* console.log(`$50 USD = ${result.cryptoAmount} SOL (rate: 1 SOL = $${result.rate})`);
319+
*/
320+
async convertFiatToCrypto(fiatAmount, fiatCurrency, cryptoCurrency) {
321+
const rateData = await this.request(`/rates?coin=${cryptoCurrency}&fiat=${fiatCurrency}`);
322+
323+
if (!rateData.success || !rateData.rate) {
324+
throw new Error(`Failed to get exchange rate for ${cryptoCurrency}/${fiatCurrency}`);
325+
}
326+
327+
const cryptoAmount = fiatAmount / rateData.rate;
328+
329+
return {
330+
cryptoAmount: cryptoAmount,
331+
rate: rateData.rate,
332+
fiat: fiatCurrency,
333+
crypto: cryptoCurrency
334+
};
335+
}
336+
309337
/**
310338
* Get multiple exchange rates
311339
* @param {string[]} cryptocurrencies - Array of cryptocurrency codes
@@ -483,6 +511,17 @@ export class CoinPayClient {
483511
const { waitForEscrow } = await import('./escrow.js');
484512
return waitForEscrow(this, escrowId, options);
485513
}
514+
515+
/**
516+
* Authenticate with escrow using token
517+
* @param {string} escrowId - Escrow ID
518+
* @param {string} token - Release token or beneficiary token
519+
* @returns {Promise<Object>} { escrow, role }
520+
*/
521+
async authenticateEscrow(escrowId, token) {
522+
const { authenticateEscrow } = await import('./escrow.js');
523+
return authenticateEscrow(this, escrowId, token);
524+
}
486525
}
487526

488527
export default CoinPayClient;

packages/sdk/src/escrow.js

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
* @param {CoinPayClient} client - API client instance
3232
* @param {Object} params - Escrow parameters
3333
* @param {string} params.chain - Blockchain (BTC, ETH, SOL, POL, etc.)
34-
* @param {number} params.amount - Crypto amount to escrow
34+
* @param {number} [params.amount] - Crypto amount to escrow
35+
* @param {number} [params.amountFiat] - Fiat amount to escrow (alternative to amount)
36+
* @param {string} [params.fiatCurrency] - Fiat currency (required if amountFiat is used)
3537
* @param {string} params.depositorAddress - Wallet address for refunds
3638
* @param {string} params.beneficiaryAddress - Wallet address for releases
3739
* @param {string} [params.arbiterAddress] - Optional dispute resolver address
@@ -42,15 +44,33 @@
4244
export async function createEscrow(client, {
4345
chain,
4446
amount,
47+
amountFiat,
48+
fiatCurrency,
4549
depositorAddress,
4650
beneficiaryAddress,
4751
arbiterAddress,
4852
metadata,
4953
expiresInHours,
5054
}) {
55+
let finalAmount = amount;
56+
57+
// If fiat amount is provided, convert to crypto first
58+
if (amountFiat && fiatCurrency) {
59+
if (amount) {
60+
throw new Error('Cannot specify both amount and amountFiat. Use one or the other.');
61+
}
62+
63+
const conversion = await client.convertFiatToCrypto(amountFiat, fiatCurrency, chain);
64+
finalAmount = conversion.cryptoAmount;
65+
} else if (amountFiat && !fiatCurrency) {
66+
throw new Error('fiatCurrency is required when amountFiat is specified');
67+
} else if (!amount && !amountFiat) {
68+
throw new Error('Either amount or amountFiat must be specified');
69+
}
70+
5171
const body = {
5272
chain,
53-
amount,
73+
amount: finalAmount,
5474
depositor_address: depositorAddress,
5575
beneficiary_address: beneficiaryAddress,
5676
};
@@ -165,6 +185,30 @@ export async function disputeEscrow(client, escrowId, token, reason) {
165185
return normalizeEscrow(data);
166186
}
167187

188+
/**
189+
* Authenticate with escrow using token
190+
* @param {CoinPayClient} client
191+
* @param {string} escrowId - Escrow ID
192+
* @param {string} token - Release token or beneficiary token
193+
* @returns {Promise<Object>} { escrow, role } where role is 'depositor' or 'beneficiary'
194+
*
195+
* @example
196+
* const auth = await client.authenticateEscrow('escrow_123', 'rel_abc123');
197+
* console.log(`Authenticated as: ${auth.role}`);
198+
* console.log(`Escrow status: ${auth.escrow.status}`);
199+
*/
200+
export async function authenticateEscrow(client, escrowId, token) {
201+
const data = await client.request(`/escrow/${escrowId}/auth`, {
202+
method: 'POST',
203+
body: JSON.stringify({ token }),
204+
});
205+
206+
return {
207+
escrow: normalizeEscrow(data.escrow),
208+
role: data.role,
209+
};
210+
}
211+
168212
/**
169213
* Get escrow event log
170214
* @param {CoinPayClient} client

packages/sdk/src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
disputeEscrow,
6464
getEscrowEvents,
6565
waitForEscrow,
66+
authenticateEscrow,
6667
} from './escrow.js';
6768

6869
// Wallet exports
@@ -106,6 +107,7 @@ export {
106107
disputeEscrow,
107108
getEscrowEvents,
108109
waitForEscrow,
110+
authenticateEscrow,
109111

110112
// Wallet
111113
WalletClient,

packages/sdk/src/payments.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ export const FiatCurrency = {
169169
GBP: 'GBP',
170170
CAD: 'CAD',
171171
AUD: 'AUD',
172+
JPY: 'JPY',
173+
CHF: 'CHF',
174+
CNY: 'CNY',
175+
INR: 'INR',
176+
BRL: 'BRL',
172177
};
173178

174179
export default {

0 commit comments

Comments
 (0)