From 4d4727082f408c523be5c2f01d21907c93e2e098 Mon Sep 17 00:00:00 2001 From: Derek Sonnenberg Date: Thu, 12 Dec 2024 10:50:22 -0600 Subject: [PATCH] feat(fund-from): add Fund-From tag to eligible methods/commands PE-7291 --- src/cli/cli.ts | 152 +++----------- src/cli/commands/arNSWriteCommands.ts | 251 +++++++++++++++++++++++ src/cli/commands/gatewayWriteCommands.ts | 10 +- src/cli/commands/readCommands.ts | 16 +- src/cli/commands/transfer.ts | 8 +- src/cli/options.ts | 13 +- src/cli/types.ts | 20 +- src/cli/utils.ts | 75 ++++++- src/common/io.ts | 82 ++++---- src/types/io.ts | 17 +- 10 files changed, 433 insertions(+), 211 deletions(-) create mode 100644 src/cli/commands/arNSWriteCommands.ts diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 7bd329af..46e77e7e 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -21,6 +21,13 @@ import { program } from 'commander'; import { spawnANT } from '../node/index.js'; import { mARIOToken } from '../types/token.js'; import { version } from '../version.js'; +import { + buyRecordCLICommand, + extendLeaseCLICommand, + increaseUndernameLimitCLICommand, + requestPrimaryNameCLICommand, + upgradeRecordCLICommand, +} from './commands/arNSWriteCommands.js'; import { cancelWithdrawal, decreaseDelegateStake, @@ -58,18 +65,15 @@ import { import { transfer } from './commands/transfer.js'; import { addressAndVaultIdOptions, - addressOptions, antStateOptions, + arNSPurchaseOptions, buyRecordOptions, decreaseDelegateStakeOptions, delegateStakeOptions, epochOptions, getVaultOptions, globalOptions, - initiatorOptions, joinNetworkOptions, - nameOptions, - nameWriteOptions, operatorStakeOptions, optionMap, paginationAddressOptions, @@ -84,17 +88,12 @@ import { ANTStateCLIOptions, AddressAndNameCLIOptions, AddressCLIOptions, - BuyRecordCLIOptions, DecreaseDelegateStakeCLIOptions, - ExtendLeaseCLIOptions, - IncreaseUndernameLimitCLIOptions, InitiatorCLIOptions, - NameWriteCLIOptions, PaginationAddressCLIOptions, PaginationCLIOptions, ProcessIdCLIOptions, ProcessIdWriteActionCLIOptions, - UpgradeRecordCLIOptions, } from './types.js'; import { applyOptions, @@ -106,17 +105,13 @@ import { getLoggerFromOptions, makeCommand, paginationParamsFromOptions, - positiveIntegerFromOptions, readANTFromOptions, readARIOFromOptions, - recordTypeFromOptions, requiredAddressFromOptions, requiredAoSignerFromOptions, - requiredPositiveIntegerFromOptions, requiredStringArrayFromOptions, requiredStringFromOptions, writeANTFromOptions, - writeARIOFromOptions, writeActionTagsFromOptions, } from './utils.js'; @@ -156,7 +151,7 @@ makeCommand({ makeCommand({ name: 'get-gateway', description: 'Get the gateway of an address', - options: addressOptions, + options: [optionMap.address], action: getGateway, }); @@ -177,7 +172,7 @@ makeCommand({ makeCommand({ name: 'get-delegations', description: 'Get all stake delegated to gateways from this address', - options: addressOptions, + options: [optionMap.address], action: getDelegations, }); @@ -191,7 +186,7 @@ makeCommand({ makeCommand({ name: 'get-arns-record', description: 'Get an ArNS record by name', - options: nameOptions, + options: [optionMap.name], action: getArNSRecord, }); @@ -205,7 +200,7 @@ makeCommand({ makeCommand({ name: 'get-arns-reserved-name', description: 'Get a reserved ArNS name', - options: nameOptions, + options: [optionMap.name], action: getArNSReservedName, }); @@ -219,7 +214,7 @@ makeCommand({ makeCommand({ name: 'get-arns-returned-name', description: 'Get an ArNS returned name by name', - options: nameOptions, + options: [optionMap.name], action: getArNSReturnedName, }); @@ -304,7 +299,7 @@ makeCommand({ makeCommand({ name: 'get-primary-name-request', description: 'Get primary name request', - options: initiatorOptions, + options: [optionMap.initiator], action: (o) => readARIOFromOptions(o) .getPrimaryNameRequest({ @@ -333,7 +328,7 @@ makeCommand({ makeCommand({ name: 'get-primary-name', description: 'Get primary name', - options: [...addressOptions, optionMap.name], + options: [...[optionMap.address], optionMap.name], action: getPrimaryName, }); @@ -352,7 +347,7 @@ makeCommand({ makeCommand({ name: 'balance', description: 'Get the balance of an address', - options: addressOptions, + options: [optionMap.address], action: (options) => readARIOFromOptions(options) .getBalance({ address: requiredAddressFromOptions(options) }) @@ -380,7 +375,7 @@ makeCommand({ makeCommand({ name: 'get-redelegation-fee', description: 'Get redelegation fee', - options: addressOptions, + options: [optionMap.address], action: (options) => readARIOFromOptions(options).getRedelegationFee({ address: requiredAddressFromOptions(options), @@ -489,124 +484,39 @@ makeCommand({ action: redelegateStake, }); -makeCommand({ +makeCommand({ name: 'buy-record', description: 'Buy a record', options: buyRecordOptions, - action: async (options) => { - const ario = writeARIOFromOptions(options).ario; - const name = requiredStringFromOptions(options, 'name'); - const type = recordTypeFromOptions(options); - const years = positiveIntegerFromOptions(options, 'years'); - - // TODO: Assert balance is sufficient for action - // TODO: Assert record is not already owned - - const processId = options.processId; - if (processId === undefined) { - // TODO: Spawn ANT process, register it to ANT registry, get process ID - throw new Error('Process ID must be provided for buy-record'); - } - - await assertConfirmationPrompt( - `Are you sure you want to ${type} the record ${name}?`, - options, - ); - - return ario.buyRecord({ - name: requiredStringFromOptions(options, 'name'), - processId, - type, - years, - }); - }, + action: buyRecordCLICommand, }); -makeCommand({ +makeCommand({ name: 'upgrade-record', description: 'Upgrade the lease of a record to a permabuy', - options: [...nameOptions, ...writeActionOptions], - // TODO: could assert record is leased by sender, assert balance is sufficient - action: async (options) => { - const name = requiredStringFromOptions(options, 'name'); - await assertConfirmationPrompt( - `Are you sure you want to upgrade the lease of ${name} to a permabuy?`, - options, - ); - return writeARIOFromOptions(options).ario.upgradeRecord({ - name, - }); - }, + options: arNSPurchaseOptions, + action: upgradeRecordCLICommand, }); -makeCommand({ +makeCommand({ name: 'extend-lease', description: 'Extend the lease of a record', - options: [...writeActionOptions, optionMap.name, optionMap.years], - action: async (options) => { - const name = requiredStringFromOptions(options, 'name'); - const years = requiredPositiveIntegerFromOptions(options, 'years'); - - await assertConfirmationPrompt( - `Are you sure you want to extend the lease of ${name} by ${years}?`, - options, - ); - - return writeARIOFromOptions(options).ario.extendLease( - { - name, - years, - }, - writeActionTagsFromOptions(options), - ); - }, + options: [...arNSPurchaseOptions, optionMap.years], + action: extendLeaseCLICommand, }); -makeCommand({ +makeCommand({ name: 'increase-undername-limit', description: 'Increase the limit of a name', - options: [...writeActionOptions, optionMap.name, optionMap.increaseCount], - action: async (options) => { - const name = requiredStringFromOptions(options, 'name'); - const increaseCount = requiredPositiveIntegerFromOptions( - options, - 'increaseCount', - ); - - await assertConfirmationPrompt( - `Are you sure you want to increase the undername limit of ${name} by ${increaseCount}?`, - options, - ); - - return writeARIOFromOptions(options).ario.increaseUndernameLimit( - { - name, - increaseCount, - }, - writeActionTagsFromOptions(options), - ); - }, + options: [...arNSPurchaseOptions, optionMap.increaseCount], + action: increaseUndernameLimitCLICommand, }); -makeCommand({ +makeCommand({ name: 'request-primary-name', description: 'Request a primary name', - options: nameWriteOptions, - action: async (options) => { - // TODO: Assert balance is sufficient for action? - // TODO: Assert name requested is not already owned - // TODO: More assertions? - const name = requiredStringFromOptions(options, 'name'); - - await assertConfirmationPrompt( - `Are you sure you want to request the primary name ${name}?`, - options, - ); - - return writeARIOFromOptions(options).ario.requestPrimaryName({ - name, - }); - }, + options: arNSPurchaseOptions, + action: requestPrimaryNameCLICommand, }); makeCommand({ diff --git a/src/cli/commands/arNSWriteCommands.ts b/src/cli/commands/arNSWriteCommands.ts new file mode 100644 index 00000000..12913088 --- /dev/null +++ b/src/cli/commands/arNSWriteCommands.ts @@ -0,0 +1,251 @@ +/** + * 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 { + AoArNSPurchaseParams, + AoBuyRecordParams, + AoExtendLeaseParams, + AoIncreaseUndernameLimitParams, +} from '../../types/io.js'; +import { CLIWriteOptionsFromAoParams } from '../types.js'; +import { + assertConfirmationPrompt, + assertEnoughBalanceForArNSPurchase, + fundFromFromOptions, + positiveIntegerFromOptions, + recordTypeFromOptions, + requiredPositiveIntegerFromOptions, + requiredStringFromOptions, + writeARIOFromOptions, + writeActionTagsFromOptions, +} from '../utils.js'; + +export async function buyRecordCLICommand( + o: CLIWriteOptionsFromAoParams, +) { + const { ario, signerAddress } = writeARIOFromOptions(o); + const name = requiredStringFromOptions(o, 'name'); + const type = recordTypeFromOptions(o); + const years = positiveIntegerFromOptions(o, 'years'); + const fundFrom = fundFromFromOptions(o); + + const processId = o.processId; + if (processId === undefined) { + // TODO: Spawn ANT process, register it to ANT registry, get process ID + throw new Error('Process ID must be provided for buy-record'); + } + + if (!o.skipConfirmation) { + const existingRecord = await ario.getArNSRecord({ + name, + }); + if (existingRecord !== undefined) { + throw new Error(`ArNS Record ${name} is already owned`); + } + + await assertEnoughBalanceForArNSPurchase({ + ario, + address: signerAddress, + costDetailsParams: { + intent: 'Buy-Record', + type, + name, + years, + fundFrom, + fromAddress: signerAddress, + }, + }); + + await assertConfirmationPrompt( + `Are you sure you want to ${type} the record ${name}?`, + o, + ); + } + + return ario.buyRecord( + { + name: requiredStringFromOptions(o, 'name'), + processId, + type, + years, + fundFrom: fundFromFromOptions(o), + }, + writeActionTagsFromOptions(o), + ); +} + +export async function upgradeRecordCLICommand( + o: CLIWriteOptionsFromAoParams, +) { + const name = requiredStringFromOptions(o, 'name'); + const { ario, signerAddress } = writeARIOFromOptions(o); + const fundFrom = fundFromFromOptions(o); + + if (!o.skipConfirmation) { + const existingRecord = await ario.getArNSRecord({ + name, + }); + if (existingRecord === undefined) { + throw new Error(`ArNS Record ${name} does not exist`); + } + if (existingRecord.type === 'permabuy') { + throw new Error(`ArNS Record ${name} is already a permabuy`); + } + await assertEnoughBalanceForArNSPurchase({ + ario, + address: signerAddress, + costDetailsParams: { + intent: 'Upgrade-Name', + name, + fundFrom, + fromAddress: signerAddress, + }, + }); + + await assertConfirmationPrompt( + `Are you sure you want to upgrade the lease of ${name} to a permabuy?`, + o, + ); + } + return ario.upgradeRecord({ + name, + fundFrom, + }); +} + +export async function extendLeaseCLICommand( + o: CLIWriteOptionsFromAoParams, +) { + const name = requiredStringFromOptions(o, 'name'); + const years = requiredPositiveIntegerFromOptions(o, 'years'); + const fundFrom = fundFromFromOptions(o); + const { ario, signerAddress } = writeARIOFromOptions(o); + + if (!o.skipConfirmation) { + const existingRecord = await ario.getArNSRecord({ + name, + }); + if (existingRecord === undefined) { + throw new Error(`ArNS Record ${name} does not exist`); + } + if (existingRecord.type === 'permabuy') { + throw new Error( + `ArNS Record ${name} is a permabuy and cannot be extended`, + ); + } + + await assertEnoughBalanceForArNSPurchase({ + ario: ario, + address: signerAddress, + costDetailsParams: { + intent: 'Extend-Lease', + name, + years, + fundFrom, + fromAddress: signerAddress, + }, + }); + await assertConfirmationPrompt( + `Are you sure you want to extend the lease of ${name} by ${years}?`, + o, + ); + } + return ario.extendLease( + { + name, + years, + }, + writeActionTagsFromOptions(o), + ); +} + +export async function increaseUndernameLimitCLICommand( + o: CLIWriteOptionsFromAoParams, +) { + const name = requiredStringFromOptions(o, 'name'); + const increaseCount = requiredPositiveIntegerFromOptions(o, 'increaseCount'); + const fundFrom = fundFromFromOptions(o); + const { ario, signerAddress } = writeARIOFromOptions(o); + + if (!o.skipConfirmation) { + const existingRecord = await ario.getArNSRecord({ + name, + }); + if (existingRecord === undefined) { + throw new Error(`ArNS Record ${name} does not exist`); + } + + await assertEnoughBalanceForArNSPurchase({ + ario, + address: signerAddress, + costDetailsParams: { + intent: 'Increase-Undername-Limit', + name, + quantity: increaseCount, + fundFrom, + fromAddress: signerAddress, + }, + }); + + await assertConfirmationPrompt( + `Are you sure you want to increase the undername limit of ${name} by ${increaseCount}?`, + o, + ); + } + + return ario.increaseUndernameLimit( + { + name, + increaseCount, + }, + writeActionTagsFromOptions(o), + ); +} + +export async function requestPrimaryNameCLICommand( + o: CLIWriteOptionsFromAoParams, +) { + const { ario, signerAddress } = writeARIOFromOptions(o); + const fundFrom = fundFromFromOptions(o); + const name = requiredStringFromOptions(o, 'name'); + + if (!o.skipConfirmation) { + // TODO: Assert name requested is not already owned? + // TODO: More assertions? + await assertEnoughBalanceForArNSPurchase({ + ario, + address: signerAddress, + costDetailsParams: { + intent: 'Primary-Name-Request', + name, + fromAddress: signerAddress, + fundFrom, + }, + }); + + await assertConfirmationPrompt( + `Are you sure you want to request the primary name ${name}?`, + o, + ); + } + + return ario.requestPrimaryName( + { + name, + fundFrom, + }, + writeActionTagsFromOptions(o), + ); +} diff --git a/src/cli/commands/gatewayWriteCommands.ts b/src/cli/commands/gatewayWriteCommands.ts index 77c61c63..e920ee12 100644 --- a/src/cli/commands/gatewayWriteCommands.ts +++ b/src/cli/commands/gatewayWriteCommands.ts @@ -28,12 +28,12 @@ import { } from '../types.js'; import { assertConfirmationPrompt, - assertEnoughBalance, + assertEnoughMARIOBalance, formatARIOWithCommas, gatewaySettingsFromOptions, redelegateParamsFromOptions, requiredAddressFromOptions, - requiredMIOFromOptions as requiredMARIOFromOptions, + requiredMARIOFromOptions, requiredStringArrayFromOptions, requiredStringFromOptions, requiredTargetAndQuantityFromOptions, @@ -70,7 +70,11 @@ export async function joinNetwork(options: JoinNetworkCLIOptions) { )} ARIO. Please provide a higher stake.`, ); } - await assertEnoughBalance(ario, signerAddress, mARIOQuantity.toARIO()); + await assertEnoughMARIOBalance({ + ario, + address: signerAddress, + mARIOQuantity, + }); await assertConfirmationPrompt( `Gateway Settings:\n\n${JSON.stringify(settings, null, 2)}\n\nYou are about to stake ${formatARIOWithCommas(mARIOQuantity.toARIO())} ARIO to join the AR.IO network\nAre you sure?\n`, diff --git a/src/cli/commands/readCommands.ts b/src/cli/commands/readCommands.ts index b7ca3522..bbb4d140 100644 --- a/src/cli/commands/readCommands.ts +++ b/src/cli/commands/readCommands.ts @@ -13,11 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - AoGetCostDetailsParams, - fundFromOptions, - isValidFundFrom, -} from '../../types/io.js'; +import { AoGetCostDetailsParams } from '../../types/io.js'; import { mARIOToken } from '../../types/token.js'; import { AddressAndNameCLIOptions, @@ -35,6 +31,7 @@ import { addressFromOptions, epochInputFromOptions, formatARIOWithCommas, + fundFromFromOptions, getTokenCostParamsFromOptions, paginationParamsFromOptions, readARIOFromOptions, @@ -188,16 +185,9 @@ export async function getTokenCost(o: GetTokenCostCLIOptions) { export async function getCostDetails( o: GlobalCLIOptions & CLIOptionsFromAoParams, ) { - if (o.fundFrom !== undefined) { - if (!isValidFundFrom(o.fundFrom)) { - throw new Error( - `Invalid fund from: ${o.fundFrom}. Please use one of ${fundFromOptions.join(', ')}`, - ); - } - } const costDetails = await readARIOFromOptions(o).getCostDetails({ ...getTokenCostParamsFromOptions(o), - fundFrom: o.fundFrom, + fundFrom: fundFromFromOptions(o), }); const output = { diff --git a/src/cli/commands/transfer.ts b/src/cli/commands/transfer.ts index 0e04ead0..c7619c25 100644 --- a/src/cli/commands/transfer.ts +++ b/src/cli/commands/transfer.ts @@ -15,7 +15,7 @@ */ import { TransferCLIOptions } from '../types.js'; import { - assertEnoughBalance, + assertEnoughMARIOBalance, confirmationPrompt, formatARIOWithCommas, requiredTargetAndQuantityFromOptions, @@ -29,7 +29,11 @@ export async function transfer(options: TransferCLIOptions) { const { ario, signerAddress } = writeARIOFromOptions(options); if (!options.skipConfirmation) { - await assertEnoughBalance(ario, signerAddress, arioQuantity); + await assertEnoughMARIOBalance({ + ario, + address: signerAddress, + mARIOQuantity: arioQuantity.toMARIO(), + }); const confirm = await confirmationPrompt( `Are you sure you want to transfer ${formatARIOWithCommas(arioQuantity)} ARIO to ${target}?`, diff --git a/src/cli/options.ts b/src/cli/options.ts index da13d66f..7fb32c8c 100644 --- a/src/cli/options.ts +++ b/src/cli/options.ts @@ -261,13 +261,15 @@ export const globalOptions = [ export const writeActionOptions = [optionMap.skipConfirmation, optionMap.tags]; -export const addressOptions = [optionMap.address]; -export const nameOptions = [optionMap.name]; -export const initiatorOptions = [optionMap.initiator]; +export const arNSPurchaseOptions = [ + ...writeActionOptions, + optionMap.name, + optionMap.fundFrom, +]; export const epochOptions = [optionMap.epochIndex, optionMap.timestamp]; -export const addressAndVaultIdOptions = [...addressOptions, optionMap.vaultId]; +export const addressAndVaultIdOptions = [optionMap.address, optionMap.vaultId]; export const nameWriteOptions = [...writeActionOptions, optionMap.name]; @@ -337,8 +339,7 @@ export const joinNetworkOptions = [ ]; export const buyRecordOptions = [ - ...writeActionOptions, - optionMap.name, + ...arNSPurchaseOptions, optionMap.quantity, optionMap.type, optionMap.years, diff --git a/src/cli/types.ts b/src/cli/types.ts index fd070b44..aa604bf3 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -16,10 +16,7 @@ import { AoAddressParams, AoArNSNameParams, - AoBuyRecordParams, - AoExtendLeaseParams, AoGetVaultParams, - AoIncreaseUndernameLimitParams, AoJoinNetworkParams, AoTokenCostParams, PaginationParams, @@ -77,6 +74,12 @@ export type CLIOptionsFromAoParams = { T[K]; }; +export type CLIReadOptionsFromAoParams = CLIOptionsFromAoParams & + GlobalCLIOptions; + +export type CLIWriteOptionsFromAoParams = WriteActionCLIOptions & + CLIOptionsFromAoParams; + export type PaginationCLIOptions = GlobalCLIOptions & CLIOptionsFromAoParams; @@ -148,17 +151,6 @@ export type DecreaseDelegateStakeCLIOptions = DelegateStakeCLIOptions & { instant: boolean; }; -export type BuyRecordCLIOptions = WriteActionCLIOptions & - CLIOptionsFromAoParams; - -export type UpgradeRecordCLIOptions = NameWriteCLIOptions; - -export type ExtendLeaseCLIOptions = WriteActionCLIOptions & - CLIOptionsFromAoParams; - -export type IncreaseUndernameLimitCLIOptions = WriteActionCLIOptions & - CLIOptionsFromAoParams; - export type ANTStateCLIOptions = WriteActionCLIOptions & { target?: string; keywords?: string[]; diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 2143e7c2..96574285 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -30,19 +30,23 @@ import { AoANTWrite, AoARIORead, AoARIOWrite, + AoGetCostDetailsParams, AoRedelegateStakeParams, AoSigner, AoUpdateGatewaySettingsParams, ArweaveSigner, ContractSigner, EpochInput, + FundFrom, Logger, PaginationParams, SpawnANTState, WriteOptions, createAoSigner, fromB64Url, + fundFromOptions, initANTStateForAddress, + isValidFundFrom, isValidIntent, mARIOToken, sha256B64Url, @@ -249,6 +253,10 @@ export function formatARIOWithCommas(value: ARIOToken): string { return integerWithCommas + '.' + decimalPart; } +export function formatMARIOToARIOWithCommas(value: mARIOToken): string { + return formatARIOWithCommas(value.toARIO()); +} + /** helper to get address from --address option first, then check wallet options */ export function addressFromOptions( options: O, @@ -421,7 +429,7 @@ export function recordTypeFromOptions( return options.type; } -export function requiredMIOFromOptions( +export function requiredMARIOFromOptions( options: O, key: string, ): mARIOToken { @@ -431,16 +439,52 @@ export function requiredMIOFromOptions( return new ARIOToken(+options[key]).toMARIO(); } -export async function assertEnoughBalance( - ario: AoARIORead, - address: string, - arioQuantity: ARIOToken, -) { +export async function assertEnoughBalanceForArNSPurchase({ + ario, + address, + costDetailsParams, +}: { + ario: AoARIORead; + address: string; + costDetailsParams: AoGetCostDetailsParams; +}) { + const costDetails = await ario.getCostDetails(costDetailsParams); + if (costDetails.fundingPlan) { + if (costDetails.fundingPlan.shortfall > 0) { + throw new Error( + `Insufficient balance for action. Shortfall: ${formatMARIOToARIOWithCommas( + new mARIOToken(costDetails.fundingPlan.shortfall), + )}\n${JSON.stringify(costDetails, null, 2)}`, + ); + } + } else { + await assertEnoughMARIOBalance({ + ario, + address, + mARIOQuantity: costDetails.tokenCost, + }); + } +} + +export async function assertEnoughMARIOBalance({ + address, + ario, + mARIOQuantity, +}: { + ario: AoARIORead; + address: string; + mARIOQuantity: mARIOToken | number; +}) { + if (typeof mARIOQuantity === 'number') { + mARIOQuantity = new mARIOToken(mARIOQuantity); + } const balance = await ario.getBalance({ address }); - if (balance < arioQuantity.toMARIO().valueOf()) { + if (balance < mARIOQuantity.valueOf()) { throw new Error( - `Insufficient ARIO balance for action. Balance available: ${new mARIOToken(balance).toARIO()} ARIO`, + `Insufficient ARIO balance for action. Balance available: ${formatMARIOToARIOWithCommas( + new mARIOToken(balance), + )} ARIO`, ); } } @@ -588,3 +632,18 @@ export function getTokenCostParamsFromOptions(o: GetTokenCostCLIOptions) { fromAddress: addressFromOptions(o), }; } + +export function fundFromFromOptions< + O extends { + fundFrom?: string; + }, +>(o: O): FundFrom { + if (o.fundFrom !== undefined) { + if (!isValidFundFrom(o.fundFrom)) { + throw new Error( + `Invalid fund from: ${o.fundFrom}. Please use one of ${fundFromOptions.join(', ')}`, + ); + } + } + return o.fundFrom ?? 'balance'; +} diff --git a/src/common/io.ts b/src/common/io.ts index 73dc4564..7096ee13 100644 --- a/src/common/io.ts +++ b/src/common/io.ts @@ -46,15 +46,19 @@ import { AoARIORead, AoARIOWrite, AoArNSNameData, + AoArNSPurchaseParams, AoArNSReservedNameDataWithName, + AoBuyRecordParams, AoDelegation, AoEpochData, AoEpochSettings, + AoExtendLeaseParams, AoGateway, AoGatewayDelegateWithAddress, AoGatewayRegistrySettings, AoGatewayVault, AoGetCostDetailsParams, + AoIncreaseUndernameLimitParams, AoPaginatedAddressParams, AoRegistrationFees, AoTokenCostParams, @@ -1065,12 +1069,7 @@ export class ARIOWriteable extends ARIOReadable implements AoARIOWrite { } async buyRecord( - params: { - name: string; - years?: number; - type: 'lease' | 'permabuy'; - processId: string; - }, + params: AoBuyRecordParams, options?: WriteOptions, ): Promise { const { tags = [] } = options || {}; @@ -1081,6 +1080,7 @@ export class ARIOWriteable extends ARIOReadable implements AoARIOWrite { { name: 'Years', value: params.years?.toString() ?? '1' }, { name: 'Process-Id', value: params.processId }, { name: 'Purchase-Type', value: params.type || 'lease' }, + { name: 'Fund-From', value: params.fundFrom }, ]; return this.process.send({ @@ -1098,19 +1098,19 @@ export class ARIOWriteable extends ARIOReadable implements AoARIOWrite { * @returns {Promise} The result of the upgrade */ async upgradeRecord( - params: { - name: string; - }, + params: AoArNSPurchaseParams, options?: WriteOptions, ): Promise { const { tags = [] } = options || {}; + const allTags = [ + ...tags, + { name: 'Action', value: 'Upgrade-Name' }, // TODO: align on Update-Record vs. Upgrade-Name (contract currently uses Upgrade-Name) + { name: 'Name', value: params.name }, + { name: 'Fund-From', value: params.fundFrom }, + ]; return this.process.send({ signer: this.signer, - tags: [ - ...tags, - { name: 'Action', value: 'Upgrade-Name' }, // TODO: align on Update-Record vs. Upgrade-Name (contract currently uses Upgrade-Name) - { name: 'Name', value: params.name }, - ], + tags: pruneTags(allTags), }); } @@ -1124,40 +1124,38 @@ export class ARIOWriteable extends ARIOReadable implements AoARIOWrite { * @returns {Promise} The result of the extension */ async extendLease( - params: { - name: string; - years: number; - }, + params: AoExtendLeaseParams, options?: WriteOptions, ): Promise { const { tags = [] } = options || {}; + const allTags = [ + ...tags, + { name: 'Action', value: 'Extend-Lease' }, + { name: 'Name', value: params.name }, + { name: 'Years', value: params.years.toString() }, + { name: 'Fund-From', value: params.fundFrom }, + ]; return this.process.send({ signer: this.signer, - tags: [ - ...tags, - { name: 'Action', value: 'Extend-Lease' }, - { name: 'Name', value: params.name }, - { name: 'Years', value: params.years.toString() }, - ], + tags: pruneTags(allTags), }); } async increaseUndernameLimit( - params: { - name: string; - increaseCount: number; - }, + params: AoIncreaseUndernameLimitParams, options?: WriteOptions, ): Promise { const { tags = [] } = options || {}; + const allTags = [ + ...tags, + { name: 'Action', value: 'Increase-Undername-Limit' }, + { name: 'Name', value: params.name }, + { name: 'Quantity', value: params.increaseCount.toString() }, + { name: 'Fund-From', value: params.fundFrom }, + ]; return this.process.send({ signer: this.signer, - tags: [ - ...tags, - { name: 'Action', value: 'Increase-Undername-Limit' }, - { name: 'Name', value: params.name }, - { name: 'Quantity', value: params.increaseCount.toString() }, - ], + tags: pruneTags(allTags), }); } @@ -1189,13 +1187,19 @@ export class ARIOWriteable extends ARIOReadable implements AoARIOWrite { }); } - async requestPrimaryName(params: { name: string }): Promise { + async requestPrimaryName( + params: AoArNSPurchaseParams, + options?: WriteOptions, + ): Promise { + const { tags = [] } = options || {}; + const allTags = [ + ...tags, + { name: 'Action', value: 'Request-Primary-Name' }, + { name: 'Name', value: params.name }, + ]; return this.process.send({ signer: this.signer, - tags: [ - { name: 'Action', value: 'Request-Primary-Name' }, - { name: 'Name', value: params.name }, - ], + tags: pruneTags(allTags), }); } diff --git a/src/types/io.ts b/src/types/io.ts index bfc25f7a..4800cf4b 100644 --- a/src/types/io.ts +++ b/src/types/io.ts @@ -393,17 +393,21 @@ export type AoGetVaultParams = { vaultId: string; }; -export type AoBuyRecordParams = AoArNSNameParams & { +export type AoArNSPurchaseParams = AoArNSNameParams & { + fundFrom?: FundFrom; +}; + +export type AoBuyRecordParams = AoArNSPurchaseParams & { years?: number; type: 'lease' | 'permabuy'; processId: string; }; -export type AoExtendLeaseParams = AoArNSNameParams & { +export type AoExtendLeaseParams = AoArNSPurchaseParams & { years: number; }; -export type AoIncreaseUndernameLimitParams = AoArNSNameParams & { +export type AoIncreaseUndernameLimitParams = AoArNSPurchaseParams & { increaseCount: number; }; @@ -624,7 +628,7 @@ export interface AoARIOWrite extends AoARIORead { options?: WriteOptions, ): Promise; upgradeRecord( - params: AoArNSNameParams, + params: AoArNSPurchaseParams, options?: WriteOptions, ): Promise; extendLease( @@ -642,7 +646,10 @@ export interface AoARIOWrite extends AoARIORead { }, options?: WriteOptions, ): Promise; - requestPrimaryName(params: { name: string }): Promise; + requestPrimaryName( + params: AoArNSPurchaseParams, + options?: WriteOptions, + ): Promise; redelegateStake( params: AoRedelegateStakeParams, options?: WriteOptions,