From 62ff2c881f51c492b37dc87bfb47354cdc978766 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Fri, 13 Sep 2024 12:18:00 -0500 Subject: [PATCH 1/7] feat(pol): add support for matic/pol token uploads and top ups PE-6721 --- src/cli/commands.ts | 5 +++++ src/cli/utils.ts | 3 ++- src/common/factory.ts | 2 ++ src/common/payment.ts | 2 ++ src/common/signer.ts | 2 ++ src/common/token/index.ts | 7 ++++++- src/node/factory.ts | 2 ++ src/types.ts | 11 +++++++++-- src/utils/common.ts | 1 + src/web/factory.ts | 2 ++ tests/turbo.node.test.ts | 4 +++- 11 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/cli/commands.ts b/src/cli/commands.ts index 4ea849fb..be02d65b 100644 --- a/src/cli/commands.ts +++ b/src/cli/commands.ts @@ -171,10 +171,15 @@ export async function topUp(options: TopUpOptions) { throw new Error('Must provide a wallet to top up'); } + console.log('privateKey', privateKey); const turbo = TurboFactory.authenticated({ ...config, privateKey, }); + console.log( + 'await turbo.signer.getNativeAddress()', + await turbo.signer.getNativeAddress(), + ); return turbo.createCheckoutSession({ amount, owner: await turbo.signer.getNativeAddress(), diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 377e1a3b..597fc0b5 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -167,7 +167,8 @@ const tokenToDevGatewayMap: Record = { solana: 'https://api.devnet.solana.com', ethereum: 'https://ethereum-holesky-rpc.publicnode.com', kyve: 'https://api.korellia.kyve.network', - // matic: 'https://rpc-amoy.polygon.technology', + matic: 'https://rpc-amoy.polygon.technology', + pol: 'https://rpc-amoy.polygon.technology', }; export function configFromOptions( diff --git a/src/common/factory.ts b/src/common/factory.ts index e599252b..36963ec5 100644 --- a/src/common/factory.ts +++ b/src/common/factory.ts @@ -38,6 +38,8 @@ export abstract class TurboBaseFactory { uploadServiceConfig = {}, token, }: TurboUnauthenticatedConfiguration = {}) { + token = token === 'pol' ? 'matic' : token; + const paymentService = new TurboUnauthenticatedPaymentService({ ...paymentServiceConfig, logger: this.logger, diff --git a/src/common/payment.ts b/src/common/payment.ts index fa340744..e7cf978e 100644 --- a/src/common/payment.ts +++ b/src/common/payment.ts @@ -182,6 +182,8 @@ export class TurboUnauthenticatedPaymentService ): Promise { const { amount: paymentAmount, type: currencyType } = amount; + console.log('owner', owner); + console.log('this.token', this.token); const endpoint = `/top-up/checkout-session/${owner}/${currencyType}/${paymentAmount}?uiMode=${uiMode}${ promoCodes.length > 0 ? `&${this.appendPromoCodesToQuery(promoCodes)}` diff --git a/src/common/signer.ts b/src/common/signer.ts index c8dbb1f6..60da2d40 100644 --- a/src/common/signer.ts +++ b/src/common/signer.ts @@ -78,6 +78,7 @@ export abstract class TurboDataItemAbstractSigner return bs58.encode(fromB64Url(owner)); case 'ethereum': + case 'matic': return computeAddress(computePublicKey(fromB64Url(owner))); case 'kyve': @@ -112,6 +113,7 @@ export abstract class TurboDataItemAbstractSigner } public async getNativeAddress(): Promise { + console.log('this.token', this.token); return this.ownerToNativeAddress( toB64Url(await this.getPublicKey()), this.token, diff --git a/src/common/token/index.ts b/src/common/token/index.ts index 5dfccd61..50bd56f1 100644 --- a/src/common/token/index.ts +++ b/src/common/token/index.ts @@ -34,11 +34,16 @@ export const defaultTokenMap: TokenFactory = { kyve: (config: TokenConfig) => new KyveToken(config), } as const; -export const tokenToBaseMap = { +export const tokenToBaseMap: Record< + TokenType, + (a: BigNumber.Value) => BigNumber.Value +> = { arweave: (a: BigNumber.Value) => ARToTokenAmount(a), solana: (a: BigNumber.Value) => SOLToTokenAmount(a), ethereum: (a: BigNumber.Value) => ETHToTokenAmount(a), kyve: (a: BigNumber.Value) => KYVEToTokenAmount(a), + matic: (a: BigNumber.Value) => ETHToTokenAmount(a), + pol: (a: BigNumber.Value) => ETHToTokenAmount(a), } as const; export function isTokenType(token: string): token is TokenType { diff --git a/src/node/factory.ts b/src/node/factory.ts index 5a535f6a..5120f1f5 100644 --- a/src/node/factory.ts +++ b/src/node/factory.ts @@ -58,6 +58,8 @@ export class TurboFactory extends TurboBaseFactory { gatewayUrl, tokenTools, }: TurboAuthenticatedConfiguration) { + token = token === 'pol' ? 'matic' : token; + if (!token) { if (providedSigner) { // Derive token from signer if not provided diff --git a/src/types.ts b/src/types.ts index cef4a17b..6c47f9aa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,7 +55,14 @@ export function isCurrency(currency: string): currency is Currency { export type Country = 'United States' | 'United Kingdom' | 'Canada'; // TODO: add full list -export const tokenTypes = ['arweave', 'solana', 'ethereum', 'kyve'] as const; +export const tokenTypes = [ + 'arweave', + 'solana', + 'ethereum', + 'kyve', + 'matic', + 'pol', +] as const; export type TokenType = (typeof tokenTypes)[number]; export type Adjustment = { @@ -332,7 +339,7 @@ export type TurboAuthenticatedPaymentServiceConfiguration = export type TurboUnauthenticatedConfiguration = { paymentServiceConfig?: TurboUnauthenticatedPaymentServiceConfiguration; uploadServiceConfig?: TurboUnauthenticatedUploadServiceConfiguration; - token?: TokenType; + token?: TokenType | 'pol'; gatewayUrl?: string; }; diff --git a/src/utils/common.ts b/src/utils/common.ts index 643f697b..9b1d53ba 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -61,6 +61,7 @@ export function createTurboSigner({ case 'solana': return new HexSolanaSigner(clientProvidedPrivateKey); case 'ethereum': + case 'matic': if (!isEthPrivateKey(clientProvidedPrivateKey)) { throw new Error( 'A valid Ethereum private key must be provided for EthereumSigner.', diff --git a/src/web/factory.ts b/src/web/factory.ts index bea8d163..0e04a9fb 100644 --- a/src/web/factory.ts +++ b/src/web/factory.ts @@ -60,6 +60,8 @@ export class TurboFactory extends TurboBaseFactory { tokenMap, tokenTools, }: TurboAuthenticatedConfiguration) { + token = token === 'pol' ? 'matic' : token; + if (!token) { if (providedSigner) { // Derive token from signer if not provided diff --git a/tests/turbo.node.test.ts b/tests/turbo.node.test.ts index c4e9310d..79105788 100644 --- a/tests/turbo.node.test.ts +++ b/tests/turbo.node.test.ts @@ -65,11 +65,13 @@ describe('Node environment', () => { }); describe('TurboDataItemSigner', () => { - const signers: Record = { + const signers: Record = { arweave: [new ArweaveSigner(testJwk), testArweaveNativeB64Address], ethereum: [new EthereumSigner(testEthWallet), testEthNativeAddress], solana: [new HexSolanaSigner(testSolWallet), testSolNativeAddress], kyve: [new EthereumSigner(testKyvePrivatekey), testKyveNativeAddress], + matic: [new EthereumSigner(testEthWallet), testEthNativeAddress], + pol: [new EthereumSigner(testEthWallet), testEthNativeAddress], }; for (const [token, [signer, expectedNativeAddress]] of Object.entries( From 77cd697c4c2bfa36853568bd1b0ea3ac50379569 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Fri, 13 Sep 2024 12:23:14 -0500 Subject: [PATCH 2/7] docs(pol): init readme updates for showing matic support PE-6721 --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c677902..ec50fb1a 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Welcome to the `@ardrive/turbo-sdk`! This SDK provides functionality for interac - [ArconnectSigner](#arconnectsigner) - [EthereumSigner](#ethereumsigner) - [Ethereum Private Key](#ethereum-private-key) + - [POL (MATIC) Private Key](#pol-matic-private-key) - [HexSolanaSigner](#hexsolanasigner) - [Solana Secret Key](#solana-secret-key) - [KYVE Private Key](#kyve-private-key) @@ -46,6 +47,7 @@ Welcome to the `@ardrive/turbo-sdk`! This SDK provides functionality for interac - [Arweave (AR) Fiat Top Up](#arweave-ar-fiat-top-up) - [Ethereum (ETH) Fiat Top Up](#ethereum-eth-fiat-top-up) - [Solana (SOL) Fiat Top Up](#solana-sol-fiat-top-up) + - [Polygon (POL / MATIC) Fiat Top Up](#polygon-pol--matic-fiat-top-up) - [KYVE Fiat Top Up](#kyve-fiat-top-up) - [`submitFundTransaction({ txId })`](#submitfundtransaction-txid-) - [TurboAuthenticatedClient](#turboauthenticatedclient) @@ -269,6 +271,15 @@ const turbo = TurboFactory.authenticated({ }); ``` +##### POL (MATIC) Private Key + +```typescript +const turbo = TurboFactory.authenticated({ + privateKey: ethHexadecimalPrivateKey, + token: 'pol', +}); +``` + ##### HexSolanaSigner ```typescript @@ -433,6 +444,17 @@ const { url, winc, paymentAmount } = await turbo.createCheckoutSession({ }); ``` +##### Polygon (POL / MATIC) Fiat Top Up + +```ts +const turbo = TurboFactory.unauthenticated({ token: 'pol' }); + +const { url, winc, paymentAmount } = await turbo.createCheckoutSession({ + amount: USD(10.0), // $10.00 USD + owner: publicPolygonAddress, +}); +``` + ##### KYVE Fiat Top Up ```ts @@ -695,7 +717,7 @@ npx turbo --help - `-g, --gateway ` - Set a custom crypto gateway URL - `-t, --token ` - Token type for the command or connected wallet (default: "arweave") -- `-w, --wallet-file ` - Wallet file to use with the action. Formats accepted: JWK.json, KYVE or ETH private key as a string, or SOL Secret Key as a Uint8Array +- `-w, --wallet-file ` - Wallet file to use with the action. Formats accepted: JWK.json, KYVE, ETH, or POL private key as a string, or SOL Secret Key as a Uint8Array - `-m, --mnemonic ` - Mnemonic to use with the action (KYVE only) - `-p, --private-key ` - Private key to use with the action From 230ea4ccce4a045feb2fd398080f9d10e0ff4544 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Fri, 13 Sep 2024 12:25:31 -0500 Subject: [PATCH 3/7] refactor: add pol to ownerToNativeAddress check PE-6721 --- src/common/signer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/signer.ts b/src/common/signer.ts index 60da2d40..33e29541 100644 --- a/src/common/signer.ts +++ b/src/common/signer.ts @@ -79,6 +79,7 @@ export abstract class TurboDataItemAbstractSigner case 'ethereum': case 'matic': + case 'pol': return computeAddress(computePublicKey(fromB64Url(owner))); case 'kyve': From 8dcdae740c697030cf61068f45d16fd5428f4703 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Fri, 13 Sep 2024 12:28:12 -0500 Subject: [PATCH 4/7] refactor: remove debug logs PE-6721 --- src/cli/commands.ts | 5 ----- src/common/payment.ts | 2 -- src/common/signer.ts | 1 - src/types.ts | 2 +- tests/turbo.node.test.ts | 2 +- 5 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/cli/commands.ts b/src/cli/commands.ts index be02d65b..4ea849fb 100644 --- a/src/cli/commands.ts +++ b/src/cli/commands.ts @@ -171,15 +171,10 @@ export async function topUp(options: TopUpOptions) { throw new Error('Must provide a wallet to top up'); } - console.log('privateKey', privateKey); const turbo = TurboFactory.authenticated({ ...config, privateKey, }); - console.log( - 'await turbo.signer.getNativeAddress()', - await turbo.signer.getNativeAddress(), - ); return turbo.createCheckoutSession({ amount, owner: await turbo.signer.getNativeAddress(), diff --git a/src/common/payment.ts b/src/common/payment.ts index e7cf978e..fa340744 100644 --- a/src/common/payment.ts +++ b/src/common/payment.ts @@ -182,8 +182,6 @@ export class TurboUnauthenticatedPaymentService ): Promise { const { amount: paymentAmount, type: currencyType } = amount; - console.log('owner', owner); - console.log('this.token', this.token); const endpoint = `/top-up/checkout-session/${owner}/${currencyType}/${paymentAmount}?uiMode=${uiMode}${ promoCodes.length > 0 ? `&${this.appendPromoCodesToQuery(promoCodes)}` diff --git a/src/common/signer.ts b/src/common/signer.ts index 33e29541..8a0375af 100644 --- a/src/common/signer.ts +++ b/src/common/signer.ts @@ -114,7 +114,6 @@ export abstract class TurboDataItemAbstractSigner } public async getNativeAddress(): Promise { - console.log('this.token', this.token); return this.ownerToNativeAddress( toB64Url(await this.getPublicKey()), this.token, diff --git a/src/types.ts b/src/types.ts index 6c47f9aa..8214cd27 100644 --- a/src/types.ts +++ b/src/types.ts @@ -339,7 +339,7 @@ export type TurboAuthenticatedPaymentServiceConfiguration = export type TurboUnauthenticatedConfiguration = { paymentServiceConfig?: TurboUnauthenticatedPaymentServiceConfiguration; uploadServiceConfig?: TurboUnauthenticatedUploadServiceConfiguration; - token?: TokenType | 'pol'; + token?: TokenType; gatewayUrl?: string; }; diff --git a/tests/turbo.node.test.ts b/tests/turbo.node.test.ts index 79105788..fcf8e1a1 100644 --- a/tests/turbo.node.test.ts +++ b/tests/turbo.node.test.ts @@ -65,7 +65,7 @@ describe('Node environment', () => { }); describe('TurboDataItemSigner', () => { - const signers: Record = { + const signers: Record = { arweave: [new ArweaveSigner(testJwk), testArweaveNativeB64Address], ethereum: [new EthereumSigner(testEthWallet), testEthNativeAddress], solana: [new HexSolanaSigner(testSolWallet), testSolNativeAddress], From ee523bacac0b0e3213e6e132af6b0195cbf64562 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Fri, 13 Sep 2024 13:38:45 -0500 Subject: [PATCH 5/7] feat(pol): add support for matic/pol crypto fund PE-6722 --- src/cli/cli.ts | 6 +- src/cli/commands.ts | 260 ------------------------------- src/cli/commands/balance.ts | 53 +++++++ src/cli/commands/cryptoFund.ts | 81 ++++++++++ src/cli/commands/index.ts | 22 +++ src/cli/commands/topUp.ts | 99 ++++++++++++ src/cli/commands/uploadFile.ts | 40 +++++ src/cli/commands/uploadFolder.ts | 46 ++++++ src/cli/constants.ts | 23 +++ src/common/token/index.ts | 3 + src/common/token/polygon.ts | 33 ++++ src/common/turbo.ts | 6 +- 12 files changed, 407 insertions(+), 265 deletions(-) delete mode 100644 src/cli/commands.ts create mode 100644 src/cli/commands/balance.ts create mode 100644 src/cli/commands/cryptoFund.ts create mode 100644 src/cli/commands/index.ts create mode 100644 src/cli/commands/topUp.ts create mode 100644 src/cli/commands/uploadFile.ts create mode 100644 src/cli/commands/uploadFolder.ts create mode 100644 src/cli/constants.ts create mode 100644 src/common/token/polygon.ts diff --git a/src/cli/cli.ts b/src/cli/cli.ts index b8e47a8d..00aed79b 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -21,12 +21,12 @@ import { Command, program } from 'commander'; import { version } from '../version.js'; import { + balance, cryptoFund, - getBalance, topUp, uploadFile, uploadFolder, -} from './commands.js'; +} from './commands/index.js'; import { globalOptions, optionMap, @@ -50,7 +50,7 @@ applyOptions( program.command('balance').description('Get balance of a Turbo address'), [optionMap.address, ...walletOptions], ).action(async (_commandOptions, command: Command) => { - await runCommand(command, getBalance); + await runCommand(command, balance); }); applyOptions( diff --git a/src/cli/commands.ts b/src/cli/commands.ts deleted file mode 100644 index 4ea849fb..00000000 --- a/src/cli/commands.ts +++ /dev/null @@ -1,260 +0,0 @@ -/** - * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * 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 { createReadStream, statSync } from 'node:fs'; -import prompts from 'prompts'; - -import { - TurboFactory, - currencyMap, - fiatCurrencyTypes, - isCurrency, - tokenToBaseMap, -} from '../node/index.js'; -import { sleep } from '../utils/common.js'; -import { version } from '../version.js'; -import { - AddressOptions, - CryptoFundOptions, - TopUpOptions, - UploadFileOptions, - UploadFolderOptions, -} from './types.js'; -import { - addressOrPrivateKeyFromOptions, - configFromOptions, - getUploadFolderOptions, - tokenFromOptions, - turboFromOptions, -} from './utils.js'; - -export async function getBalance(options: AddressOptions) { - const config = configFromOptions(options); - - const { address, privateKey } = await addressOrPrivateKeyFromOptions(options); - - if (address !== undefined) { - const turbo = TurboFactory.unauthenticated(config); - const { winc } = await turbo.getBalance(address); - - console.log( - `Turbo Balance for Native Address "${address}"\nCredits: ${ - +winc / 1_000_000_000_000 - }`, - ); - return; - } - - if (privateKey === undefined) { - throw new Error('Must provide an (--address) or use a valid wallet'); - } - - const turbo = TurboFactory.authenticated({ - ...config, - privateKey, - }); - - const { winc } = await turbo.getBalance(); - console.log( - `Turbo Balance for Wallet Address "${await turbo.signer.getNativeAddress()}"\nCredits: ${ - +winc / 1_000_000_000_000 - }`, - ); -} - -/** Fund the connected signer with crypto */ -export async function cryptoFund(options: CryptoFundOptions) { - const value = options.value; - const txId = options.txId; - - if (txId !== undefined) { - const turbo = TurboFactory.unauthenticated(configFromOptions(options)); - const result = await turbo.submitFundTransaction({ txId: txId }); - - console.log( - 'Submitted existing crypto fund transaction to payment service: \n', - JSON.stringify(result, null, 2), - ); - return; - } - - if (value === undefined) { - throw new Error( - 'Must provide a --value or --transaction-id for crypto-fund command', - ); - } - - const turbo = await turboFromOptions(options); - - const token = tokenFromOptions(options); - const tokenAmount = tokenToBaseMap[token](value); - - if (!options.skipConfirmation) { - const { winc } = await turbo.getWincForToken({ tokenAmount }); - const targetWallet = (await turbo.getTurboCryptoWallets())[token]; - - const credits = (+winc / 1_000_000_000_000).toFixed(12); - - const { confirm } = await prompts({ - type: 'confirm', - name: 'confirm', - message: `\nTransaction details:\n\n Amount: ${value} ${token}\n Target: ${targetWallet}\n Credits received: ${credits}\n Credit recipient: ${await turbo.signer.getNativeAddress()}\n Network fees: (Gas fees apply)\n\nThis payment is non-refundable. Proceed with transaction?`, - initial: true, - }); - - if (!confirm) { - console.log('Aborted crypto fund transaction'); - return; - } - } - - const result = await turbo.topUpWithTokens({ - tokenAmount, - }); - - console.log( - 'Sent crypto fund transaction: \n', - 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); - } -} - -const turboCliTags: { name: string; value: string }[] = [ - { name: 'App-Name', value: 'Turbo-CLI' }, - { name: 'App-Version', value: version }, - { name: 'App-Platform', value: process.platform }, -]; - -export async function uploadFolder( - options: UploadFolderOptions, -): Promise { - const turbo = await turboFromOptions(options); - - const { - disableManifest, - fallbackFile, - folderPath, - indexFile, - maxConcurrentUploads, - } = getUploadFolderOptions(options); - - const result = await turbo.uploadFolder({ - folderPath: folderPath, - dataItemOpts: { tags: [...turboCliTags] }, // TODO: Inject user tags - manifestOptions: { - disableManifest, - indexFile, - fallbackFile, - }, - maxConcurrentUploads, - }); - - console.log('Uploaded folder:', JSON.stringify(result, null, 2)); -} - -export async function uploadFile(options: UploadFileOptions): Promise { - const { filePath } = options; - if (filePath === undefined) { - throw new Error('Must provide a --file-path to upload'); - } - - const turbo = await turboFromOptions(options); - - const fileSize = statSync(filePath).size; - - const result = await turbo.uploadFile({ - fileStreamFactory: () => createReadStream(filePath), - fileSizeFactory: () => fileSize, - dataItemOpts: { tags: [...turboCliTags] }, // TODO: Inject user tags - }); - - console.log('Uploaded file:', JSON.stringify(result, null, 2)); -} diff --git a/src/cli/commands/balance.ts b/src/cli/commands/balance.ts new file mode 100644 index 00000000..cd30a2eb --- /dev/null +++ b/src/cli/commands/balance.ts @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { TurboFactory } from '../../node/factory.js'; +import { AddressOptions } from '../types.js'; +import { addressOrPrivateKeyFromOptions, configFromOptions } from '../utils.js'; + +export async function balance(options: AddressOptions) { + const config = configFromOptions(options); + + const { address, privateKey } = await addressOrPrivateKeyFromOptions(options); + + if (address !== undefined) { + const turbo = TurboFactory.unauthenticated(config); + const { winc } = await turbo.getBalance(address); + + console.log( + `Turbo Balance for Native Address "${address}"\nCredits: ${ + +winc / 1_000_000_000_000 + }`, + ); + return; + } + + if (privateKey === undefined) { + throw new Error('Must provide an (--address) or use a valid wallet'); + } + + const turbo = TurboFactory.authenticated({ + ...config, + privateKey, + }); + + const { winc } = await turbo.getBalance(); + console.log( + `Turbo Balance for Wallet Address "${await turbo.signer.getNativeAddress()}"\nCredits: ${ + +winc / 1_000_000_000_000 + }`, + ); +} diff --git a/src/cli/commands/cryptoFund.ts b/src/cli/commands/cryptoFund.ts new file mode 100644 index 00000000..943f2b96 --- /dev/null +++ b/src/cli/commands/cryptoFund.ts @@ -0,0 +1,81 @@ +/** + * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import prompts from 'prompts'; + +import { tokenToBaseMap } from '../../common/index.js'; +import { TurboFactory } from '../../node/factory.js'; +import { CryptoFundOptions } from '../types.js'; +import { + configFromOptions, + tokenFromOptions, + turboFromOptions, +} from '../utils.js'; + +export async function cryptoFund(options: CryptoFundOptions) { + const value = options.value; + const txId = options.txId; + + if (txId !== undefined) { + const turbo = TurboFactory.unauthenticated(configFromOptions(options)); + const result = await turbo.submitFundTransaction({ txId: txId }); + + console.log( + 'Submitted existing crypto fund transaction to payment service: \n', + JSON.stringify(result, null, 2), + ); + return; + } + + if (value === undefined) { + throw new Error( + 'Must provide a --value or --transaction-id for crypto-fund command', + ); + } + + const turbo = await turboFromOptions(options); + + const token = tokenFromOptions(options); + const tokenAmount = tokenToBaseMap[token](value); + + if (!options.skipConfirmation) { + const { winc } = await turbo.getWincForToken({ tokenAmount }); + const targetWallet = (await turbo.getTurboCryptoWallets())[token]; + + const credits = (+winc / 1_000_000_000_000).toFixed(12); + + const { confirm } = await prompts({ + type: 'confirm', + name: 'confirm', + message: `\nTransaction details:\n\n Amount: ${value} ${token}\n Target: ${targetWallet}\n Credits received: ${credits}\n Credit recipient: ${await turbo.signer.getNativeAddress()}\n Network fees: (Gas fees apply)\n\nThis payment is non-refundable. Proceed with transaction?`, + initial: true, + }); + + if (!confirm) { + console.log('Aborted crypto fund transaction'); + return; + } + } + + const result = await turbo.topUpWithTokens({ + tokenAmount, + }); + + console.log( + 'Sent crypto fund transaction: \n', + JSON.stringify(result, null, 2), + ); +} diff --git a/src/cli/commands/index.ts b/src/cli/commands/index.ts new file mode 100644 index 00000000..45f49fa5 --- /dev/null +++ b/src/cli/commands/index.ts @@ -0,0 +1,22 @@ +/** + * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +export * from './balance.js'; +export * from './cryptoFund.js'; +export * from './topUp.js'; +export * from './uploadFile.js'; +export * from './uploadFolder.js'; diff --git a/src/cli/commands/topUp.ts b/src/cli/commands/topUp.ts new file mode 100644 index 00000000..31113124 --- /dev/null +++ b/src/cli/commands/topUp.ts @@ -0,0 +1,99 @@ +/** + * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { exec } from 'child_process'; + +import { currencyMap } from '../../common/currency.js'; +import { TurboFactory } from '../../node/factory.js'; +import { fiatCurrencyTypes, isCurrency } from '../../types.js'; +import { sleep } from '../../utils/common.js'; +import { TopUpOptions } from '../types.js'; +import { addressOrPrivateKeyFromOptions, configFromOptions } from '../utils.js'; + +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); + } +} + +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); +} diff --git a/src/cli/commands/uploadFile.ts b/src/cli/commands/uploadFile.ts new file mode 100644 index 00000000..0fe2631e --- /dev/null +++ b/src/cli/commands/uploadFile.ts @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { createReadStream, statSync } from 'fs'; + +import { turboCliTags } from '../constants.js'; +import { UploadFileOptions } from '../types.js'; +import { turboFromOptions } from '../utils.js'; + +export async function uploadFile(options: UploadFileOptions): Promise { + const { filePath } = options; + if (filePath === undefined) { + throw new Error('Must provide a --file-path to upload'); + } + + const turbo = await turboFromOptions(options); + + const fileSize = statSync(filePath).size; + + const result = await turbo.uploadFile({ + fileStreamFactory: () => createReadStream(filePath), + fileSizeFactory: () => fileSize, + dataItemOpts: { tags: [...turboCliTags] }, // TODO: Inject user tags + }); + + console.log('Uploaded file:', JSON.stringify(result, null, 2)); +} diff --git a/src/cli/commands/uploadFolder.ts b/src/cli/commands/uploadFolder.ts new file mode 100644 index 00000000..385cdbc9 --- /dev/null +++ b/src/cli/commands/uploadFolder.ts @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { turboCliTags } from '../constants.js'; +import { UploadFolderOptions } from '../types.js'; +import { getUploadFolderOptions, turboFromOptions } from '../utils.js'; + +export async function uploadFolder( + options: UploadFolderOptions, +): Promise { + const turbo = await turboFromOptions(options); + + const { + disableManifest, + fallbackFile, + folderPath, + indexFile, + maxConcurrentUploads, + } = getUploadFolderOptions(options); + + const result = await turbo.uploadFolder({ + folderPath: folderPath, + dataItemOpts: { tags: [...turboCliTags] }, // TODO: Inject user tags + manifestOptions: { + disableManifest, + indexFile, + fallbackFile, + }, + maxConcurrentUploads, + }); + + console.log('Uploaded folder:', JSON.stringify(result, null, 2)); +} diff --git a/src/cli/constants.ts b/src/cli/constants.ts new file mode 100644 index 00000000..e3a13ddf --- /dev/null +++ b/src/cli/constants.ts @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { version } from '../version.js'; + +export const turboCliTags: { name: string; value: string }[] = [ + { name: 'App-Name', value: 'Turbo-CLI' }, + { name: 'App-Version', value: version }, + { name: 'App-Platform', value: process.platform }, +]; diff --git a/src/common/token/index.ts b/src/common/token/index.ts index 50bd56f1..a952107d 100644 --- a/src/common/token/index.ts +++ b/src/common/token/index.ts @@ -25,6 +25,7 @@ import { import { ARToTokenAmount, ArweaveToken } from './arweave.js'; import { ETHToTokenAmount, EthereumToken } from './ethereum.js'; import { KYVEToTokenAmount, KyveToken } from './kyve.js'; +import { PolygonToken } from './polygon.js'; import { SOLToTokenAmount, SolanaToken } from './solana.js'; export const defaultTokenMap: TokenFactory = { @@ -32,6 +33,8 @@ export const defaultTokenMap: TokenFactory = { solana: (config: TokenConfig) => new SolanaToken(config), ethereum: (config: TokenConfig) => new EthereumToken(config), kyve: (config: TokenConfig) => new KyveToken(config), + matic: (config: TokenConfig) => new PolygonToken(config), + pol: (config: TokenConfig) => new PolygonToken(config), } as const; export const tokenToBaseMap: Record< diff --git a/src/common/token/polygon.ts b/src/common/token/polygon.ts new file mode 100644 index 00000000..0884c232 --- /dev/null +++ b/src/common/token/polygon.ts @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { TokenConfig } from '../../types.js'; +import { TurboWinstonLogger } from '../logger.js'; +import { EthereumToken } from './ethereum.js'; + +export class PolygonToken extends EthereumToken { + constructor({ + logger = TurboWinstonLogger.default, + gatewayUrl = 'https://polygon-rpc.com/', + pollingOptions = { + maxAttempts: 10, + pollingIntervalMs: 4_000, + initialBackoffMs: 5_000, + }, + }: TokenConfig = {}) { + super({ logger, gatewayUrl, pollingOptions }); + } +} diff --git a/src/common/turbo.ts b/src/common/turbo.ts index c622fb49..bb44a6cb 100644 --- a/src/common/turbo.ts +++ b/src/common/turbo.ts @@ -203,8 +203,10 @@ export class TurboUnauthenticatedClient /** * Returns the connected target Turbo wallet addresses for all supported tokens. */ - getTurboCryptoWallets(): Promise> { - return this.paymentService.getTurboCryptoWallets(); + async getTurboCryptoWallets(): Promise> { + const wallets = await this.paymentService.getTurboCryptoWallets(); + wallets.pol = wallets.matic; + return wallets; } } From 092dbe16430e57b4ac5e7073fe5d4124d9e67819 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Fri, 13 Sep 2024 13:41:54 -0500 Subject: [PATCH 6/7] docs(pol): init readme updates for showing matic crypto fund support PE-6721 --- README.md | 11 +++++++++++ src/common/token/index.ts | 6 +++--- src/common/token/polygon.ts | 4 +++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ec50fb1a..b65e79ec 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ Welcome to the `@ardrive/turbo-sdk`! This SDK provides functionality for interac - [`topUpWithTokens({ tokenAmount, feeMultiplier })`](#topupwithtokens-tokenamount-feemultiplier-) - [Arweave (AR) Crypto Top Up](#arweave-ar-crypto-top-up) - [Ethereum (ETH) Crypto Top Up](#ethereum-eth-crypto-top-up) + - [Polygon (POL / MATIC) Crypto Top Up](#polygon-pol--matic-crypto-top-up) - [Solana (SOL) Crypto Top Up](#solana-sol-crypto-top-up) - [KYVE Crypto Top Up](#kyve-crypto-top-up) - [CLI](#cli) @@ -645,6 +646,16 @@ const { winc, status, id, ...fundResult } = await turbo.topUpWithTokens({ }); ``` +##### Polygon (POL / MATIC) Crypto Top Up + +```ts +const turbo = TurboFactory.authenticated({ signer, token: 'pol' }); + +const { winc, status, id, ...fundResult } = await turbo.topUpWithTokens({ + tokenAmount: POLToTokenAmount(0.00001), // 0.00001 POL +}); +``` + ##### Solana (SOL) Crypto Top Up ```ts diff --git a/src/common/token/index.ts b/src/common/token/index.ts index a952107d..f57fb91d 100644 --- a/src/common/token/index.ts +++ b/src/common/token/index.ts @@ -25,7 +25,7 @@ import { import { ARToTokenAmount, ArweaveToken } from './arweave.js'; import { ETHToTokenAmount, EthereumToken } from './ethereum.js'; import { KYVEToTokenAmount, KyveToken } from './kyve.js'; -import { PolygonToken } from './polygon.js'; +import { POLToTokenAmount, PolygonToken } from './polygon.js'; import { SOLToTokenAmount, SolanaToken } from './solana.js'; export const defaultTokenMap: TokenFactory = { @@ -45,8 +45,8 @@ export const tokenToBaseMap: Record< solana: (a: BigNumber.Value) => SOLToTokenAmount(a), ethereum: (a: BigNumber.Value) => ETHToTokenAmount(a), kyve: (a: BigNumber.Value) => KYVEToTokenAmount(a), - matic: (a: BigNumber.Value) => ETHToTokenAmount(a), - pol: (a: BigNumber.Value) => ETHToTokenAmount(a), + matic: (a: BigNumber.Value) => POLToTokenAmount(a), + pol: (a: BigNumber.Value) => POLToTokenAmount(a), } as const; export function isTokenType(token: string): token is TokenType { diff --git a/src/common/token/polygon.ts b/src/common/token/polygon.ts index 0884c232..82217ffc 100644 --- a/src/common/token/polygon.ts +++ b/src/common/token/polygon.ts @@ -16,7 +16,9 @@ */ import { TokenConfig } from '../../types.js'; import { TurboWinstonLogger } from '../logger.js'; -import { EthereumToken } from './ethereum.js'; +import { ETHToTokenAmount, EthereumToken } from './ethereum.js'; + +export const POLToTokenAmount = ETHToTokenAmount; export class PolygonToken extends EthereumToken { constructor({ From a48d90237ba2b9e4307af0ab6f5ae5bdfcb1402d Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Fri, 13 Sep 2024 13:50:14 -0500 Subject: [PATCH 7/7] chore: ignore codecov in token dir PE-6722 --- codecov.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/codecov.yml b/codecov.yml index 2c71dae7..25b2c554 100644 --- a/codecov.yml +++ b/codecov.yml @@ -6,3 +6,4 @@ coverage: threshold: 1% ignore: - 'src/cli/*' + - 'src/common/token/*'