diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 84068283..bb43d222 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -20,7 +20,7 @@ import { Command, program } from 'commander'; import { version } from '../version.js'; -import { cryptoFund, getBalance } from './commands.js'; +import { cryptoFund, getBalance, topUp } from './commands.js'; import { applyOptions, configFromOptions, @@ -53,14 +53,12 @@ applyOptions( applyOptions( program.command('top-up').description('Top up a Turbo address with Fiat'), - [optionMap.address, optionMap.value, optionMap.token], -).action((options) => { - console.log( - 'TODO: fiat top-up', - options.address, - options.token, - options.value, - ); + [...walletOptions, optionMap.address, optionMap.value, optionMap.currency], +).action(async (_commandOptions, command: Command) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const options: any = command.optsWithGlobals(); + + return topUp(options); }); applyOptions( diff --git a/src/cli/commands.ts b/src/cli/commands.ts index f071acea..1dc16467 100644 --- a/src/cli/commands.ts +++ b/src/cli/commands.ts @@ -14,37 +14,57 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import { exec } from 'node:child_process'; + import { TokenType, TurboFactory, TurboUnauthenticatedConfiguration, TurboWallet, + currencyMap, + fiatCurrencyTypes, + isCurrency, tokenToBaseMap, } from '../node/index.js'; -import { AddressOptions } from './types.js'; +import { sleep } from '../utils/common.js'; +import { AddressOptions, TopUpOptions } from './types.js'; import { configFromOptions, optionalPrivateKeyFromOptions } from './utils.js'; +export async function addressOrPrivateKeyFromOptions( + options: AddressOptions, +): Promise<{ + address: string | undefined; + privateKey: string | undefined; +}> { + if (options.address !== undefined) { + return { address: options.address, privateKey: undefined }; + } + + return { + address: undefined, + privateKey: await optionalPrivateKeyFromOptions(options), + }; +} + export async function getBalance(options: AddressOptions) { const config = configFromOptions(options); - if (options.address !== undefined) { + const { address, privateKey } = await addressOrPrivateKeyFromOptions(options); + + if (address !== undefined) { const turbo = TurboFactory.unauthenticated(config); - const { winc } = await turbo.getBalance(options.address); + const { winc } = await turbo.getBalance(address); console.log( - `Turbo Balance for Native Address "${options.address}"\nCredits: ${ + `Turbo Balance for Native Address "${address}"\nCredits: ${ +winc / 1_000_000_000_000 }`, ); return; } - const privateKey = await optionalPrivateKeyFromOptions(options); - if (privateKey === undefined) { - throw new Error( - 'Must provide an address (--address) or use a valid wallet', - ); + throw new Error('Must provide an (--address) or use a valid wallet'); } const turbo = TurboFactory.authenticated({ @@ -88,3 +108,96 @@ export async function cryptoFund({ JSON.stringify(result, null, 2), ); } + +export async function topUp(options: TopUpOptions) { + const config = configFromOptions(options); + + const { address, privateKey } = await addressOrPrivateKeyFromOptions(options); + + const value = options.value; + if (value === undefined) { + throw new Error('Must provide a --value to top up'); + } + + const currency = options.currency ?? 'usd'; + + if (!isCurrency(currency)) { + throw new Error( + `Invalid fiat currency type ${currency}!\nPlease use one of these:\n${JSON.stringify( + fiatCurrencyTypes, + null, + 2, + )}`, + ); + } + + // TODO: Pay in CLI prompts via --cli options + + // if (address !== undefined) { + // const turbo = TurboFactory.unauthenticated(config); + // const { url, paymentAmount, winc } = await turbo.createCheckoutSession({ + // amount: currencyMap[currency](+options.value), + // owner: address, + // }); + // } + + // if (privateKey === undefined) { + // throw new Error('Must provide a wallet to top up'); + // } + + // const turbo = TurboFactory.authenticated({ + // ...config, + // privateKey, + // }); + + // const { url, paymentAmount, winc } = await turbo.createCheckoutSession({ + // amount: currencyMap[currency](+options.value), + // owner: await turbo.signer.getNativeAddress(), + // }); + + const { url, paymentAmount, winc } = await (async () => { + const amount = currencyMap[currency](+value); + + if (address !== undefined) { + const turbo = TurboFactory.unauthenticated(config); + return turbo.createCheckoutSession({ + amount, + owner: address, + }); + } + + if (privateKey === undefined) { + throw new Error('Must provide a wallet to top up'); + } + + const turbo = TurboFactory.authenticated({ + ...config, + privateKey, + }); + return turbo.createCheckoutSession({ + amount, + owner: await turbo.signer.getNativeAddress(), + }); + })(); + + console.log( + 'Got Checkout Session\n' + JSON.stringify({ url, paymentAmount, winc }), + ); + console.log('Opening checkout session in browser...'); + await sleep(2000); + + openUrl(url); +} + +export function openUrl(url: string) { + if (process.platform === 'darwin') { + // macOS + exec(`open ${url}`); + } else if (process.platform === 'win32') { + // Windows + exec(`start "" "${url}"`, { windowsHide: true }); + } else { + // Linux/Unix + open(url); + } +} diff --git a/src/cli/types.ts b/src/cli/types.ts index 064264ab..eef90a65 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -33,3 +33,8 @@ export type WalletOptions = GlobalOptions & { export type AddressOptions = WalletOptions & { address: string | undefined; }; + +export type TopUpOptions = AddressOptions & { + value: string | undefined; + currency: string | undefined; +}; diff --git a/src/common/currency.ts b/src/common/currency.ts index dc8b1cd7..d4b128ea 100644 --- a/src/common/currency.ts +++ b/src/common/currency.ts @@ -52,3 +52,16 @@ export const BRL = (brl: number) => new TwoDecimalCurrency(brl, 'brl'); // Zero decimal currencies that are supported by the Turbo API export const JPY = (jpy: number) => new ZeroDecimalCurrency(jpy, 'jpy'); + +export const currencyMap: Record CurrencyMap> = { + usd: USD, + eur: EUR, + gbp: GBP, + cad: CAD, + aud: AUD, + inr: INR, + sgd: SGD, + hkd: HKD, + brl: BRL, + jpy: JPY, +}; diff --git a/src/common/payment.ts b/src/common/payment.ts index 9aa66000..db7a9315 100644 --- a/src/common/payment.ts +++ b/src/common/payment.ts @@ -21,10 +21,10 @@ import { Currency, TokenTools, TokenType, - TopUpRawResponse, TurboAuthenticatedPaymentServiceConfiguration, TurboAuthenticatedPaymentServiceInterface, TurboBalanceResponse, + TurboCheckoutRawResponse, TurboCheckoutSessionParams, TurboCheckoutSessionResponse, TurboCountriesResponse, @@ -157,7 +157,7 @@ export class TurboUnauthenticatedPaymentService }&token=${this.token}`; const { adjustments, paymentSession, topUpQuote } = - await this.httpService.get({ + await this.httpService.get({ endpoint, headers, }); @@ -165,9 +165,8 @@ export class TurboUnauthenticatedPaymentService return { winc: topUpQuote.winstonCreditAmount, adjustments, - url: paymentSession.url ?? undefined, + url: paymentSession.url, id: paymentSession.id, - client_secret: paymentSession.client_secret ?? undefined, paymentAmount: topUpQuote.paymentAmount, quotedPaymentAmount: topUpQuote.quotedPaymentAmount, }; diff --git a/src/types.ts b/src/types.ts index f5f1515d..001afa64 100644 --- a/src/types.ts +++ b/src/types.ts @@ -35,17 +35,24 @@ export type NativeAddress = string; export type PublicArweaveAddress = Base64String; export type TransactionId = Base64String; export type UserAddress = string | PublicArweaveAddress; -export type Currency = - | 'usd' - | 'eur' - | 'gbp' - | 'cad' - | 'aud' - | 'jpy' - | 'inr' - | 'sgd' - | 'hkd' - | 'brl'; + +export const fiatCurrencyTypes = [ + 'usd', + 'eur', + 'gbp', + 'cad', + 'aud', + 'jpy', + 'inr', + 'sgd', + 'hkd', + 'brl', +] as const; +export type Currency = (typeof fiatCurrencyTypes)[number]; +export function isCurrency(currency: string): currency is Currency { + return fiatCurrencyTypes.includes(currency as Currency); +} + export type Country = 'United States' | 'United Kingdom' | 'Canada'; // TODO: add full list export const tokenTypes = ['arweave', 'solana', 'ethereum', 'kyve'] as const; @@ -94,17 +101,26 @@ export type TopUpRawResponse = { winstonCreditAmount: string; }; paymentSession: { - url: string | null; id: string; - client_secret: string | null; }; adjustments: Adjustment[]; }; +export type TurboPaymentIntentRawResponse = TopUpRawResponse & { + paymentSession: { + client_secret: string; + }; +}; + +export type TurboCheckoutRawResponse = TopUpRawResponse & { + paymentSession: { + url: string; + }; +}; + export type TurboCheckoutSessionResponse = TurboWincForFiatResponse & { id: string; - client_secret?: string; - url?: string; + url: string; }; export type TurboBalanceResponse = Omit;