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

payment intent top up PoC #336

Draft
wants to merge 13 commits into
base: dev
Choose a base branch
from
Draft
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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"commander": "^8.2.0",
"ipfs-only-hash": "^4.0.0",
"lodash": "^4.17.21",
"prompts": "^2.4.0"
"prompts": "^2.4.0",
"stripe": "^12.2.0"
},
"engines": {
"node": ">=18"
Expand Down Expand Up @@ -51,7 +52,7 @@
"typescript": "^5.1.6"
},
"scripts": {
"clean": "rimraf [ lib .nyc_output node_modules coverage ]",
"clean": "rimraf [ lib .nyc_output coverage ]",
"format": "prettier --write \"src/**/*.ts\"",
"lint": "eslint . --ext .ts",
"lintfix": "eslint . --ext .ts --fix",
Expand Down
28 changes: 25 additions & 3 deletions src/commands/get_balance.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { AR } from 'ardrive-core-js';
import { AR, JWKWallet } from 'ardrive-core-js';
import axios from 'axios';
import { cliWalletDAOFactory } from '..';
import { CLICommand, ParametersHelper } from '../CLICommand';
import { CLIAction } from '../CLICommand/action';
import { SUCCESS_EXIT_CODE } from '../CLICommand/error_codes';
import { AddressParameter, GatewayParameter, WalletTypeParameters } from '../parameter_declarations';
import { toB64Url } from '../utils/base64';
import { getArweaveFromURL } from '../utils/get_arweave_for_url';
import { jwkInterfaceToPublicKey, jwkInterfaceToPrivateKey, signData, publicKeyToHeader } from '../utils/signData';

new CLICommand({
name: 'get-balance',
Expand All @@ -16,8 +19,27 @@ new CLICommand({
const walletDAO = cliWalletDAOFactory(arweave);
const balanceInWinston = await walletDAO.getAddressWinstonBalance(address);
const balanceInAR = new AR(balanceInWinston);
console.log(`${balanceInWinston}\tWinston`);
console.log(`${balanceInAR}\tAR`);
console.log(`Winston:\t${balanceInWinston}`);
console.log(`AR:\t\t${balanceInAR}`);

const wallet = (await parameters.getOptionalWallet()) as JWKWallet;

if (wallet) {
const nonce = '123';
const publicKey = jwkInterfaceToPublicKey(wallet.getPrivateKey());
const privateKey = jwkInterfaceToPrivateKey(wallet.getPrivateKey());
const signature = await signData(privateKey, nonce);

const { data } = await axios.get(`https://payment.ardrive.dev/v1/balance`, {
headers: {
'x-public-key': publicKeyToHeader(publicKey),
'x-nonce': nonce,
'x-signature': toB64Url(Buffer.from(signature))
}
});
console.log(`Turbo Credits:\t${+data / 1_000_000_000_000}`);
}

return SUCCESS_EXIT_CODE;
})
});
1 change: 1 addition & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import './rename_folder';
import './retry_tx';
import './send_ar';
import './send_tx';
import './top_up';
import './tx_status';
import './upload_file';

Expand Down
124 changes: 124 additions & 0 deletions src/commands/top_up.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import axios from 'axios';
import { CLICommand, ParametersHelper } from '../CLICommand';
import { CLIAction } from '../CLICommand/action';
import { ERROR_EXIT_CODE, SUCCESS_EXIT_CODE } from '../CLICommand/error_codes';
import {
CurrencyTypeParameter,
DestinationAddressParameter,
PayInCliParameter,
PaymentAmountParameter
} from '../parameter_declarations';
import { Stripe } from 'stripe';
import prompts from 'prompts';
import { exec } from 'child_process';

// ArDrive Stripe Test PUBLISHABLE Key. Enable this one to test workflows
const stripeTestPublishableKey =
// /* cspell:disable */ 'pk_test_51JUAtwC8apPOWkDLh2FPZkQkiKZEkTo6wqgLCtQoClL6S4l2jlbbc5MgOdwOUdU9Tn93NNvqAGbu115lkJChMikG00XUfTmo2z'; /* cspell:enable */

// ArDrive Stripe Production PUBLISHABLE Key. This one is safe to have on a front end application 👍
// const stripeProdPublishableKey =
/* cspell:disable */ 'pk_live_51JUAtwC8apPOWkDLMQqNF9sPpfneNSPnwX8YZ8y1FNDl6v94hZIwzgFSYl27bWE4Oos8CLquunUswKrKcaDhDO6m002Yj9AeKj'; /* cspell:enable */

const stripe = new Stripe(stripeTestPublishableKey, { apiVersion: '2022-11-15' });

new CLICommand({
name: 'top-up',
parameters: [PaymentAmountParameter, CurrencyTypeParameter, PayInCliParameter, DestinationAddressParameter],
action: new CLIAction(async function action(options) {
const parameters = new ParametersHelper(options);

const paymentAmount = parameters.getRequiredParameterValue(PaymentAmountParameter);
const currencyType = parameters.getRequiredParameterValue(CurrencyTypeParameter);
const destinationAddress = parameters.getRequiredParameterValue<string>(DestinationAddressParameter);
const payInCli = parameters.getParameterValue<boolean>(PayInCliParameter);

const method = payInCli ? 'payment-intent' : 'checkout-session';

const paymentServiceUrl = 'https://payment.ardrive.io';
// const paymentServiceUrl = 'https://payment.ardrive.dev';
// const paymentServiceUrl = 'http://localhost:3001';

const { data } = await axios.get<{
paymentSession: { client_secret: string; id: string; url: string };
topUpQuote: { winstonCreditAmount: string };
}>(`${paymentServiceUrl}/v1/top-up/${method}/${destinationAddress}/${currencyType}/${paymentAmount}`);

const { client_secret, id, url } = data.paymentSession;

console.error(`${method} from payment service has been successfully received!`);

if (payInCli) {
const { number } = await prompts<string>({
type: 'text',
name: 'number',
message: 'Enter your card number'
});

const { expiration } = await prompts({
type: 'text',
name: 'expiration',
message: 'Enter your card expiration with the format mm/dd. e.g: "01/26"'
});

const { cvc } = await prompts({
type: 'password',
name: 'cvc',
message: 'Enter your card cvc'
});

// Use test card for faster testing 🚀
// const testCard = { number: '4242424242424242', exp_month: 12, exp_year: 26, cvc: '123' };

const [exp_month, exp_year] = (expiration as string).split('/').map((e) => +e);
const realCard = { number: (number as string).replace('-', ''), exp_month, exp_year, cvc };

console.error('Creating payment using Stripe payment method...');

// Create the PaymentMethod using the details collected by the Payment Element
const paymentMethod = await stripe.paymentMethods.create({
type: 'card',
card: realCard
});

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await stripe.paymentIntents.confirm(id, {
payment_method: paymentMethod.id,
client_secret: client_secret
});
} else {
// Build the Stripe Checkout URL with the Payment Intent client secret
const checkoutUrl = url;

// Open the user's default browser and redirect them to the Stripe Checkout page
console.error(
'Checkout session from payment service has been successfully received!\nOpening default browser and sending to Stripe checkout...'
);

try {
if (process.platform === 'darwin') {
// macOS
exec(`open ${checkoutUrl}`);
} else if (process.platform === 'win32') {
// Windows
exec(`start "" "${checkoutUrl}"`);
} else {
// Linux/Unix
open(checkoutUrl);
}
} catch (error) {
console.error(error);
console.error(
'Stripe checkout session failed to open! Please go here in a browser to fulfill your top up quote: ',
checkoutUrl
);
return ERROR_EXIT_CODE;
}
}

console.error(`You've topped up for ${+data.topUpQuote.winstonCreditAmount / 1_000_000_000_000} ARC!`);

return SUCCESS_EXIT_CODE;
})
});
22 changes: 22 additions & 0 deletions src/parameter_declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export const DataGqlTagsParameter = 'dataGqlTags';
export const MetaDataFileParameter = 'metadataFile';
export const MetaDataGqlTagsParameter = 'metadataGqlTags';
export const MetadataJsonParameter = 'metadataJson';
export const PaymentAmountParameter = 'paymentAmount';
export const CurrencyTypeParameter = 'currencyType';
export const PayInCliParameter = 'payInCli';

// Aggregates for convenience
export const CustomMetaDataParameters = [
Expand Down Expand Up @@ -569,3 +572,22 @@ Parameter.declare({
'(OPTIONAL) A list of custom Arweave tag name and value pairs in the format `"TAG_NAME" "TAG_VALUE"` that will be applied to all file data transactions created during an invocation. Must be an even number of string values. Can NOT be used in conjunction with --metadata-file',
forbiddenConjunctionParameters: [MetaDataFileParameter]
});

Parameter.declare({
name: CurrencyTypeParameter,
aliases: ['-c', '--currency-type'],
description: 'Currency type to get top up for'
});

Parameter.declare({
name: PaymentAmountParameter,
aliases: ['-a', '--payment-amount'],
description: 'Payment amount get top up for'
});

Parameter.declare({
name: PayInCliParameter,
aliases: ['-cli', '--pay-in-cli'],
description: `Pay in the CLI (BETA - LESS SECURE - NOT RECOMMENDED)`,
type: 'boolean'
});
7 changes: 7 additions & 0 deletions src/utils/base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function fromB64UrlToBuffer(input: string): Buffer {
return Buffer.from(input, 'base64');
}

export function toB64Url(buffer: Buffer): string {
return buffer.toString('base64');
}
47 changes: 47 additions & 0 deletions src/utils/signData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { JWKInterface } from 'arweave/node/lib/wallet';
import crypto, { createPrivateKey, createPublicKey, KeyLike, KeyObject } from 'crypto';
import { toB64Url } from './base64';

export async function signData(privateKey: KeyLike, dataToSign: string): Promise<Uint8Array> {
const pem = ((privateKey as unknown) as crypto.KeyObject).export({
format: 'pem',
type: 'pkcs1'
});
const sign = crypto.createSign('sha256');
sign.update(dataToSign);

const signature = sign.sign({
key: pem,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: 0 // We do not need to salt the signature since we combine with a random UUID
});
return Promise.resolve(signature);
}

export function jwkInterfaceToPublicKey(jwk: JWKInterface): KeyObject {
const publicKey = createPublicKey({
key: {
...jwk,
kty: 'RSA'
},
format: 'jwk'
});

return publicKey;
}

export function jwkInterfaceToPrivateKey(jwk: JWKInterface): KeyObject {
const privateKey = createPrivateKey({
key: {
...jwk,
kty: 'RSA'
},
format: 'jwk'
});

return privateKey;
}

export function publicKeyToHeader(publicKey: KeyObject) {
return toB64Url(Buffer.from(JSON.stringify(publicKey.export({ format: 'jwk' }))));
}
Loading