From e565bf709a54d14411bd09bd46599a943ef0f73c Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 16 Oct 2024 15:16:25 -0500 Subject: [PATCH 01/28] chore: adjust sol types PE-6754 --- src/common/token/solana.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/common/token/solana.ts b/src/common/token/solana.ts index 48f0f4e9..c43d598c 100644 --- a/src/common/token/solana.ts +++ b/src/common/token/solana.ts @@ -69,7 +69,9 @@ export class SolanaToken implements TokenTools { id: string; target: string; }> { - const publicKey = new PublicKey(bs58.encode(await signer.getPublicKey())); + const publicKey = new PublicKey( + bs58.encode(Uint8Array.from(await signer.getPublicKey())), + ); const tx = new Transaction({ feePayer: publicKey, ...(await this.connection.getLatestBlockhash()), @@ -84,7 +86,7 @@ export class SolanaToken implements TokenTools { ); const serializedTx = tx.serializeMessage(); - const signature = await signer.signData(serializedTx); + const signature = await signer.signData(Uint8Array.from(serializedTx)); tx.addSignature(publicKey, Buffer.from(signature)); From 953648e67719cc3c663ac75a702fd653e33b7c8f Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 16 Oct 2024 15:17:39 -0500 Subject: [PATCH 02/28] feat(delegated payments): add paid-by headers for uploads when applicable PE-6754 --- src/cli/commands/uploadFile.ts | 4 ++-- src/cli/options.ts | 5 +++++ src/cli/types.ts | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/cli/commands/uploadFile.ts b/src/cli/commands/uploadFile.ts index 43ff13ad..3629bd3c 100644 --- a/src/cli/commands/uploadFile.ts +++ b/src/cli/commands/uploadFile.ts @@ -20,7 +20,7 @@ import { UploadFileOptions } from '../types.js'; import { turboFromOptions } from '../utils.js'; export async function uploadFile(options: UploadFileOptions): Promise { - const { filePath } = options; + const { filePath, paidBy } = options; if (filePath === undefined) { throw new Error('Must provide a --file-path to upload'); } @@ -32,7 +32,7 @@ export async function uploadFile(options: UploadFileOptions): Promise { const result = await turbo.uploadFile({ fileStreamFactory: () => createReadStream(filePath), fileSizeFactory: () => fileSize, - dataItemOpts: { tags: [...turboCliTags] }, // TODO: Inject user tags + dataItemOpts: { tags: [...turboCliTags], paidBy }, // TODO: Inject user tags }); console.log('Uploaded file:', JSON.stringify(result, null, 2)); diff --git a/src/cli/options.ts b/src/cli/options.ts index b66113aa..4182fae1 100644 --- a/src/cli/options.ts +++ b/src/cli/options.ts @@ -120,6 +120,11 @@ export const optionMap = { alias: '--max-concurrency ', description: 'Maximum number of concurrent uploads', }, + paidBy: { + alias: '--paid-by ', + description: 'Address to pay for the upload', + type: 'array', + }, } as const; export const walletOptions = [ diff --git a/src/cli/types.ts b/src/cli/types.ts index 5c4edcb5..e2365570 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -50,6 +50,7 @@ export type UploadFolderOptions = WalletOptions & { export type UploadFileOptions = WalletOptions & { filePath: string | undefined; + paidBy: string[]; }; export type PriceOptions = GlobalOptions & { From 10477621bad131ec85203814ce3846040cba15ea Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 16 Oct 2024 15:18:04 -0500 Subject: [PATCH 03/28] feat(delegated payments): init logic for create-approval PE-6754 --- src/cli/cli.ts | 11 +++++++ src/cli/commands/createApproval.ts | 50 ++++++++++++++++++++++++++++++ src/cli/options.ts | 18 ++++++++++- src/cli/types.ts | 6 ++++ src/common/turbo.ts | 12 +++++++ src/common/upload.ts | 50 +++++++++++++++++++++++++++--- src/types.ts | 14 ++++++++- 7 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 src/cli/commands/createApproval.ts diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 76c33a16..bf66424e 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -20,6 +20,7 @@ import { Command, program } from 'commander'; import { version } from '../version.js'; +import { createApproval } from './commands/createApproval.js'; import { balance, cryptoFund, @@ -29,6 +30,7 @@ import { uploadFolder, } from './commands/index.js'; import { + createApprovalOptions, globalOptions, optionMap, uploadFileOptions, @@ -93,6 +95,15 @@ applyOptions( await runCommand(command, price); }); +applyOptions( + program + .command('create-approval') + .description('Create a Turbo delegated payment approval'), + createApprovalOptions, +).action(async (_commandOptions, command: Command) => { + await runCommand(command, createApproval); +}); + if ( process.argv[1].includes('bin/turbo') || // Running from global .bin process.argv[1].includes('cli/cli') // Running from source diff --git a/src/cli/commands/createApproval.ts b/src/cli/commands/createApproval.ts new file mode 100644 index 00000000..d00c1bd0 --- /dev/null +++ b/src/cli/commands/createApproval.ts @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { BigNumber } from 'bignumber.js'; + +import { CreateApprovalOptions } from '../types.js'; +import { turboFromOptions } from '../utils.js'; + +export async function createApproval( + options: CreateApprovalOptions, +): Promise { + const { + address: approvedAddress, + value: creditAmount, + expiresBySeconds, + } = options; + if (approvedAddress === undefined) { + throw new Error( + 'Must provide an approved --address to create approval for', + ); + } + if (creditAmount === undefined) { + throw new Error('Must provide a credit --value to create approval for'); + } + + const turbo = await turboFromOptions(options); + + const approvedWincAmount = new BigNumber(creditAmount) + .shiftedBy(12) + .toString(); + const result = await turbo.createDelegatedPaymentApproval({ + approvedAddress, + approvedWincAmount, + expiresBySeconds, + }); + + console.log('Created approval:', JSON.stringify(result, null, 2)); +} diff --git a/src/cli/options.ts b/src/cli/options.ts index 4182fae1..9d50e2d5 100644 --- a/src/cli/options.ts +++ b/src/cli/options.ts @@ -125,6 +125,10 @@ export const optionMap = { description: 'Address to pay for the upload', type: 'array', }, + expiresBySeconds: { + alias: '--expires-by-seconds ', + description: 'Expiration time in seconds', + }, } as const; export const walletOptions = [ @@ -151,6 +155,18 @@ export const uploadFolderOptions = [ optionMap.fallbackFile, optionMap.manifest, optionMap.maxConcurrency, + optionMap.paidBy, +]; + +export const uploadFileOptions = [ + ...walletOptions, + optionMap.filePath, + optionMap.paidBy, ]; -export const uploadFileOptions = [...walletOptions, optionMap.filePath]; +export const createApprovalOptions = [ + ...walletOptions, + optionMap.value, + optionMap.address, + optionMap.expiresBySeconds, +]; diff --git a/src/cli/types.ts b/src/cli/types.ts index e2365570..7aca7060 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -62,3 +62,9 @@ export type CryptoFundOptions = WalletOptions & { value: string | undefined; txId: string | undefined; }; + +export type CreateApprovalOptions = WalletOptions & { + address: string | undefined; + value: string | undefined; + expiresBySeconds: number | undefined; +}; diff --git a/src/common/turbo.ts b/src/common/turbo.ts index 9ce64e0c..fa507d1e 100644 --- a/src/common/turbo.ts +++ b/src/common/turbo.ts @@ -26,6 +26,7 @@ import { TurboCheckoutSessionParams, TurboCheckoutSessionResponse, TurboCountriesResponse, + TurboCreateDelegatedPaymentApprovalParams, TurboCryptoFundResponse, TurboCurrenciesResponse, TurboDataItemSigner, @@ -265,4 +266,15 @@ export class TurboAuthenticatedClient ): Promise { return this.paymentService.topUpWithTokens(p); } + + /** + * Creates a data item with tags that designate it as a delegated payment approval. + * Signs the data item and sends it to the Turbo Upload Service, which will verify + * the signature and forward the admin action towards the Turbo Payment Service. + */ + createDelegatedPaymentApproval( + p: TurboCreateDelegatedPaymentApprovalParams, + ): Promise { + return this.uploadService.createDelegatedPaymentApproval(p); + } } diff --git a/src/common/upload.ts b/src/common/upload.ts index 231862b5..12437fc8 100644 --- a/src/common/upload.ts +++ b/src/common/upload.ts @@ -24,6 +24,7 @@ import { TurboAbortSignal, TurboAuthenticatedUploadServiceConfiguration, TurboAuthenticatedUploadServiceInterface, + TurboCreateDelegatedPaymentApprovalParams, TurboDataItemSigner, TurboFileFactory, TurboLogger, @@ -37,6 +38,11 @@ import { import { TurboHTTPService } from './http.js'; import { TurboWinstonLogger } from './logger.js'; +export const createDelegatedPaymentApprovalTagName = 'x-approve-payment'; +export const approvalAmountTagName = 'x-amount'; +export const approvalExpiresBySecondsTagName = 'x-expires-seconds'; +export const revokeDelegatePaymentApprovalTagName = 'x-delete-payment-approval'; + export const developmentUploadServiceURL = 'https://upload.ardrive.dev'; export const defaultUploadServiceURL = 'https://upload.ardrive.io'; @@ -115,17 +121,23 @@ export abstract class TurboAuthenticatedBaseUploadService dataItemOpts, }); const signedDataItem = dataItemStreamFactory(); - const fileSize = dataItemSizeFactory(); this.logger.debug('Uploading signed data item...'); // TODO: add p-limit constraint or replace with separate upload class + console.log('dataItemOpts.paidBy', dataItemOpts?.paidBy); + + const headers = { + 'content-type': 'application/octet-stream', + 'content-length': `${dataItemSizeFactory()}`, + }; + if (dataItemOpts && dataItemOpts.paidBy && dataItemOpts.paidBy.length > 0) { + headers['x-paid-by'] = dataItemOpts.paidBy; + } + return this.httpService.post({ endpoint: `/tx/${this.token}`, signal, data: signedDataItem, - headers: { - 'content-type': 'application/octet-stream', - 'content-length': `${fileSize}`, - }, + headers, }); } @@ -296,4 +308,32 @@ export abstract class TurboAuthenticatedBaseUploadService manifestResponse, }; } + + public async createDelegatedPaymentApproval({ + approvedAddress, + approvedWincAmount, + expiresBySeconds, + }: TurboCreateDelegatedPaymentApprovalParams): Promise { + const dataItemOpts = { + tags: [ + { name: createDelegatedPaymentApprovalTagName, value: approvedAddress }, + { name: approvalAmountTagName, value: approvedWincAmount.toString() }, + ], + }; + if (expiresBySeconds !== undefined) { + dataItemOpts.tags.push({ + name: approvalExpiresBySecondsTagName, + value: expiresBySeconds.toString(), + }); + } + + const nonceData = Buffer.from( + approvedAddress + approvedWincAmount + Date.now(), + ); + return this.uploadFile({ + fileStreamFactory: () => Readable.from(nonceData), + fileSizeFactory: () => nonceData.byteLength, + dataItemOpts, + }); + } } diff --git a/src/types.ts b/src/types.ts index aca6c86b..b52bea6a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -207,6 +207,12 @@ export const isWebUploadFolderParams = ( ): p is WebUploadFolderParams => (p as WebUploadFolderParams).files !== undefined; +export type TurboCreateDelegatedPaymentApprovalParams = { + approvedAddress: string; + approvedWincAmount: BigNumber.Value; + expiresBySeconds?: number; +}; + export type TurboUploadFolderResponse = { fileResponses: TurboUploadDataItemResponse[]; manifestResponse?: TurboUploadDataItemResponse; @@ -355,7 +361,9 @@ export interface TurboLogger { debug: (message: string, ...args: unknown[]) => void; } -export type DataItemOptions = DataItemCreateOptions; +export type DataItemOptions = DataItemCreateOptions & { + paidBy?: UserAddress[]; +}; // Supported signers - we will continue to add more export type TurboSigner = @@ -574,6 +582,10 @@ export interface TurboAuthenticatedUploadServiceInterface }: TurboFileFactory & TurboAbortSignal): Promise; uploadFolder(p: TurboUploadFolderParams): Promise; + + createDelegatedPaymentApproval( + p: TurboCreateDelegatedPaymentApprovalParams, + ): Promise; } export interface TurboUnauthenticatedClientInterface From f2d26daa5ee12008dc8173a0ee44bfc2828028ba Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 16 Oct 2024 15:50:05 -0500 Subject: [PATCH 04/28] feat(delegated payments): add revoke approvals to SDK and CLI PE-6754 --- src/cli/cli.ts | 12 ++++++++++ src/cli/commands/revokeApprovals.ts | 36 +++++++++++++++++++++++++++++ src/cli/options.ts | 2 ++ src/cli/types.ts | 4 ++++ src/common/turbo.ts | 14 +++++++++++ src/common/upload.ts | 21 +++++++++++++++++ src/types.ts | 7 ++++++ 7 files changed, 96 insertions(+) create mode 100644 src/cli/commands/revokeApprovals.ts diff --git a/src/cli/cli.ts b/src/cli/cli.ts index bf66424e..8df93bdd 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -29,10 +29,12 @@ import { uploadFile, uploadFolder, } from './commands/index.js'; +import { revokeApprovals } from './commands/revokeApprovals.js'; import { createApprovalOptions, globalOptions, optionMap, + revokeApprovalsOptions, uploadFileOptions, uploadFolderOptions, walletOptions, @@ -103,6 +105,16 @@ applyOptions( ).action(async (_commandOptions, command: Command) => { await runCommand(command, createApproval); }); +applyOptions( + program + .command('revoke-approvals') + .description( + 'Revokes all Turbo delegated payment approvals for given address', + ), + revokeApprovalsOptions, +).action(async (_commandOptions, command: Command) => { + await runCommand(command, revokeApprovals); +}); if ( process.argv[1].includes('bin/turbo') || // Running from global .bin diff --git a/src/cli/commands/revokeApprovals.ts b/src/cli/commands/revokeApprovals.ts new file mode 100644 index 00000000..1c0fe981 --- /dev/null +++ b/src/cli/commands/revokeApprovals.ts @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { RevokeApprovalsOptions } from '../types.js'; +import { turboFromOptions } from '../utils.js'; + +export async function revokeApprovals( + options: RevokeApprovalsOptions, +): Promise { + const { address: revokedAddress } = options; + if (revokedAddress === undefined) { + throw new Error( + 'Must provide an approved --address to create approval for', + ); + } + + const turbo = await turboFromOptions(options); + + const result = await turbo.revokeDelegatedPaymentApprovals({ + revokedAddress, + }); + + console.log('Created approval:', JSON.stringify(result, null, 2)); +} diff --git a/src/cli/options.ts b/src/cli/options.ts index 9d50e2d5..4f739a8e 100644 --- a/src/cli/options.ts +++ b/src/cli/options.ts @@ -170,3 +170,5 @@ export const createApprovalOptions = [ optionMap.address, optionMap.expiresBySeconds, ]; + +export const revokeApprovalsOptions = [...walletOptions, optionMap.address]; diff --git a/src/cli/types.ts b/src/cli/types.ts index 7aca7060..8d6fa61b 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -68,3 +68,7 @@ export type CreateApprovalOptions = WalletOptions & { value: string | undefined; expiresBySeconds: number | undefined; }; + +export type RevokeApprovalsOptions = WalletOptions & { + address: string | undefined; +}; diff --git a/src/common/turbo.ts b/src/common/turbo.ts index fa507d1e..1e2d7ae5 100644 --- a/src/common/turbo.ts +++ b/src/common/turbo.ts @@ -35,6 +35,7 @@ import { TurboFundWithTokensParams, TurboPriceResponse, TurboRatesResponse, + TurboRevokeDelegatePaymentApprovalsParams, TurboSignedDataItemFactory, TurboSubmitFundTxResponse, TurboUnauthenticatedClientConfiguration, @@ -277,4 +278,17 @@ export class TurboAuthenticatedClient ): Promise { return this.uploadService.createDelegatedPaymentApproval(p); } + + /** + * Creates a data item with tags that designate it as a revoke action for delegated + * payment approvals for target revokedAddress. Signs the data item and sends it to + * the Turbo Upload Service, which will verify the signature and forward the admin + * action towards the Turbo Payment Service. + */ + + revokeDelegatedPaymentApprovals( + p: TurboRevokeDelegatePaymentApprovalsParams, + ): Promise { + return this.uploadService.revokeDelegatedPaymentApprovals(p); + } } diff --git a/src/common/upload.ts b/src/common/upload.ts index 12437fc8..56f864c4 100644 --- a/src/common/upload.ts +++ b/src/common/upload.ts @@ -28,6 +28,7 @@ import { TurboDataItemSigner, TurboFileFactory, TurboLogger, + TurboRevokeDelegatePaymentApprovalsParams, TurboSignedDataItemFactory, TurboUnauthenticatedUploadServiceConfiguration, TurboUnauthenticatedUploadServiceInterface, @@ -336,4 +337,24 @@ export abstract class TurboAuthenticatedBaseUploadService dataItemOpts, }); } + + public async revokeDelegatedPaymentApprovals({ + revokedAddress, + }: TurboRevokeDelegatePaymentApprovalsParams): Promise { + const dataItemOpts = { + tags: [ + { + name: revokeDelegatePaymentApprovalTagName, + value: revokedAddress, + }, + ], + }; + + const nonceData = Buffer.from(revokedAddress + Date.now()); + return this.uploadFile({ + fileStreamFactory: () => Readable.from(nonceData), + fileSizeFactory: () => nonceData.byteLength, + dataItemOpts, + }); + } } diff --git a/src/types.ts b/src/types.ts index b52bea6a..c7d15fc5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -213,6 +213,10 @@ export type TurboCreateDelegatedPaymentApprovalParams = { expiresBySeconds?: number; }; +export type TurboRevokeDelegatePaymentApprovalsParams = { + revokedAddress: string; +}; + export type TurboUploadFolderResponse = { fileResponses: TurboUploadDataItemResponse[]; manifestResponse?: TurboUploadDataItemResponse; @@ -586,6 +590,9 @@ export interface TurboAuthenticatedUploadServiceInterface createDelegatedPaymentApproval( p: TurboCreateDelegatedPaymentApprovalParams, ): Promise; + revokeDelegatedPaymentApprovals( + p: TurboRevokeDelegatePaymentApprovalsParams, + ): Promise; } export interface TurboUnauthenticatedClientInterface From baec1072b7750cc08bb1588dc5ff2a870f47b3e5 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Mon, 21 Oct 2024 13:47:01 -0500 Subject: [PATCH 05/28] feat(delegated payments): extend turbo.getBalance method to include approval details PE-6754 --- src/common/payment.ts | 10 +++++++++- src/types.ts | 33 +++++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/common/payment.ts b/src/common/payment.ts index e5276393..63b8397c 100644 --- a/src/common/payment.ts +++ b/src/common/payment.ts @@ -81,7 +81,15 @@ export class TurboUnauthenticatedPaymentService allowedStatuses: [200, 404], }); - return balance.winc ? balance : { winc: '0' }; + return balance.winc + ? balance + : { + winc: '0', + controlledWinc: '0', + effectiveBalance: '0', + givenApprovals: [], + receivedApprovals: [], + }; } public getFiatRates(): Promise { diff --git a/src/types.ts b/src/types.ts index c7d15fc5..ac051761 100644 --- a/src/types.ts +++ b/src/types.ts @@ -155,10 +155,35 @@ export type TurboCheckoutSessionResponse = TurboWincForFiatResponse & { paymentAmount: number; }; -export type TurboBalanceResponse = Omit< - TurboPriceResponse, - 'adjustments' | 'fees' ->; +export interface DelegatedPaymentApproval { + approvalDataItemId: TransactionId; + approvedAddress: UserAddress; + payerAddress: UserAddress; + approvedWincAmount: string; + usedWincAmount: string; + creationDate: string; + expirationDate: string | undefined; +} + +export type TurboBalanceResponse = { + /** + * Amount of winc controlled by the user, that they could + * spend or share if all current approvals were revoked + */ + controlledWinc: string; + /** + * Amount of winc that a user can currently spend or share + */ + winc: string; + /** + * Amount of winc that a user can currently spend or share + * plus the amount of remaining winc from received approvals + */ + effectiveBalance: string; + + receivedApprovals: DelegatedPaymentApproval[]; + givenApprovals: DelegatedPaymentApproval[]; +}; export type TurboFiatToArResponse = { currency: Currency; From f6101d98cf7ef6af65b250fbd7703138ed1e5005 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 23 Oct 2024 14:04:19 -0500 Subject: [PATCH 06/28] test(delegated payments): init docker integration tests for delegated payments PE-6754 --- docker-compose.yml | 6 +- package.json | 5 +- src/common/payment.ts | 22 ++- src/common/turbo.ts | 23 ++- src/common/upload.ts | 11 +- src/types.ts | 18 ++- tests/approval.node.test.ts | 281 ++++++++++++++++++++++++++++++++++++ tests/helpers.ts | 10 +- yarn.lock | 2 +- 9 files changed, 359 insertions(+), 19 deletions(-) create mode 100644 tests/approval.node.test.ts diff --git a/docker-compose.yml b/docker-compose.yml index 8dd7120e..2935e664 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.0' - services: arlocal: image: textury/arlocal:v1.1.66 @@ -25,6 +23,8 @@ services: # - '8545:8545' upload-service: + # build: + # context: ../upload-service image: ghcr.io/ardriveapp/upload-service:latest ports: - '3000:3000' @@ -112,6 +112,8 @@ services: start_period: 10s payment-service: + # build: + # context: ../payment-service image: ghcr.io/ardriveapp/payment-service:latest ports: - '4000:4000' diff --git a/package.json b/package.json index ef6c84f7..84ff3442 100644 --- a/package.json +++ b/package.json @@ -69,16 +69,16 @@ "example:web": "http-server --port 8080 --host -o examples/web", "example:ts:esm": "cd examples/typescript/esm && yarn && yarn test", "example:ts:cjs": "cd examples/typescript/cjs && yarn && yarn test", - "docker:up": "docker compose up --quiet-pull --pull always -d", + "docker:up": "docker compose up --quiet-pull -d", "docker:down": "docker compose down -v", "docs": "markdown-toc-gen insert README.md" }, "dependencies": { - "@dha-team/arbundles": "^1.0.1", "@cosmjs/amino": "^0.32.4", "@cosmjs/crypto": "^0.32.4", "@cosmjs/encoding": "^0.32.4", "@cosmjs/proto-signing": "^0.32.4", + "@dha-team/arbundles": "^1.0.1", "@ethersproject/signing-key": "^5.7.0", "@kyvejs/sdk": "^1.3.3", "@solana/web3.js": "^1.91.7", @@ -113,6 +113,7 @@ "@typescript-eslint/parser": "^6.4.0", "c8": "^8.0.1", "chai": "^4.3.7", + "dotenv": "^16.4.5", "dotenv-cli": "^7.4.1", "esbuild": "^0.19.2", "esbuild-plugin-polyfill-node": "^0.3.0", diff --git a/src/common/payment.ts b/src/common/payment.ts index 63b8397c..50a7e6c3 100644 --- a/src/common/payment.ts +++ b/src/common/payment.ts @@ -18,6 +18,7 @@ import { Buffer } from 'node:buffer'; import { Currency, + GetDelegatedPaymentApprovalsResponse, RawWincForTokenResponse, TokenTools, TokenType, @@ -46,6 +47,7 @@ import { TurboWincForFiatResponse, TurboWincForTokenParams, TurboWincForTokenResponse, + UserAddress, } from '../types.js'; import { TurboHTTPService } from './http.js'; import { TurboWinstonLogger } from './logger.js'; @@ -262,6 +264,14 @@ export class TurboUnauthenticatedPaymentService } throw new Error('Unknown response from payment service: ' + response); } + + public async getDelegatedPaymentApprovals( + userAddress: UserAddress, + ): Promise { + return this.httpService.get({ + endpoint: `/account/approvals/get?userAddress=${userAddress}`, + }); + } } // NOTE: to avoid redundancy, we use inheritance here - but generally prefer composition over inheritance export class TurboAuthenticatedPaymentService @@ -284,10 +294,16 @@ export class TurboAuthenticatedPaymentService this.tokenTools = tokenTools; } - public async getBalance(address?: string): Promise { - address ??= await this.signer.getNativeAddress(); + public async getBalance(userAddress?: string): Promise { + userAddress ??= await this.signer.getNativeAddress(); + return super.getBalance(userAddress); + } - return super.getBalance(address); + public async getDelegatedPaymentApprovals( + userAddress?: string, + ): Promise { + userAddress ??= await this.signer.getNativeAddress(); + return super.getDelegatedPaymentApprovals(userAddress); } public async getWincForFiat({ diff --git a/src/common/turbo.ts b/src/common/turbo.ts index 1e2d7ae5..5a865e4d 100644 --- a/src/common/turbo.ts +++ b/src/common/turbo.ts @@ -15,6 +15,7 @@ */ import { Currency, + GetDelegatedPaymentApprovalsResponse, NativeAddress, TokenType, TurboAbortSignal, @@ -209,6 +210,15 @@ export class TurboUnauthenticatedClient wallets.pol = wallets.matic; return wallets; } + + /** + * Returns a list of all delegated payment approvals for the user. + */ + getDelegatedPaymentApprovals( + userAddress: NativeAddress, + ): Promise { + return this.paymentService.getDelegatedPaymentApprovals(userAddress); + } } export class TurboAuthenticatedClient @@ -232,8 +242,17 @@ export class TurboAuthenticatedClient /** * Returns the current balance of the user's wallet in 'winc'. */ - getBalance(address?: NativeAddress): Promise { - return this.paymentService.getBalance(address); + getBalance(userAddress?: NativeAddress): Promise { + return this.paymentService.getBalance(userAddress); + } + + /** + * Returns a list of all delegated payment approvals for the user. + */ + getDelegatedPaymentApprovals( + userAddress?: NativeAddress, + ): Promise { + return this.paymentService.getDelegatedPaymentApprovals(userAddress); } /** diff --git a/src/common/upload.ts b/src/common/upload.ts index 56f864c4..a1943009 100644 --- a/src/common/upload.ts +++ b/src/common/upload.ts @@ -124,14 +124,19 @@ export abstract class TurboAuthenticatedBaseUploadService const signedDataItem = dataItemStreamFactory(); this.logger.debug('Uploading signed data item...'); // TODO: add p-limit constraint or replace with separate upload class - console.log('dataItemOpts.paidBy', dataItemOpts?.paidBy); const headers = { 'content-type': 'application/octet-stream', 'content-length': `${dataItemSizeFactory()}`, }; - if (dataItemOpts && dataItemOpts.paidBy && dataItemOpts.paidBy.length > 0) { - headers['x-paid-by'] = dataItemOpts.paidBy; + if (dataItemOpts !== undefined && dataItemOpts.paidBy !== undefined) { + const paidBy = Array.isArray(dataItemOpts.paidBy) + ? dataItemOpts.paidBy + : [dataItemOpts.paidBy]; + + if (dataItemOpts.paidBy.length > 0) { + headers['x-paid-by'] = paidBy; + } } return this.httpService.post({ diff --git a/src/types.ts b/src/types.ts index ac051761..5a65f1ec 100644 --- a/src/types.ts +++ b/src/types.ts @@ -158,13 +158,18 @@ export type TurboCheckoutSessionResponse = TurboWincForFiatResponse & { export interface DelegatedPaymentApproval { approvalDataItemId: TransactionId; approvedAddress: UserAddress; - payerAddress: UserAddress; + payingAddress: UserAddress; approvedWincAmount: string; usedWincAmount: string; creationDate: string; expirationDate: string | undefined; } +export interface GetDelegatedPaymentApprovalsResponse { + givenApprovals: DelegatedPaymentApproval[]; + receivedApprovals: DelegatedPaymentApproval[]; +} + export type TurboBalanceResponse = { /** * Amount of winc controlled by the user, that they could @@ -201,6 +206,7 @@ export type TurboUploadDataItemResponse = { fastFinalityIndexes: string[]; id: TransactionId; owner: PublicArweaveAddress; + winc: string; }; type UploadFolderParams = { @@ -391,7 +397,7 @@ export interface TurboLogger { } export type DataItemOptions = DataItemCreateOptions & { - paidBy?: UserAddress[]; + paidBy?: UserAddress | UserAddress[]; }; // Supported signers - we will continue to add more @@ -579,6 +585,9 @@ export interface TurboUnauthenticatedPaymentServiceInterface { submitFundTransaction(p: { txId: string; }): Promise; + getDelegatedPaymentApprovals( + userAddress: UserAddress, + ): Promise; } export type TurboFundWithTokensParams = { @@ -589,7 +598,10 @@ export type TurboFundWithTokensParams = { export interface TurboAuthenticatedPaymentServiceInterface extends TurboUnauthenticatedPaymentServiceInterface { - getBalance: (address?: string) => Promise; + getBalance: (userAddress?: UserAddress) => Promise; + getDelegatedPaymentApprovals( + userAddress?: UserAddress, + ): Promise; topUpWithTokens( p: TurboFundWithTokensParams, ): Promise; diff --git a/tests/approval.node.test.ts b/tests/approval.node.test.ts new file mode 100644 index 00000000..e0bd02b9 --- /dev/null +++ b/tests/approval.node.test.ts @@ -0,0 +1,281 @@ +import Arweave from 'arweave'; +import { expect } from 'chai'; +import { createReadStream, statSync } from 'node:fs'; + +import { JWKInterface } from '../src/common/jwk.js'; +import { TurboAuthenticatedClient } from '../src/common/turbo.js'; +import { TurboFactory } from '../src/node/factory.js'; +import { UserAddress } from '../src/types.js'; +import { jwkToPublicArweaveAddress } from '../src/utils/base64.js'; +import { sleep } from '../src/utils/common.js'; +import { + arweaveUrlString, + expectAsyncErrorThrow, + fundArLocalWalletAddress, + mineArLocalBlock, + sendFundTransaction, + turboDevelopmentConfigurations, +} from './helpers.js'; + +describe('Delegated Payments', () => { + let fundedPayerArweaveJwk: JWKInterface; + + let arweavePayerAddress: UserAddress; + const unfundedSignerAddress1: UserAddress = + '43CharArweaveStubAddress1234567890123456789'; + + const arweaveTestConfig = { + ...turboDevelopmentConfigurations, + gatewayUrl: arweaveUrlString, + }; + let turbo: TurboAuthenticatedClient; + + before(async () => { + fundedPayerArweaveJwk = await Arweave.crypto.generateJWK(); + + arweavePayerAddress = jwkToPublicArweaveAddress(fundedPayerArweaveJwk); + + await fundArLocalWalletAddress(arweavePayerAddress); + + turbo = TurboFactory.authenticated({ + privateKey: fundedPayerArweaveJwk, + ...arweaveTestConfig, + }); + const id = await sendFundTransaction(1000, fundedPayerArweaveJwk); + await mineArLocalBlock(25); + await turbo.submitFundTransaction({ + txId: id, + }); + + const res = await turbo.getBalance(); + const { controlledWinc, effectiveBalance, winc: wincLater } = res; + + expect(controlledWinc).to.equal('766'); + expect(effectiveBalance).to.equal('766'); + expect(wincLater).to.equal('766'); + }); + + let oldestApprovalId: string; + + describe('createDelegatedPaymentApproval', () => { + it('should properly create a delegated payment approval', async () => { + const { id, owner } = await turbo.createDelegatedPaymentApproval({ + approvedWincAmount: '100', + approvedAddress: unfundedSignerAddress1, + }); + oldestApprovalId = id; + expect(id).to.be.a('string'); + expect(owner).to.equal(arweavePayerAddress); + + const balance = await turbo.getBalance(); + const { + controlledWinc, + effectiveBalance, + givenApprovals, + receivedApprovals, + winc, + } = balance; + + expect(controlledWinc).to.equal('766'); + expect(winc).to.equal('666'); + expect(effectiveBalance).to.equal('666'); + expect(givenApprovals).to.have.length(1); + expect(receivedApprovals).to.have.length(0); + }); + + it('should properly create a delegated payment approval with expiration, and the approval should expire as expected', async () => { + const { id, owner } = await turbo.createDelegatedPaymentApproval({ + approvedWincAmount: '100', + approvedAddress: unfundedSignerAddress1, + expiresBySeconds: 1, + }); + expect(id).to.be.a('string'); + expect(owner).to.equal(arweavePayerAddress); + + const balance = await turbo.getBalance(); + const { + controlledWinc, + effectiveBalance, + givenApprovals, + receivedApprovals, + winc, + } = balance; + + expect(controlledWinc).to.equal('766'); + expect(winc).to.equal('566'); + expect(effectiveBalance).to.equal('566'); + expect(givenApprovals).to.have.length(2); + expect(receivedApprovals).to.have.length(0); + await sleep(1500); + + const balanceLater = await turbo.getBalance(); + const { + controlledWinc: controlledWincLater, + effectiveBalance: effectiveBalanceLater, + givenApprovals: givenApprovalsLater, + receivedApprovals: receivedApprovalsLater, + winc: wincLater, + } = balanceLater; + + expect(controlledWincLater).to.equal('766'); + expect(effectiveBalanceLater).to.equal('666'); + expect(wincLater).to.equal('666'); + expect(givenApprovalsLater).to.have.length(1); + expect(receivedApprovalsLater).to.have.length(0); + }); + + it('should fail to create payment approvals to invalid addresses', async () => { + await expectAsyncErrorThrow({ + promiseToError: turbo.createDelegatedPaymentApproval({ + approvedWincAmount: '100', + approvedAddress: 'invalidAddress', + }), + errorMessage: + 'Failed request: 400: Unable to create delegated payment approval : Invalid approved address', + errorType: 'FailedRequestError', + }); + }); + + it('should fail to create payment approvals when payer has insufficient balance for approval', async () => { + await expectAsyncErrorThrow({ + promiseToError: turbo.createDelegatedPaymentApproval({ + approvedWincAmount: '10000', + approvedAddress: unfundedSignerAddress1, + }), + errorMessage: `Failed request: 400: Unable to create delegated payment approval : Insufficient balance for '${arweavePayerAddress}'`, + errorType: 'FailedRequestError', + }); + }); + }); + + describe('getDelegatedPaymentApprovals', () => { + it('should properly get all delegated payment approvals for given signer -- sorted by expiration date first, then by creation date', async () => { + const newApprovalWithNoExpirationId = ( + await turbo.createDelegatedPaymentApproval({ + approvedWincAmount: '100', + approvedAddress: unfundedSignerAddress1, + }) + ).id; + const approvalWithFarExpirationId = ( + await turbo.createDelegatedPaymentApproval({ + approvedWincAmount: '100', + approvedAddress: unfundedSignerAddress1, + expiresBySeconds: 10000, + }) + ).id; + const approvalWithNearExpirationId = ( + await turbo.createDelegatedPaymentApproval({ + approvedWincAmount: '100', + approvedAddress: unfundedSignerAddress1, + expiresBySeconds: 10, + }) + ).id; + + const { givenApprovals } = await turbo.getDelegatedPaymentApprovals(); + expect(givenApprovals).to.have.length(4); + expect(givenApprovals[0].approvalDataItemId).to.equal( + approvalWithNearExpirationId, + ); + expect(givenApprovals[1].approvalDataItemId).to.equal( + approvalWithFarExpirationId, + ); + expect(givenApprovals[2].approvalDataItemId).to.equal(oldestApprovalId); + expect(givenApprovals[3].approvalDataItemId).to.equal( + newApprovalWithNoExpirationId, + ); + }); + }); + + describe('revokeDelegatedPaymentApprovals', () => { + it('should properly revoke all delegated payment approvals for given address', async () => { + const { givenApprovals } = await turbo.getBalance(); + expect(givenApprovals).to.have.length(4); + + await turbo.revokeDelegatedPaymentApprovals({ + revokedAddress: unfundedSignerAddress1, + }); + + const { givenApprovals: givenApprovalsLater } = await turbo.getBalance(); + expect(givenApprovalsLater).to.have.length(0); + }); + + it('should fail to revoke if there are no delegated payment approvals for given address', async () => { + await expectAsyncErrorThrow({ + promiseToError: turbo.revokeDelegatedPaymentApprovals({ + revokedAddress: 'stub-43-char-address-stub-43-char-address-0', + }), + errorMessage: + 'Failed request: 400: Unable to revoke delegated payment approvals!', + errorType: 'FailedRequestError', + }); + }); + }); + + describe('using delegated payment approvals', () => { + let signerJwk: JWKInterface; + let payingJwk: JWKInterface; + + let signerTurbo: TurboAuthenticatedClient; + let payingTurbo: TurboAuthenticatedClient; + + let signerAddress: UserAddress; + let payingAddress: UserAddress; + + before(async () => { + signerJwk = await Arweave.crypto.generateJWK(); + signerTurbo = TurboFactory.authenticated({ + ...arweaveTestConfig, + privateKey: signerJwk, + }); + signerAddress = jwkToPublicArweaveAddress(signerJwk); + + payingJwk = await Arweave.crypto.generateJWK(); + payingTurbo = TurboFactory.authenticated({ + ...arweaveTestConfig, + privateKey: payingJwk, + }); + payingAddress = jwkToPublicArweaveAddress(payingJwk); + + await fundArLocalWalletAddress(jwkToPublicArweaveAddress(payingJwk)); + const id = await sendFundTransaction(1_000_000_000_000, payingJwk); + await mineArLocalBlock(25); + await payingTurbo.submitFundTransaction({ + txId: id, + }); + + await payingTurbo.createDelegatedPaymentApproval({ + approvedWincAmount: 766_000_000_000, + approvedAddress: signerAddress, + }); + + const payerBalance = await payingTurbo.getBalance(); + expect(payerBalance.winc).to.equal('0'); + expect(+payerBalance.controlledWinc).to.equal(766_000_000_000); + + const signerBalance = await signerTurbo.getBalance(); + expect(signerBalance.winc).to.equal('0'); + expect(+signerBalance.effectiveBalance).to.equal(766_000_000_000); + }); + + it('should properly use a delegated payment approvals to upload data when paid-by is provided', async () => { + const filePath = new URL('files/1MB_file', import.meta.url).pathname; + const fileSize = statSync(filePath).size; + + const { winc } = await signerTurbo.uploadFile({ + dataItemOpts: { paidBy: payingAddress }, + fileStreamFactory: () => createReadStream(filePath), + fileSizeFactory: () => fileSize, + }); + + expect(winc).to.not.equal('0'); + + const payerBalance = await payingTurbo.getBalance(); + expect(payerBalance.winc).to.equal('0'); + expect(+payerBalance.controlledWinc).to.equal(766_000_000_000 - +winc); + + const signerBalance = await signerTurbo.getBalance(); + expect(signerBalance.winc).to.equal('0'); + expect(+signerBalance.effectiveBalance).to.equal(766_000_000_000 - +winc); + }); + }); +}); diff --git a/tests/helpers.ts b/tests/helpers.ts index 174522ec..eb266c55 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -115,7 +115,8 @@ export const testKyveNativeAddress = 'kyve1xddfun7awnee70xdq5fnt5ja3vxh93v3dj4k8v'; export const base64KyveAddress = 'Rdhf8cqIdoeb7scy9l0d1iVmhu6nmRJIGR-V7YQPKy8'; // cspell:enable -const arweaveUrlString = process.env.ARWEAVE_GATEWAY ?? 'http://localhost:1984'; +export const arweaveUrlString = + process.env.ARWEAVE_GATEWAY ?? 'http://localhost:1984'; const arweaveUrl = new URL(arweaveUrlString); export const testArweave = Arweave.init({ host: arweaveUrl.hostname, @@ -140,7 +141,10 @@ export async function mineArLocalBlock(numBlocks = 1): Promise { await testArweave.api.get(`mine/${numBlocks}`); } -export async function sendFundTransaction(quantity = 1000): Promise { +export async function sendFundTransaction( + quantity = 1000, + jwk = testJwk, +): Promise { const paymentUrl = new URL( turboDevelopmentConfigurations.paymentServiceConfig.url, ); @@ -150,7 +154,7 @@ export async function sendFundTransaction(quantity = 1000): Promise { target, }); - await testArweave.transactions.sign(tx, testJwk); + await testArweave.transactions.sign(tx, jwk); await testArweave.transactions.post(tx); return tx.id; diff --git a/yarn.lock b/yarn.lock index a278e878..e779dea3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3357,7 +3357,7 @@ dotenv-expand@^10.0.0: resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== -dotenv@^16.3.0: +dotenv@^16.3.0, dotenv@^16.4.5: version "16.4.5" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== From 96d4a32fe3d2e84a6dd3742a2d1c981acc786596 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Thu, 24 Oct 2024 16:01:13 -0500 Subject: [PATCH 07/28] feat(delegated payments): use any approvals first by default on CLI PE-6754 --- src/cli/commands/uploadFile.ts | 6 ++++-- src/cli/commands/uploadFolder.ts | 9 +++++++-- src/cli/options.ts | 28 +++++++++++++++++++++------- src/cli/types.ts | 11 ++++++++--- src/cli/utils.ts | 31 +++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 14 deletions(-) diff --git a/src/cli/commands/uploadFile.ts b/src/cli/commands/uploadFile.ts index 3629bd3c..8acdcb3e 100644 --- a/src/cli/commands/uploadFile.ts +++ b/src/cli/commands/uploadFile.ts @@ -17,15 +17,17 @@ import { createReadStream, statSync } from 'fs'; import { turboCliTags } from '../constants.js'; import { UploadFileOptions } from '../types.js'; -import { turboFromOptions } from '../utils.js'; +import { paidByFromOptions, turboFromOptions } from '../utils.js'; export async function uploadFile(options: UploadFileOptions): Promise { - const { filePath, paidBy } = options; + const { filePath } = options; if (filePath === undefined) { throw new Error('Must provide a --file-path to upload'); } const turbo = await turboFromOptions(options); + const paidBy = await paidByFromOptions(options, turbo); + console.log('paidBy', paidBy); const fileSize = statSync(filePath).size; diff --git a/src/cli/commands/uploadFolder.ts b/src/cli/commands/uploadFolder.ts index 24ca60cb..83762e52 100644 --- a/src/cli/commands/uploadFolder.ts +++ b/src/cli/commands/uploadFolder.ts @@ -15,12 +15,17 @@ */ import { turboCliTags } from '../constants.js'; import { UploadFolderOptions } from '../types.js'; -import { getUploadFolderOptions, turboFromOptions } from '../utils.js'; +import { + getUploadFolderOptions, + paidByFromOptions, + turboFromOptions, +} from '../utils.js'; export async function uploadFolder( options: UploadFolderOptions, ): Promise { const turbo = await turboFromOptions(options); + const paidBy = await paidByFromOptions(options, turbo); const { disableManifest, @@ -32,7 +37,7 @@ export async function uploadFolder( const result = await turbo.uploadFolder({ folderPath: folderPath, - dataItemOpts: { tags: [...turboCliTags] }, // TODO: Inject user tags + dataItemOpts: { tags: [...turboCliTags], paidBy }, // TODO: Inject user tags manifestOptions: { disableManifest, indexFile, diff --git a/src/cli/options.ts b/src/cli/options.ts index 4f739a8e..6acf9cae 100644 --- a/src/cli/options.ts +++ b/src/cli/options.ts @@ -129,6 +129,18 @@ export const optionMap = { alias: '--expires-by-seconds ', description: 'Expiration time in seconds', }, + ignoreApprovals: { + alias: '--ignore-approvals', + description: + "Ignore all delegated payment approvals, only use signing wallet's balance", + default: false, + }, + useSignerBalanceFirst: { + alias: '--use-signer-balance-first', + description: + 'Use the signer balance first before using delegated payment approvals', + default: false, + }, } as const; export const walletOptions = [ @@ -148,21 +160,23 @@ export const globalOptions = [ optionMap.uploadUrl, ]; -export const uploadFolderOptions = [ +export const uploadOptions = [ ...walletOptions, + optionMap.paidBy, + optionMap.ignoreApprovals, + optionMap.useSignerBalanceFirst, +]; + +export const uploadFolderOptions = [ + ...uploadOptions, optionMap.folderPath, optionMap.indexFile, optionMap.fallbackFile, optionMap.manifest, optionMap.maxConcurrency, - optionMap.paidBy, ]; -export const uploadFileOptions = [ - ...walletOptions, - optionMap.filePath, - optionMap.paidBy, -]; +export const uploadFileOptions = [...uploadOptions, optionMap.filePath]; export const createApprovalOptions = [ ...walletOptions, diff --git a/src/cli/types.ts b/src/cli/types.ts index 8d6fa61b..ecc1b385 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -40,7 +40,13 @@ export type TopUpOptions = AddressOptions & { currency: string | undefined; }; -export type UploadFolderOptions = WalletOptions & { +export type UploadOptions = WalletOptions & { + paidBy: string[]; + ignoreApprovals: boolean; + useSignerBalanceFirst: boolean; +}; + +export type UploadFolderOptions = UploadOptions & { folderPath: string | undefined; indexFile: string | undefined; fallbackFile: string | undefined; @@ -48,9 +54,8 @@ export type UploadFolderOptions = WalletOptions & { maxConcurrency: number | undefined; }; -export type UploadFileOptions = WalletOptions & { +export type UploadFileOptions = UploadOptions & { filePath: string | undefined; - paidBy: string[]; }; export type PriceOptions = GlobalOptions & { diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 5cc72ea3..e34b3ae3 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -32,6 +32,7 @@ import { AddressOptions, GlobalOptions, UploadFolderOptions, + UploadOptions, WalletOptions, } from './types.js'; @@ -222,6 +223,36 @@ export async function turboFromOptions( }); } +export async function paidByFromOptions( + { + paidBy: paidByCliInput, + ignoreApprovals, + useSignerBalanceFirst, + }: UploadOptions, + turbo: TurboAuthenticatedClient, +): Promise { + const paidBy = await (async () => { + if (paidByCliInput !== undefined && paidByCliInput.length > 0) { + return paidByCliInput; + } + if (ignoreApprovals) { + return undefined; + } + const { receivedApprovals } = await turbo.getBalance(); + if (receivedApprovals.length !== 0) { + return receivedApprovals.map((approval) => approval.payingAddress); + } + return undefined; + })(); + + if (paidBy !== undefined && useSignerBalanceFirst) { + // Add the signer's address to the front of the paidBy array + paidBy.unshift(await turbo.signer.getNativeAddress()); + } + + return paidBy; +} + export function getUploadFolderOptions(options: UploadFolderOptions): { folderPath: string; indexFile: string | undefined; From 1cccdb737114297d47e95c9281d5bcfc6d1b8f81 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Thu, 24 Oct 2024 16:05:36 -0500 Subject: [PATCH 08/28] docs(delegated payments): add README for paid-by PE-6754 --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 87306054..e0625901 100644 --- a/README.md +++ b/README.md @@ -723,6 +723,8 @@ npx turbo --help #### Options +Global options: + - `-V, --version` - output the version number - `-h, --help` - display help for command - `--dev` - Enable development endpoints (default: false) @@ -731,10 +733,18 @@ npx turbo --help - `--payment-url ` - Set a custom payment service URL - `-t, --token ` - Token type for the command or connected wallet (default: "arweave") +Wallet options: + - `-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 +Upload options: + +- `--paid-by ` - An array of native addresses to pay for the upload +- `--ignore-approvals` - The CLI will normally use any delegated payment approvals for the upload. This flag will ignore any approvals and only use the connected wallet's balance for upload payment. Default: false +- `--use-signer-balance-first` - Use the connected wallet's balance before using any delegated payment approvals for the upload. Default: false + #### Commands ##### `balance` @@ -820,7 +830,7 @@ Command Options: e.g: ```shell -turbo upload-file --file-path '../path/to/my/file.txt' --token ethereum --wallet-file ../path/to/eth/private/key.txt +turbo upload-file --file-path '../path/to/my/file.txt' --token ethereum --wallet-file ../path/to/eth/private/key.txt --paid-by '0x...address' '0x...another-address' ``` ##### `price` From 669dfca76332756e93accf1b0a01cfa8538f9e84 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Thu, 24 Oct 2024 16:24:40 -0500 Subject: [PATCH 09/28] feat(delegated payments): display approvals if they exist on `balance` command PE-6754 --- src/cli/commands/balance.ts | 44 ++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/cli/commands/balance.ts b/src/cli/commands/balance.ts index 37b0f652..cfd5e7d7 100644 --- a/src/cli/commands/balance.ts +++ b/src/cli/commands/balance.ts @@ -19,34 +19,32 @@ 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, givenApprovals, receivedApprovals } = await (async () => { + if (address !== undefined) { + return TurboFactory.unauthenticated(config).getBalance(address); + } + if (privateKey !== undefined) { + throw new Error('Must provide an (--address) or use a valid wallet'); + } + return TurboFactory.authenticated({ + ...config, + privateKey, + }).getBalance(); + })(); - const { winc } = await turbo.getBalance(); console.log( - `Turbo Balance for Wallet Address "${await turbo.signer.getNativeAddress()}"\nCredits: ${ + `Turbo Balance for Native Address "${address}"\nCredits: ${ +winc / 1_000_000_000_000 + }${ + givenApprovals.length > 0 + ? `\nGiven Approvals:\n${JSON.stringify(givenApprovals, null, 2)}` + : '' + }${ + receivedApprovals.length > 0 + ? `\nReceived Approvals:\n${JSON.stringify(receivedApprovals, null, 2)}` + : '' }`, ); } From 35f00e1fcb3787d275da2de459e3cfa0ecb6a03d Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Thu, 24 Oct 2024 16:24:59 -0500 Subject: [PATCH 10/28] docs(delegated payments): add README for create and revoke approval commands PE-6754 --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index e0625901..2ff0ef14 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ Welcome to the `@ardrive/turbo-sdk`! This SDK provides functionality for interac - [`upload-folder`](#upload-folder) - [`upload-file`](#upload-file) - [`price`](#price) + - [`create-approval`](#create-approval) + - [`revoke-approvals`](#revoke-approvals) - [Developers](#developers) - [Requirements](#requirements) - [Setup & Build](#setup--build) @@ -856,6 +858,36 @@ turbo price --value 1024 --type bytes turbo price --value 1.1 --type arweave ``` +##### `create-approval` + +Create a delegated payment approval from the connected wallet to the provided native address and approved winc amount. + +Command Options: + +- `-a, --address ` - Native address to that will receive the delegated payment approval +- `-v, --value ` - Value of winc to create delegated payment approval for +- `-e, --expires-by-seconds ` - Expiry time in seconds for the delegated payment approval + +e.g: + +```shell +turbo create-approval --address 2cor...VUa --value 0.083155650320 --wallet-file ../path/to/my/wallet --expires-by-seconds 3600 +``` + +##### `revoke-approvals` + +Revoke all delegated payment approvals from the connected wallet to the provided native address. + +Command Options: + +- `-a, --address ` - Native address to revoke delegated payment approvals for + +e.g: + +```shell +turbo revoke-approvals --wallet-file ../path/to/my/wallet +``` + ## Developers ### Requirements From 8dc4ec877e4dafb59e36fb533232908e4e5c0877 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Thu, 24 Oct 2024 16:25:58 -0500 Subject: [PATCH 11/28] refactor: clean-up whitespace PE-6754 --- src/common/turbo.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/common/turbo.ts b/src/common/turbo.ts index 5a865e4d..d7697ee4 100644 --- a/src/common/turbo.ts +++ b/src/common/turbo.ts @@ -304,7 +304,6 @@ export class TurboAuthenticatedClient * the Turbo Upload Service, which will verify the signature and forward the admin * action towards the Turbo Payment Service. */ - revokeDelegatedPaymentApprovals( p: TurboRevokeDelegatePaymentApprovalsParams, ): Promise { From 33da73d29b6db6f706f1debd1e83aa5acb5f9ee5 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Thu, 24 Oct 2024 16:49:40 -0500 Subject: [PATCH 12/28] feat(delegated payments): push created/revoked approvals into upload response if they exist PE-6754 --- src/common/turbo.ts | 5 +++-- src/common/upload.ts | 25 +++++++++++++++++++++---- src/types.ts | 10 ++++++++-- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/common/turbo.ts b/src/common/turbo.ts index d7697ee4..9f3aa11f 100644 --- a/src/common/turbo.ts +++ b/src/common/turbo.ts @@ -15,6 +15,7 @@ */ import { Currency, + DelegatedPaymentApproval, GetDelegatedPaymentApprovalsResponse, NativeAddress, TokenType, @@ -294,7 +295,7 @@ export class TurboAuthenticatedClient */ createDelegatedPaymentApproval( p: TurboCreateDelegatedPaymentApprovalParams, - ): Promise { + ): Promise { return this.uploadService.createDelegatedPaymentApproval(p); } @@ -306,7 +307,7 @@ export class TurboAuthenticatedClient */ revokeDelegatedPaymentApprovals( p: TurboRevokeDelegatePaymentApprovalsParams, - ): Promise { + ): Promise { return this.uploadService.revokeDelegatedPaymentApprovals(p); } } diff --git a/src/common/upload.ts b/src/common/upload.ts index a1943009..49515feb 100644 --- a/src/common/upload.ts +++ b/src/common/upload.ts @@ -20,6 +20,7 @@ import { pLimit } from 'plimit-lit'; import { ArweaveManifest, DataItemOptions, + DelegatedPaymentApproval, TokenType, TurboAbortSignal, TurboAuthenticatedUploadServiceConfiguration, @@ -319,7 +320,7 @@ export abstract class TurboAuthenticatedBaseUploadService approvedAddress, approvedWincAmount, expiresBySeconds, - }: TurboCreateDelegatedPaymentApprovalParams): Promise { + }: TurboCreateDelegatedPaymentApprovalParams): Promise { const dataItemOpts = { tags: [ { name: createDelegatedPaymentApprovalTagName, value: approvedAddress }, @@ -336,16 +337,25 @@ export abstract class TurboAuthenticatedBaseUploadService const nonceData = Buffer.from( approvedAddress + approvedWincAmount + Date.now(), ); - return this.uploadFile({ + const { createdApproval, ...uploadResponse } = await this.uploadFile({ fileStreamFactory: () => Readable.from(nonceData), fileSizeFactory: () => nonceData.byteLength, dataItemOpts, }); + if (!createdApproval) { + throw new Error( + 'Failed to create delegated payment approval but upload has succeeded\n' + + JSON.stringify(uploadResponse), + ); + } + return createdApproval; } public async revokeDelegatedPaymentApprovals({ revokedAddress, - }: TurboRevokeDelegatePaymentApprovalsParams): Promise { + }: TurboRevokeDelegatePaymentApprovalsParams): Promise< + DelegatedPaymentApproval[] + > { const dataItemOpts = { tags: [ { @@ -356,10 +366,17 @@ export abstract class TurboAuthenticatedBaseUploadService }; const nonceData = Buffer.from(revokedAddress + Date.now()); - return this.uploadFile({ + const { revokedApprovals, ...uploadResponse } = await this.uploadFile({ fileStreamFactory: () => Readable.from(nonceData), fileSizeFactory: () => nonceData.byteLength, dataItemOpts, }); + if (!revokedApprovals) { + throw new Error( + 'Failed to revoke delegated payment approvals but upload has succeeded\n' + + JSON.stringify(uploadResponse), + ); + } + return revokedApprovals; } } diff --git a/src/types.ts b/src/types.ts index 5a65f1ec..5e2b432f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -207,6 +207,8 @@ export type TurboUploadDataItemResponse = { id: TransactionId; owner: PublicArweaveAddress; winc: string; + createdApproval?: DelegatedPaymentApproval; + revokedApprovals?: DelegatedPaymentApproval[]; }; type UploadFolderParams = { @@ -599,9 +601,11 @@ export type TurboFundWithTokensParams = { export interface TurboAuthenticatedPaymentServiceInterface extends TurboUnauthenticatedPaymentServiceInterface { getBalance: (userAddress?: UserAddress) => Promise; + getDelegatedPaymentApprovals( userAddress?: UserAddress, ): Promise; + topUpWithTokens( p: TurboFundWithTokensParams, ): Promise; @@ -626,10 +630,11 @@ export interface TurboAuthenticatedUploadServiceInterface createDelegatedPaymentApproval( p: TurboCreateDelegatedPaymentApprovalParams, - ): Promise; + ): Promise; + revokeDelegatedPaymentApprovals( p: TurboRevokeDelegatePaymentApprovalsParams, - ): Promise; + ): Promise; } export interface TurboUnauthenticatedClientInterface @@ -652,6 +657,7 @@ export interface TokenTools { target: string; reward?: string; }>; + pollForTxBeingAvailable: (p: { txId: string }) => Promise; } From 925ee407cc9b5b6674e47fc8036c34cad04a963f Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Thu, 24 Oct 2024 17:03:11 -0500 Subject: [PATCH 13/28] docs(delegated payments): add README for create/revoke/get approvals PE-6754 --- README.md | 36 ++++++++++++++++++++++++++++++++++++ src/common/payment.ts | 18 +++++++++++------- src/common/turbo.ts | 14 ++++++++------ src/types.ts | 12 ++++++------ tests/approval.node.test.ts | 36 +++++++++++++++++++----------------- 5 files changed, 80 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 2ff0ef14..89207855 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ Welcome to the `@ardrive/turbo-sdk`! This SDK provides functionality for interac - [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) + - [`createDelegatedPaymentApproval({ approvedAddress, approvedWincAmount, expiresBySeconds })`](#createdelegatedpaymentapproval-approvedaddress-approvedwincamount-expiresbyseconds-) + - [`revokeDelegatedPaymentApprovals({ approvedAddress })`](#revokedelegatedpaymentapprovals-approvedaddress-) - [CLI](#cli) - [Install CLI](#install-cli) - [CLI Usage](#cli-usage) @@ -679,6 +681,40 @@ const { winc, status, id, ...fundResult } = await turbo.topUpWithTokens({ }); ``` +#### `createDelegatedPaymentApproval({ approvedAddress, approvedWincAmount, expiresBySeconds })` + +Creates a delegated payment approval from the connected wallet to the provided native address and approved winc amount. This action will create a data item for the approval + +```typescript +const { approvalDataItemId, approvedWincAmount } = + await turbo.createDelegatedPaymentApproval({ + approvedAddress: '2cor...VUa', + approvedWincAmount: 0.08315565032, + expiresBySeconds: 3600, + }); +``` + +#### `revokeDelegatedPaymentApprovals({ approvedAddress })` + +Revokes all delegated payment approvals from the connected wallet to the provided native address. + +```typescript +const revokedApprovals = await turbo.revokeDelegatePaymentApprovals({ + approvedAddress: '2cor...VUa', +}); +``` + +#### `getDelegatedPaymentApprovals({ userAddress })` + +Returns all delegated payment approvals from the connected wallet or the provided native address. + +```typescript +const { givenApprovals, receivedApprovals } = + await turbo.getDelegatedPaymentApprovals({ + userAddress: '2cor...VUa', + }); +``` + ## CLI ### Install CLI diff --git a/src/common/payment.ts b/src/common/payment.ts index 50a7e6c3..a5f4510a 100644 --- a/src/common/payment.ts +++ b/src/common/payment.ts @@ -265,9 +265,11 @@ export class TurboUnauthenticatedPaymentService throw new Error('Unknown response from payment service: ' + response); } - public async getDelegatedPaymentApprovals( - userAddress: UserAddress, - ): Promise { + public async getDelegatedPaymentApprovals({ + userAddress, + }: { + userAddress: UserAddress; + }): Promise { return this.httpService.get({ endpoint: `/account/approvals/get?userAddress=${userAddress}`, }); @@ -299,11 +301,13 @@ export class TurboAuthenticatedPaymentService return super.getBalance(userAddress); } - public async getDelegatedPaymentApprovals( - userAddress?: string, - ): Promise { + public async getDelegatedPaymentApprovals({ + userAddress, + }: { + userAddress?: string; + }): Promise { userAddress ??= await this.signer.getNativeAddress(); - return super.getDelegatedPaymentApprovals(userAddress); + return super.getDelegatedPaymentApprovals({ userAddress }); } public async getWincForFiat({ diff --git a/src/common/turbo.ts b/src/common/turbo.ts index 9f3aa11f..621182dd 100644 --- a/src/common/turbo.ts +++ b/src/common/turbo.ts @@ -215,10 +215,10 @@ export class TurboUnauthenticatedClient /** * Returns a list of all delegated payment approvals for the user. */ - getDelegatedPaymentApprovals( - userAddress: NativeAddress, - ): Promise { - return this.paymentService.getDelegatedPaymentApprovals(userAddress); + getDelegatedPaymentApprovals(p: { + userAddress: NativeAddress; + }): Promise { + return this.paymentService.getDelegatedPaymentApprovals(p); } } @@ -251,9 +251,11 @@ export class TurboAuthenticatedClient * Returns a list of all delegated payment approvals for the user. */ getDelegatedPaymentApprovals( - userAddress?: NativeAddress, + p: { + userAddress?: NativeAddress; + } = {}, ): Promise { - return this.paymentService.getDelegatedPaymentApprovals(userAddress); + return this.paymentService.getDelegatedPaymentApprovals(p); } /** diff --git a/src/types.ts b/src/types.ts index 5e2b432f..6c1e9806 100644 --- a/src/types.ts +++ b/src/types.ts @@ -587,9 +587,9 @@ export interface TurboUnauthenticatedPaymentServiceInterface { submitFundTransaction(p: { txId: string; }): Promise; - getDelegatedPaymentApprovals( - userAddress: UserAddress, - ): Promise; + getDelegatedPaymentApprovals(p: { + userAddress: UserAddress; + }): Promise; } export type TurboFundWithTokensParams = { @@ -602,9 +602,9 @@ export interface TurboAuthenticatedPaymentServiceInterface extends TurboUnauthenticatedPaymentServiceInterface { getBalance: (userAddress?: UserAddress) => Promise; - getDelegatedPaymentApprovals( - userAddress?: UserAddress, - ): Promise; + getDelegatedPaymentApprovals(p: { + userAddress?: UserAddress; + }): Promise; topUpWithTokens( p: TurboFundWithTokensParams, diff --git a/tests/approval.node.test.ts b/tests/approval.node.test.ts index e0bd02b9..37e1a1bb 100644 --- a/tests/approval.node.test.ts +++ b/tests/approval.node.test.ts @@ -59,13 +59,14 @@ describe('Delegated Payments', () => { describe('createDelegatedPaymentApproval', () => { it('should properly create a delegated payment approval', async () => { - const { id, owner } = await turbo.createDelegatedPaymentApproval({ - approvedWincAmount: '100', - approvedAddress: unfundedSignerAddress1, - }); - oldestApprovalId = id; - expect(id).to.be.a('string'); - expect(owner).to.equal(arweavePayerAddress); + const { approvalDataItemId, payingAddress } = + await turbo.createDelegatedPaymentApproval({ + approvedWincAmount: '100', + approvedAddress: unfundedSignerAddress1, + }); + oldestApprovalId = approvalDataItemId; + expect(approvalDataItemId).to.be.a('string'); + expect(payingAddress).to.equal(arweavePayerAddress); const balance = await turbo.getBalance(); const { @@ -84,13 +85,14 @@ describe('Delegated Payments', () => { }); it('should properly create a delegated payment approval with expiration, and the approval should expire as expected', async () => { - const { id, owner } = await turbo.createDelegatedPaymentApproval({ - approvedWincAmount: '100', - approvedAddress: unfundedSignerAddress1, - expiresBySeconds: 1, - }); - expect(id).to.be.a('string'); - expect(owner).to.equal(arweavePayerAddress); + const { approvalDataItemId, payingAddress } = + await turbo.createDelegatedPaymentApproval({ + approvedWincAmount: '100', + approvedAddress: unfundedSignerAddress1, + expiresBySeconds: 1, + }); + expect(approvalDataItemId).to.be.a('string'); + expect(payingAddress).to.equal(arweavePayerAddress); const balance = await turbo.getBalance(); const { @@ -155,21 +157,21 @@ describe('Delegated Payments', () => { approvedWincAmount: '100', approvedAddress: unfundedSignerAddress1, }) - ).id; + ).approvalDataItemId; const approvalWithFarExpirationId = ( await turbo.createDelegatedPaymentApproval({ approvedWincAmount: '100', approvedAddress: unfundedSignerAddress1, expiresBySeconds: 10000, }) - ).id; + ).approvalDataItemId; const approvalWithNearExpirationId = ( await turbo.createDelegatedPaymentApproval({ approvedWincAmount: '100', approvedAddress: unfundedSignerAddress1, expiresBySeconds: 10, }) - ).id; + ).approvalDataItemId; const { givenApprovals } = await turbo.getDelegatedPaymentApprovals(); expect(givenApprovals).to.have.length(4); From 2da65dac3d0ff9165803381feaa3749b75ebe398 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Fri, 25 Oct 2024 09:20:30 -0500 Subject: [PATCH 14/28] docs(delegated payments): add README ToC for get approvals PE-6754 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 89207855..128fde4e 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Welcome to the `@ardrive/turbo-sdk`! This SDK provides functionality for interac - [KYVE Crypto Top Up](#kyve-crypto-top-up) - [`createDelegatedPaymentApproval({ approvedAddress, approvedWincAmount, expiresBySeconds })`](#createdelegatedpaymentapproval-approvedaddress-approvedwincamount-expiresbyseconds-) - [`revokeDelegatedPaymentApprovals({ approvedAddress })`](#revokedelegatedpaymentapprovals-approvedaddress-) + - [`getDelegatedPaymentApprovals({ userAddress })`](#getdelegatedpaymentapprovals-useraddress-) - [CLI](#cli) - [Install CLI](#install-cli) - [CLI Usage](#cli-usage) From 049e9907e858cd3e4a7a7e30cd9e293d36ca1699 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Fri, 25 Oct 2024 11:25:03 -0500 Subject: [PATCH 15/28] refactor(delegated payments): gracefully handle recievedApprovals in CLI layer PE-6754 --- src/cli/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cli/utils.ts b/src/cli/utils.ts index e34b3ae3..00cb6cbd 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -232,14 +232,17 @@ export async function paidByFromOptions( turbo: TurboAuthenticatedClient, ): Promise { const paidBy = await (async () => { + console.log('paidByCliInput', paidByCliInput); if (paidByCliInput !== undefined && paidByCliInput.length > 0) { return paidByCliInput; } if (ignoreApprovals) { + console.log('ignoreApprovals', ignoreApprovals); return undefined; } const { receivedApprovals } = await turbo.getBalance(); - if (receivedApprovals.length !== 0) { + console.log('receivedApprovals', receivedApprovals); + if (receivedApprovals !== undefined && receivedApprovals.length !== 0) { return receivedApprovals.map((approval) => approval.payingAddress); } return undefined; From 2e2bb830c873f9116fa1338569d8a0d78e7c1c23 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Fri, 25 Oct 2024 11:27:42 -0500 Subject: [PATCH 16/28] refactor(delegated payments): excise rogue console.log PE-6754 --- src/cli/commands/uploadFile.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cli/commands/uploadFile.ts b/src/cli/commands/uploadFile.ts index 8acdcb3e..436d3c2f 100644 --- a/src/cli/commands/uploadFile.ts +++ b/src/cli/commands/uploadFile.ts @@ -27,7 +27,6 @@ export async function uploadFile(options: UploadFileOptions): Promise { const turbo = await turboFromOptions(options); const paidBy = await paidByFromOptions(options, turbo); - console.log('paidBy', paidBy); const fileSize = statSync(filePath).size; From 7d1cc0b0880363dfffc15a9a0d0ea8b8548e7364 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Fri, 25 Oct 2024 11:28:55 -0500 Subject: [PATCH 17/28] refactor(delegated payments): excise rogue console.log PE-6754 --- src/cli/utils.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 00cb6cbd..21c105a4 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -232,16 +232,13 @@ export async function paidByFromOptions( turbo: TurboAuthenticatedClient, ): Promise { const paidBy = await (async () => { - console.log('paidByCliInput', paidByCliInput); if (paidByCliInput !== undefined && paidByCliInput.length > 0) { return paidByCliInput; } if (ignoreApprovals) { - console.log('ignoreApprovals', ignoreApprovals); return undefined; } const { receivedApprovals } = await turbo.getBalance(); - console.log('receivedApprovals', receivedApprovals); if (receivedApprovals !== undefined && receivedApprovals.length !== 0) { return receivedApprovals.map((approval) => approval.payingAddress); } From e1c5fb5afb69ca6586b4f61011025601e6da848e Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Fri, 25 Oct 2024 12:38:43 -0500 Subject: [PATCH 18/28] refactor(delegated payments): dedupe on paying addresses for "any" paid-by helper PE-6754 --- src/cli/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 21c105a4..0713ef59 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -240,7 +240,10 @@ export async function paidByFromOptions( } const { receivedApprovals } = await turbo.getBalance(); if (receivedApprovals !== undefined && receivedApprovals.length !== 0) { - return receivedApprovals.map((approval) => approval.payingAddress); + // get unique paying addresses from any received approvals + return Array.from( + new Set(receivedApprovals.map((approval) => approval.payingAddress)), + ); } return undefined; })(); From 79fe7a0b81daeec18a9ca4aa68fe300d71bee009 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Fri, 25 Oct 2024 12:40:18 -0500 Subject: [PATCH 19/28] feat: add cli helper for --local development endpoints PE-6754 --- docker-compose.yml | 2 +- src/cli/commands/balance.ts | 38 ++++++++++++++++++++++--------------- src/cli/options.ts | 8 +++++++- src/cli/types.ts | 1 + src/cli/utils.ts | 9 +++++++++ 5 files changed, 41 insertions(+), 17 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2935e664..51b31f2e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -113,7 +113,7 @@ services: payment-service: # build: - # context: ../payment-service + # context: ../payment-service image: ghcr.io/ardriveapp/payment-service:latest ports: - '4000:4000' diff --git a/src/cli/commands/balance.ts b/src/cli/commands/balance.ts index cfd5e7d7..b814e968 100644 --- a/src/cli/commands/balance.ts +++ b/src/cli/commands/balance.ts @@ -21,28 +21,36 @@ export async function balance(options: AddressOptions) { const config = configFromOptions(options); const { address, privateKey } = await addressOrPrivateKeyFromOptions(options); - const { winc, givenApprovals, receivedApprovals } = await (async () => { - if (address !== undefined) { - return TurboFactory.unauthenticated(config).getBalance(address); - } - if (privateKey !== undefined) { - throw new Error('Must provide an (--address) or use a valid wallet'); - } - return TurboFactory.authenticated({ - ...config, - privateKey, - }).getBalance(); - })(); + const { winc, givenApprovals, receivedApprovals, nativeAddress } = + await (async () => { + if (address !== undefined) { + return { + ...(await TurboFactory.unauthenticated(config).getBalance(address)), + nativeAddress: address, + }; + } + if (privateKey === undefined) { + throw new Error('Must provide an (--address) or use a valid wallet'); + } + const turbo = TurboFactory.authenticated({ + ...config, + privateKey, + }); + return { + ...(await turbo.getBalance()), + nativeAddress: await turbo.signer.getNativeAddress(), + }; + })(); console.log( - `Turbo Balance for Native Address "${address}"\nCredits: ${ + `Turbo Balance for Native Address "${nativeAddress}"\nCredits: ${ +winc / 1_000_000_000_000 }${ - givenApprovals.length > 0 + givenApprovals?.length > 0 ? `\nGiven Approvals:\n${JSON.stringify(givenApprovals, null, 2)}` : '' }${ - receivedApprovals.length > 0 + receivedApprovals?.length > 0 ? `\nReceived Approvals:\n${JSON.stringify(receivedApprovals, null, 2)}` : '' }`, diff --git a/src/cli/options.ts b/src/cli/options.ts index 6acf9cae..c543cc85 100644 --- a/src/cli/options.ts +++ b/src/cli/options.ts @@ -74,7 +74,12 @@ export const optionMap = { }, dev: { alias: '--dev', - description: 'Enable development endpoints', + description: 'Enable Turbo development endpoints', + default: false, + }, + local: { + alias: '--local', + description: 'Enable local development endpoints', default: false, }, debug: { @@ -151,6 +156,7 @@ export const walletOptions = [ export const globalOptions = [ optionMap.dev, + optionMap.local, optionMap.gateway, optionMap.debug, optionMap.quiet, diff --git a/src/cli/types.ts b/src/cli/types.ts index ecc1b385..2e0a9ee4 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -16,6 +16,7 @@ export type GlobalOptions = { dev: boolean; + local: boolean; gateway: string | undefined; debug: boolean; quiet: boolean; diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 0713ef59..6df6ecf1 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -180,11 +180,20 @@ export function configFromOptions( let uploadUrl: string | undefined = undefined; let gatewayUrl: string | undefined = undefined; + if (options.local && options.dev) { + throw new Error('Cannot use both --local and --dev flags'); + } + if (options.dev) { // Use development endpoints paymentUrl = developmentTurboConfiguration.paymentServiceConfig.url; uploadUrl = developmentTurboConfiguration.uploadServiceConfig.url; gatewayUrl = tokenToDevGatewayMap[token]; + } else if (options.local) { + // Use local endpoints + paymentUrl = 'http://localhost:4000'; + uploadUrl = 'http://localhost:3000'; + gatewayUrl = 'http://localhost:1984'; } else { // Use default endpoints paymentUrl = defaultTurboConfiguration.paymentServiceConfig.url; From ee44ef6c1176734a72e5e4b310dcd9468028c779 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Fri, 25 Oct 2024 14:55:09 -0500 Subject: [PATCH 20/28] feat(delegated payments): add list-approvals command rather than overloaded balance command PE-6754 --- src/cli/cli.ts | 13 ++++++ src/cli/commands/balance.ts | 21 ++++----- src/cli/commands/createApproval.ts | 4 +- src/cli/commands/cryptoFund.ts | 3 +- src/cli/commands/listApprovals.ts | 67 +++++++++++++++++++++++++++++ src/cli/commands/price.ts | 3 +- src/cli/commands/revokeApprovals.ts | 6 ++- src/cli/constants.ts | 2 + src/cli/options.ts | 2 + src/cli/types.ts | 2 + 10 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 src/cli/commands/listApprovals.ts diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 8df93bdd..9181e185 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -29,10 +29,12 @@ import { uploadFile, uploadFolder, } from './commands/index.js'; +import { listApprovals } from './commands/listApprovals.js'; import { revokeApprovals } from './commands/revokeApprovals.js'; import { createApprovalOptions, globalOptions, + listApprovalsOptions, optionMap, revokeApprovalsOptions, uploadFileOptions, @@ -116,6 +118,17 @@ applyOptions( await runCommand(command, revokeApprovals); }); +applyOptions( + program + .command('list-approvals') + .description( + 'Lists all Turbo delegated payment approvals for given address or wallet', + ), + listApprovalsOptions, +).action(async (_commandOptions, command: Command) => { + await runCommand(command, listApprovals); +}); + if ( process.argv[1].includes('bin/turbo') || // Running from global .bin process.argv[1].includes('cli/cli') // Running from source diff --git a/src/cli/commands/balance.ts b/src/cli/commands/balance.ts index b814e968..58dce8b3 100644 --- a/src/cli/commands/balance.ts +++ b/src/cli/commands/balance.ts @@ -13,7 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { BigNumber } from 'bignumber.js'; + import { TurboFactory } from '../../node/factory.js'; +import { wincPerCredit } from '../constants.js'; import { AddressOptions } from '../types.js'; import { addressOrPrivateKeyFromOptions, configFromOptions } from '../utils.js'; @@ -21,7 +24,7 @@ export async function balance(options: AddressOptions) { const config = configFromOptions(options); const { address, privateKey } = await addressOrPrivateKeyFromOptions(options); - const { winc, givenApprovals, receivedApprovals, nativeAddress } = + const { effectiveBalance, nativeAddress, winc, controlledWinc } = await (async () => { if (address !== undefined) { return { @@ -43,16 +46,14 @@ export async function balance(options: AddressOptions) { })(); console.log( - `Turbo Balance for Native Address "${nativeAddress}"\nCredits: ${ - +winc / 1_000_000_000_000 - }${ - givenApprovals?.length > 0 - ? `\nGiven Approvals:\n${JSON.stringify(givenApprovals, null, 2)}` - : '' + `Turbo Balance for Native Address "${nativeAddress}"\nEffective Credits: ${ + +effectiveBalance / wincPerCredit }${ - receivedApprovals?.length > 0 - ? `\nReceived Approvals:\n${JSON.stringify(receivedApprovals, null, 2)}` - : '' + winc === controlledWinc + ? '' + : `\nCredits Shared to Other Wallets: ${BigNumber(controlledWinc) + .minus(winc) + .div(wincPerCredit)}` }`, ); } diff --git a/src/cli/commands/createApproval.ts b/src/cli/commands/createApproval.ts index d00c1bd0..9c3ffb7e 100644 --- a/src/cli/commands/createApproval.ts +++ b/src/cli/commands/createApproval.ts @@ -46,5 +46,7 @@ export async function createApproval( expiresBySeconds, }); - console.log('Created approval:', JSON.stringify(result, null, 2)); + console.log( + JSON.stringify({ message: 'Created approval:', ...result }, null, 2), + ); } diff --git a/src/cli/commands/cryptoFund.ts b/src/cli/commands/cryptoFund.ts index 46a61148..43adece0 100644 --- a/src/cli/commands/cryptoFund.ts +++ b/src/cli/commands/cryptoFund.ts @@ -17,6 +17,7 @@ import prompts from 'prompts'; import { tokenToBaseMap } from '../../common/index.js'; import { TurboFactory } from '../../node/factory.js'; +import { wincPerCredit } from '../constants.js'; import { CryptoFundOptions } from '../types.js'; import { configFromOptions, @@ -54,7 +55,7 @@ export async function cryptoFund(options: CryptoFundOptions) { const { winc } = await turbo.getWincForToken({ tokenAmount }); const targetWallet = (await turbo.getTurboCryptoWallets())[token]; - const credits = (+winc / 1_000_000_000_000).toFixed(12); + const credits = (+winc / wincPerCredit).toFixed(12); const { confirm } = await prompts({ type: 'confirm', diff --git a/src/cli/commands/listApprovals.ts b/src/cli/commands/listApprovals.ts new file mode 100644 index 00000000..b2b4adbe --- /dev/null +++ b/src/cli/commands/listApprovals.ts @@ -0,0 +1,67 @@ +/** + * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TurboFactory } from '../../node/factory.js'; +import { ListApprovalsOptions } from '../types.js'; +import { addressOrPrivateKeyFromOptions, configFromOptions } from '../utils.js'; + +export async function listApprovals( + options: ListApprovalsOptions, +): Promise { + const config = configFromOptions(options); + const { address, privateKey } = await addressOrPrivateKeyFromOptions(options); + + const { givenApprovals, receivedApprovals, nativeAddress } = + await (async () => { + if (address !== undefined) { + const approvals = await TurboFactory.unauthenticated( + config, + ).getDelegatedPaymentApprovals({ + userAddress: address, + }); + return { ...approvals, nativeAddress: address }; + } + if (privateKey === undefined) { + throw new Error('Must provide an (--address) or use a valid wallet'); + } + const turbo = TurboFactory.authenticated({ + ...config, + privateKey, + }); + const approvals = await turbo.getDelegatedPaymentApprovals(); + return { + ...approvals, + nativeAddress: await turbo.signer.getNativeAddress(), + }; + })(); + + const hasApprovals = + givenApprovals?.length === 0 && receivedApprovals?.length === 0; + const body = { + message: + `${hasApprovals ? 'No approvals found ' : 'Approvals found'}` + + ` for native address '${nativeAddress}'`, + givenApprovals, + receivedApprovals, + }; + + if (givenApprovals?.length > 0) { + body['givenApprovals'] = givenApprovals; + } + if (receivedApprovals?.length > 0) { + body['receivedApprovals'] = receivedApprovals; + } + console.log(JSON.stringify(body, null, 2)); +} diff --git a/src/cli/commands/price.ts b/src/cli/commands/price.ts index b7710c6f..0651f674 100644 --- a/src/cli/commands/price.ts +++ b/src/cli/commands/price.ts @@ -17,6 +17,7 @@ import { currencyMap } from '../../common/currency.js'; import { isTokenType, tokenToBaseMap } from '../../common/index.js'; import { TurboFactory } from '../../node/factory.js'; import { fiatCurrencyTypes, isCurrency, tokenTypes } from '../../types.js'; +import { wincPerCredit } from '../constants.js'; import { PriceOptions } from '../types.js'; import { configFromOptions } from '../utils.js'; @@ -63,7 +64,7 @@ export async function price(options: PriceOptions) { console.log( `Current price estimate for ${value} ${type} is ~${( - +winc / 1_000_000_000_000 + +winc / wincPerCredit ).toFixed(12)} Credits`, ); } diff --git a/src/cli/commands/revokeApprovals.ts b/src/cli/commands/revokeApprovals.ts index 1c0fe981..1c965430 100644 --- a/src/cli/commands/revokeApprovals.ts +++ b/src/cli/commands/revokeApprovals.ts @@ -22,7 +22,7 @@ export async function revokeApprovals( const { address: revokedAddress } = options; if (revokedAddress === undefined) { throw new Error( - 'Must provide an approved --address to create approval for', + 'Must provide an approved --address to revoke approvals for', ); } @@ -32,5 +32,7 @@ export async function revokeApprovals( revokedAddress, }); - console.log('Created approval:', JSON.stringify(result, null, 2)); + console.log( + JSON.stringify({ message: 'Revoked approvals', ...result }, null, 2), + ); } diff --git a/src/cli/constants.ts b/src/cli/constants.ts index 56e7f56f..940abc3a 100644 --- a/src/cli/constants.ts +++ b/src/cli/constants.ts @@ -20,3 +20,5 @@ export const turboCliTags: { name: string; value: string }[] = [ { name: 'App-Version', value: version }, { name: 'App-Platform', value: process.platform }, ]; + +export const wincPerCredit = 1_000_000_000_000; diff --git a/src/cli/options.ts b/src/cli/options.ts index c543cc85..7b82b7cd 100644 --- a/src/cli/options.ts +++ b/src/cli/options.ts @@ -192,3 +192,5 @@ export const createApprovalOptions = [ ]; export const revokeApprovalsOptions = [...walletOptions, optionMap.address]; + +export const listApprovalsOptions = revokeApprovalsOptions; diff --git a/src/cli/types.ts b/src/cli/types.ts index 2e0a9ee4..4123b477 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -78,3 +78,5 @@ export type CreateApprovalOptions = WalletOptions & { export type RevokeApprovalsOptions = WalletOptions & { address: string | undefined; }; + +export type ListApprovalsOptions = RevokeApprovalsOptions; From 17e215f4445dc192fd14f763f4a5ed30ab254e81 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Mon, 28 Oct 2024 10:55:26 -0500 Subject: [PATCH 21/28] test(delegated payments): update error msg expectation PE-6754 --- tests/approval.node.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/approval.node.test.ts b/tests/approval.node.test.ts index 37e1a1bb..37169e85 100644 --- a/tests/approval.node.test.ts +++ b/tests/approval.node.test.ts @@ -207,7 +207,7 @@ describe('Delegated Payments', () => { revokedAddress: 'stub-43-char-address-stub-43-char-address-0', }), errorMessage: - 'Failed request: 400: Unable to revoke delegated payment approvals!', + 'Failed request: 400: Unable to revoke delegated payment approval !', errorType: 'FailedRequestError', }); }); From 37357abf241d8d7ba0a6dfa145fec5dac340f062 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Mon, 28 Oct 2024 13:51:55 -0500 Subject: [PATCH 22/28] test(delegated payments): improve coverage on new flows PE-6754 --- tests/approval.node.test.ts | 63 +++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/tests/approval.node.test.ts b/tests/approval.node.test.ts index 37169e85..64b743c7 100644 --- a/tests/approval.node.test.ts +++ b/tests/approval.node.test.ts @@ -1,6 +1,7 @@ import Arweave from 'arweave'; import { expect } from 'chai'; import { createReadStream, statSync } from 'node:fs'; +import { restore, stub } from 'sinon'; import { JWKInterface } from '../src/common/jwk.js'; import { TurboAuthenticatedClient } from '../src/common/turbo.js'; @@ -18,6 +19,11 @@ import { } from './helpers.js'; describe('Delegated Payments', () => { + afterEach(() => { + // Restore all stubs + restore(); + }); + let fundedPayerArweaveJwk: JWKInterface; let arweavePayerAddress: UserAddress; @@ -148,6 +154,24 @@ describe('Delegated Payments', () => { errorType: 'FailedRequestError', }); }); + + it('should throw an error when create approval uploadFile succeeds but does not return the created approval', async () => { + stub(turbo['uploadService'], 'uploadFile').resolves({ + winc: '100', + dataCaches: [], + fastFinalityIndexes: [], + id: 'id', + owner: 'owner', + }); + await expectAsyncErrorThrow({ + promiseToError: turbo.createDelegatedPaymentApproval({ + approvedAddress: 'stub-43-char-address-stub-43-char-address-0', + approvedWincAmount: '100', + }), + errorMessage: `Failed to create delegated payment approval but upload has succeeded\n{"winc":"100","dataCaches":[],"fastFinalityIndexes":[],"id":"id","owner":"owner"}`, + errorType: 'Error', + }); + }); }); describe('getDelegatedPaymentApprovals', () => { @@ -211,6 +235,24 @@ describe('Delegated Payments', () => { errorType: 'FailedRequestError', }); }); + + it('should throw an error when revoke uploadFile succeeds but does not return the revoked approvals', async () => { + stub(turbo['uploadService'], 'uploadFile').resolves({ + winc: '100', + dataCaches: [], + fastFinalityIndexes: [], + id: 'id', + owner: 'owner', + }); + await expectAsyncErrorThrow({ + promiseToError: turbo.revokeDelegatedPaymentApprovals({ + revokedAddress: 'stub-43-char-address-stub-43-char-address-0', + }), + errorMessage: + 'Failed to revoke delegated payment approvals but upload has succeeded\n{"winc":"100","dataCaches":[],"fastFinalityIndexes":[],"id":"id","owner":"owner"}', + errorType: 'Error', + }); + }); }); describe('using delegated payment approvals', () => { @@ -259,10 +301,9 @@ describe('Delegated Payments', () => { expect(+signerBalance.effectiveBalance).to.equal(766_000_000_000); }); + const filePath = new URL('files/1MB_file', import.meta.url).pathname; + const fileSize = statSync(filePath).size; it('should properly use a delegated payment approvals to upload data when paid-by is provided', async () => { - const filePath = new URL('files/1MB_file', import.meta.url).pathname; - const fileSize = statSync(filePath).size; - const { winc } = await signerTurbo.uploadFile({ dataItemOpts: { paidBy: payingAddress }, fileStreamFactory: () => createReadStream(filePath), @@ -279,5 +320,21 @@ describe('Delegated Payments', () => { expect(signerBalance.winc).to.equal('0'); expect(+signerBalance.effectiveBalance).to.equal(766_000_000_000 - +winc); }); + + it('should properly use a delegated payment approvals to upload data when multiple paid-bys are provided', async () => { + const payerBalance = await payingTurbo.getBalance(); + + const { winc } = await signerTurbo.uploadFile({ + dataItemOpts: { paidBy: [signerAddress, payingAddress] }, + fileStreamFactory: () => createReadStream(filePath), + fileSizeFactory: () => fileSize, + }); + + expect(winc).to.not.equal('0'); + const payerBalanceLater = await payingTurbo.getBalance(); + expect(+payerBalanceLater.controlledWinc).to.equal( + +payerBalance.controlledWinc - +winc, + ); + }); }); }); From cee5d64881d6652b33f640bc6cf24d5dba077d26 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Mon, 28 Oct 2024 14:01:17 -0500 Subject: [PATCH 23/28] refactor: use safer bignumber.dividedBy for crypto fund confirmation command PE-6754 --- src/cli/cli.ts | 1 + src/cli/commands/cryptoFund.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 9181e185..5b8cbdcd 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -107,6 +107,7 @@ applyOptions( ).action(async (_commandOptions, command: Command) => { await runCommand(command, createApproval); }); + applyOptions( program .command('revoke-approvals') diff --git a/src/cli/commands/cryptoFund.ts b/src/cli/commands/cryptoFund.ts index 43adece0..9bb9f0b8 100644 --- a/src/cli/commands/cryptoFund.ts +++ b/src/cli/commands/cryptoFund.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { BigNumber } from 'bignumber.js'; import prompts from 'prompts'; import { tokenToBaseMap } from '../../common/index.js'; @@ -55,7 +56,7 @@ export async function cryptoFund(options: CryptoFundOptions) { const { winc } = await turbo.getWincForToken({ tokenAmount }); const targetWallet = (await turbo.getTurboCryptoWallets())[token]; - const credits = (+winc / wincPerCredit).toFixed(12); + const credits = BigNumber(winc).dividedBy(wincPerCredit).toFixed(12); const { confirm } = await prompts({ type: 'confirm', From bb30b13426f545900664d3194930d29f8f23a5b9 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Mon, 28 Oct 2024 14:09:17 -0500 Subject: [PATCH 24/28] refactor: remove potential decimals with toFixed for approved winc amount PE-6754 --- src/cli/commands/createApproval.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/createApproval.ts b/src/cli/commands/createApproval.ts index 9c3ffb7e..5d0e8f75 100644 --- a/src/cli/commands/createApproval.ts +++ b/src/cli/commands/createApproval.ts @@ -39,7 +39,7 @@ export async function createApproval( const approvedWincAmount = new BigNumber(creditAmount) .shiftedBy(12) - .toString(); + .toFixed(0); const result = await turbo.createDelegatedPaymentApproval({ approvedAddress, approvedWincAmount, From e2390b4193e83b7e7af5734d2ead98fb3f75317f Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Mon, 28 Oct 2024 14:35:09 -0500 Subject: [PATCH 25/28] test(delegated payments): improve coverage on get approvals PE-6754 --- src/common/payment.ts | 15 ++++++++++++++- tests/approval.node.test.ts | 9 +++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/common/payment.ts b/src/common/payment.ts index a5f4510a..b7709206 100644 --- a/src/common/payment.ts +++ b/src/common/payment.ts @@ -270,9 +270,22 @@ export class TurboUnauthenticatedPaymentService }: { userAddress: UserAddress; }): Promise { - return this.httpService.get({ + const response = await this.httpService.get< + GetDelegatedPaymentApprovalsResponse | undefined + >({ endpoint: `/account/approvals/get?userAddress=${userAddress}`, + allowedStatuses: [200, 404], }); + if ( + response?.givenApprovals === undefined && + response?.receivedApprovals === undefined + ) { + return { + givenApprovals: [], + receivedApprovals: [], + }; + } + return response; } } // NOTE: to avoid redundancy, we use inheritance here - but generally prefer composition over inheritance diff --git a/tests/approval.node.test.ts b/tests/approval.node.test.ts index 64b743c7..db4e45cf 100644 --- a/tests/approval.node.test.ts +++ b/tests/approval.node.test.ts @@ -210,6 +210,15 @@ describe('Delegated Payments', () => { newApprovalWithNoExpirationId, ); }); + + it('should properly get delegated payment approvals when no approvals are present', async () => { + const { givenApprovals } = await TurboFactory.unauthenticated( + {}, + ).getDelegatedPaymentApprovals({ + userAddress: 'stub-43-char-address-stub-43-char-address-0', + }); + expect(givenApprovals).to.have.length(0); + }); }); describe('revokeDelegatedPaymentApprovals', () => { From ba7d86bc9a9a16ec93bbd0408f964b11c8e047bb Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Mon, 28 Oct 2024 16:12:11 -0500 Subject: [PATCH 26/28] refactor: adjust revoke-approvals cli command output PE-6754 --- src/cli/commands/revokeApprovals.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/commands/revokeApprovals.ts b/src/cli/commands/revokeApprovals.ts index 1c965430..f2f4cb9a 100644 --- a/src/cli/commands/revokeApprovals.ts +++ b/src/cli/commands/revokeApprovals.ts @@ -28,11 +28,11 @@ export async function revokeApprovals( const turbo = await turboFromOptions(options); - const result = await turbo.revokeDelegatedPaymentApprovals({ + const revokedApprovals = await turbo.revokeDelegatedPaymentApprovals({ revokedAddress, }); console.log( - JSON.stringify({ message: 'Revoked approvals', ...result }, null, 2), + JSON.stringify({ message: 'Revoked approvals', revokedApprovals }, null, 2), ); } From a6eedd2ede3d8637dc448bbccc82afadfedfa316 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Mon, 28 Oct 2024 16:13:16 -0500 Subject: [PATCH 27/28] refactor: excise extra whitespace in list-aprovals command message PE-6754 --- src/cli/commands/listApprovals.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/listApprovals.ts b/src/cli/commands/listApprovals.ts index b2b4adbe..20de43ee 100644 --- a/src/cli/commands/listApprovals.ts +++ b/src/cli/commands/listApprovals.ts @@ -51,7 +51,7 @@ export async function listApprovals( givenApprovals?.length === 0 && receivedApprovals?.length === 0; const body = { message: - `${hasApprovals ? 'No approvals found ' : 'Approvals found'}` + + `${hasApprovals ? 'No approvals found' : 'Approvals found'}` + ` for native address '${nativeAddress}'`, givenApprovals, receivedApprovals, From b060b7818c180fde125c3b5e6dbe8845b2822d6f Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Wed, 30 Oct 2024 11:17:27 -0500 Subject: [PATCH 28/28] docs(delegated payments): add README section for list approvals PE-6754 --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 128fde4e..90efe0ba 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ Welcome to the `@ardrive/turbo-sdk`! This SDK provides functionality for interac - [`price`](#price) - [`create-approval`](#create-approval) - [`revoke-approvals`](#revoke-approvals) + - [`list-approvals`](#list-approvals) - [Developers](#developers) - [Requirements](#requirements) - [Setup & Build](#setup--build) @@ -925,6 +926,20 @@ e.g: turbo revoke-approvals --wallet-file ../path/to/my/wallet ``` +##### `list-approvals` + +List all given and received delegated payment approvals from the connected wallet or the provided native address. + +Command Options: + +- `-a, --address ` - Native address to list delegated payment approvals for + +e.g: + +```shell +turbo list-approvals --address 2cor...VUa --wallet-file ../path/to/my/wallet +``` + ## Developers ### Requirements