Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(top-up): init top-up with stripe checkout command PE-6635 #149

Merged
merged 7 commits into from
Sep 6, 2024
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
41 changes: 30 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ Welcome to the `@ardrive/turbo-sdk`! This SDK provides functionality for interac
- [CLI Usage](#cli-usage)
- [Options](#options)
- [Commands](#commands)
- [`crypto-fund`](#crypto-fund)
- [`balance`](#balance)
- [`top-up`](#top-up)
- [`crypto-fund`](#crypto-fund)
- [Developers](#developers)
- [Requirements](#requirements)
- [Setup & Build](#setup--build)
Expand Down Expand Up @@ -456,7 +457,7 @@ const { winc: balance } = await turbo.getBalance();

#### `signer.getNativeAddress()`

Returns the native address of the connected signer.
Returns the [native address][docs/native-address] of the connected signer.

```typescript
const address = await turbo.signer.getNativeAddress();
Expand Down Expand Up @@ -687,36 +688,53 @@ npx turbo --help

#### Commands

##### `crypto-fund`
##### `balance`

Fund a wallet with Turbo Credits by submitting a payment transaction for the crypto amount to the Turbo wallet and then submitting that transaction id to Turbo Payment Service for top up processing.
Get the balance of a connected wallet or native address in Turbo Credits.

Command Options:

- `-v, --value <value>` - Amount of tokens in the token type's smallest unit value to fund the wallet with
- `-a, --address <nativeAddress>` - Native address to get the balance of

e.g:

```shell
turbo crypto-fund --value 0.0001 --token kyve --private-key 'b27...45c'
turbo balance --address 'crypto-wallet-public-native-address' --token solana
```

##### `balance`
```shell
turbo balance --wallet-file '../path/to/my/wallet.json' --token arweave
```

##### `top-up`

Get the balance of a wallet or native address in Turbo Credits.
Top up a connected wallet or native address with Turbo Credits using a supported fiat currency. This command will create a Stripe checkout session for the top-up amount and open the URL in the default browser.

Command Options:

- `-a, --address <address>` - Address to get the balance of
- `-a, --address <nativeAddress>` - Native address to top up
- `-c, --currency <currency>` - Currency to top up with
- `-v, --value <value>` - Value of fiat currency for top up. e.g: 10.50 for $10.50 USD

e.g:

```shell
turbo balance --address 'crypto-wallet-public-native-address'
# Open Stripe hosted checkout session in browser to top up for 10.00 USD worth of Turbo Credits
turbo top-up --address 'crypto-wallet-public-native-address' --token ethereum --currency USD --value 10
```

##### `crypto-fund`

Fund a wallet with Turbo Credits by submitting a payment transaction for the crypto amount to the Turbo wallet and then submitting that transaction id to Turbo Payment Service for top up processing.

Command Options:

- `-v, --value <value>` - Value of crypto token for fund. e.g: 0.0001 for 0.0001 KYVE

e.g:

```shell
turbo balance --wallet-file '../path/to/my/wallet.json'
turbo crypto-fund --value 0.0001 --token kyve --private-key 'b27...45c'
```

## Developers
Expand Down Expand Up @@ -763,3 +781,4 @@ For more information on how to contribute, please see [CONTRIBUTING.md].
[TurboAuthenticatedClient]: #turboauthenticatedclient
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
[CONTRIBUTING.md]: ./CONTRIBUTING.md
[docs/native-address]: https://docs.ar.io/glossary.html#native-address
41 changes: 29 additions & 12 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
import { Command, program } from 'commander';

import { version } from '../version.js';
import { cryptoFund, getBalance } from './commands.js';
import { cryptoFund, getBalance, topUp } from './commands.js';
import { TopUpOptions } from './types.js';
import {
applyOptions,
configFromOptions,
Expand All @@ -41,26 +42,37 @@ applyOptions(
globalOptions,
);

function exitWithErrorLog(error: unknown) {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}

applyOptions(
program.command('balance').description('Get balance of a Turbo address'),
[optionMap.address, optionMap.token, ...walletOptions],
[optionMap.address, ...walletOptions],
).action(async (_commandOptions, command: Command) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options: any = command.optsWithGlobals();

return getBalance(options);
try {
await getBalance(options);
process.exit(0);
} catch (error) {
exitWithErrorLog(error);
}
});

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) => {
const options = command.optsWithGlobals<TopUpOptions>();
try {
await topUp(options);
process.exit(0);
} catch (error) {
exitWithErrorLog(error);
}
});

applyOptions(
Expand All @@ -77,7 +89,12 @@ applyOptions(

const config = configFromOptions(options);

cryptoFund({ privateKey, value, token, config });
try {
await cryptoFund({ privateKey, value, token, config });
process.exit(0);
} catch (error) {
exitWithErrorLog(error);
}
});

applyOptions(
Expand Down
113 changes: 104 additions & 9 deletions src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,57 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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({
Expand Down Expand Up @@ -88,3 +108,78 @@ 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').toLowerCase();

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

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(),
});
})();

if (url === undefined) {
throw new Error('Failed to create checkout session');
}

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);
}
}
5 changes: 5 additions & 0 deletions src/cli/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
11 changes: 6 additions & 5 deletions src/cli/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,22 @@ interface CommanderOption {
export const optionMap = {
token: {
alias: '-t, --token <type>',
description: 'Token type for wallet or action',
description: 'Crypto token type for wallet or action',
default: 'arweave',
},
currency: {
alias: '-c, --currency <currency>',
description: 'Currency type to top up with',
description: 'Fiat currency type to use for the action',
default: 'usd',
},
address: {
alias: '-a, --address <walletAddress>',
description: 'Wallet address to use for action',
alias: '-a, --address <nativeAddress>',
description: 'Native address to use for action',
},
value: {
alias: '-v, --value <value>',
description: 'Value of fiat currency or crypto token for action',
description:
'Value of fiat currency or crypto token for action. e.g: 10.50 for $10.50 USD or 0.0001 for 0.0001 AR',
},
walletFile: {
alias: '-w, --wallet-file <filePath>',
Expand Down
13 changes: 13 additions & 0 deletions src/common/currency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Currency, (amount: number) => CurrencyMap> = {
usd: USD,
eur: EUR,
gbp: GBP,
cad: CAD,
aud: AUD,
inr: INR,
sgd: SGD,
hkd: HKD,
brl: BRL,
jpy: JPY,
};
29 changes: 18 additions & 11 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,24 @@
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);
}

Check warning on line 54 in src/types.ts

View check run for this annotation

Codecov / codecov/patch

src/types.ts#L53-L54

Added lines #L53 - L54 were not covered by tests

export type Country = 'United States' | 'United Kingdom' | 'Canada'; // TODO: add full list

export const tokenTypes = ['arweave', 'solana', 'ethereum', 'kyve'] as const;
Expand Down
Loading