UNPKG

@ar.io/sdk

Version:

[![codecov](https://codecov.io/gh/ar-io/ar-io-sdk/graph/badge.svg?token=7dXKcT7dJy)](https://codecov.io/gh/ar-io/ar-io-sdk)

1,312 lines (1,311 loc) 49.5 kB
/** * 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 { connect } from '@permaweb/aoconnect'; import { ANT_REGISTRY_ID, ARIO_MAINNET_PROCESS_ID, ARIO_TESTNET_PROCESS_ID, } from '../constants.js'; import { isProcessConfiguration, isProcessIdConfiguration, } from '../types/index.js'; import { createAoSigner } from '../utils/ao.js'; import { getEpochDataFromGqlWithCUFallback, paginationParamsToTags, pruneTags, removeEligibleRewardsFromEpochData, sortAndPaginateEpochDataIntoEligibleDistributions, } from '../utils/arweave.js'; import { ANTRegistry } from './ant-registry.js'; import { ANT } from './ant.js'; import { defaultArweave } from './arweave.js'; import { AOProcess } from './contracts/ao-process.js'; import { InvalidContractConfigurationError } from './error.js'; import { createFaucet } from './faucet.js'; import { Logger } from './logger.js'; import { TurboArNSPaymentFactory, TurboArNSPaymentProviderAuthenticated, isTurboArNSSigner, } from './turbo.js'; export class ARIO { // Implementation static init(config) { if (config !== undefined && 'signer' in config) { return new ARIOWriteable(config); } return new ARIOReadable(config); } static mainnet(config) { if (config !== undefined && 'signer' in config) { return new ARIOWriteable({ ...config, process: new AOProcess({ processId: ARIO_MAINNET_PROCESS_ID, ao: connect({ MODE: 'legacy', CU_URL: 'https://cu.ardrive.io', ...config?.process?.ao, }), }), }); } return new ARIOReadable({ ...config, process: new AOProcess({ processId: ARIO_MAINNET_PROCESS_ID, ao: connect({ CU_URL: 'https://cu.ardrive.io', MODE: 'legacy', ...config?.process?.ao, }), }), }); } static testnet(config) { if (config !== undefined && 'signer' in config) { return createFaucet({ arioInstance: new ARIOWriteable({ ...config, process: new AOProcess({ processId: ARIO_TESTNET_PROCESS_ID, ao: connect({ MODE: 'legacy', CU_URL: 'https://cu.ardrive.io', ...config?.process?.ao, }), }), }), faucetApiUrl: config?.faucetUrl, }); } return createFaucet({ arioInstance: new ARIOReadable({ ...config, process: new AOProcess({ processId: ARIO_TESTNET_PROCESS_ID, ao: connect({ MODE: 'legacy', CU_URL: 'https://cu.ardrive.io', ...config?.process?.ao, }), }), }), faucetApiUrl: config?.faucetUrl, }); } } export class ARIOReadable { process; epochSettings; arweave; hyperbeamUrl; paymentProvider; // TODO: this could be an array/map of payment providers logger = Logger.default; constructor(config) { this.arweave = config?.arweave ?? defaultArweave; this.hyperbeamUrl = config?.hyperbeamUrl; if (config === undefined || Object.keys(config).length === 0) { this.process = new AOProcess({ processId: ARIO_MAINNET_PROCESS_ID, }); } else if (isProcessConfiguration(config)) { this.process = config.process; } else if (isProcessIdConfiguration(config)) { this.process = new AOProcess({ processId: config.processId, }); } else { throw new InvalidContractConfigurationError(); } this.paymentProvider = TurboArNSPaymentFactory.init({ paymentUrl: config?.paymentUrl, }); } async getInfo() { return this.process.read({ tags: [{ name: 'Action', value: 'Info' }], }); } async getTokenSupply() { return this.process.read({ tags: [{ name: 'Action', value: 'Total-Token-Supply' }], }); } async computeEpochIndexForTimestamp(timestamp) { const epochSettings = await this.getEpochSettings(); const epochZeroStartTimestamp = epochSettings.epochZeroStartTimestamp; const epochLengthMs = epochSettings.durationMs; return Math.floor((timestamp - epochZeroStartTimestamp) / epochLengthMs); } async computeCurrentEpochIndex() { return this.computeEpochIndexForTimestamp(Date.now()); } async computeEpochIndex(params) { const epochIndex = params?.epochIndex; if (epochIndex !== undefined) { return epochIndex; } const timestamp = params?.timestamp; if (timestamp !== undefined) { return this.computeEpochIndexForTimestamp(timestamp); } return undefined; } async getEpochSettings() { return (this.epochSettings ??= await this.process.read({ tags: [{ name: 'Action', value: 'Epoch-Settings' }], })); } async getEpoch(epoch) { const epochIndex = await this.computeEpochIndex(epoch); const currentIndex = await this.computeCurrentEpochIndex(); if (epochIndex !== undefined && epochIndex < currentIndex) { const epochData = await getEpochDataFromGqlWithCUFallback({ arweave: this.arweave, epochIndex: epochIndex, processId: this.process.processId, ao: this.process.ao, }); if (!epochData) { throw new Error('Epoch data not found for epoch index ' + epochIndex); } return removeEligibleRewardsFromEpochData(epochData); } // go to the process epoch and fetch the epoch data const allTags = [ { name: 'Action', value: 'Epoch' }, { name: 'Epoch-Index', value: currentIndex.toString(), }, ]; return this.process.read({ tags: pruneTags(allTags), }); } async getArNSRecord({ name }) { return this.process.read({ tags: [ { name: 'Action', value: 'Record' }, { name: 'Name', value: name }, ], }); } async getArNSRecords(params) { return this.process.read({ tags: [ { name: 'Action', value: 'Paginated-Records' }, ...paginationParamsToTags(params), ], }); } async getArNSReservedNames(params) { return this.process.read({ tags: [ { name: 'Action', value: 'Reserved-Names' }, ...paginationParamsToTags(params), ], }); } async getArNSReservedName({ name, }) { return this.process.read({ tags: [ { name: 'Action', value: 'Reserved-Name' }, { name: 'Name', value: name }, ], }); } async getBalance({ address }) { return this.process.read({ tags: [ { name: 'Action', value: 'Balance' }, { name: 'Address', value: address }, ], }); } async getBalances(params) { return this.process.read({ tags: [ { name: 'Action', value: 'Paginated-Balances' }, ...paginationParamsToTags(params), ], }); } async getVault({ address, vaultId, }) { return this.process.read({ tags: [ { name: 'Action', value: 'Vault' }, { name: 'Address', value: address }, { name: 'Vault-Id', value: vaultId }, ], }); } async getVaults(params) { return this.process.read({ tags: [ { name: 'Action', value: 'Paginated-Vaults' }, ...paginationParamsToTags(params), ], }); } async getGateway({ address, }) { return this.process.read({ tags: [ { name: 'Action', value: 'Gateway' }, { name: 'Address', value: address }, ], }); } async getGatewayDelegates({ address, ...pageParams }) { return this.process.read({ tags: [ { name: 'Action', value: 'Paginated-Delegates' }, { name: 'Address', value: address }, ...paginationParamsToTags(pageParams), ], }); } async getGatewayDelegateAllowList({ address, ...pageParams }) { return this.process.read({ tags: [ { name: 'Action', value: 'Paginated-Allowed-Delegates' }, { name: 'Address', value: address }, ...paginationParamsToTags(pageParams), ], }); } async getGateways(pageParams) { return this.process.read({ tags: [ { name: 'Action', value: 'Paginated-Gateways' }, ...paginationParamsToTags(pageParams), ], }); } async getCurrentEpoch() { return this.process.read({ tags: [{ name: 'Action', value: 'Epoch' }], }); } async getPrescribedObservers(epoch) { const epochIndex = await this.computeEpochIndex(epoch); const currentIndex = await this.computeCurrentEpochIndex(); if (epochIndex !== undefined && epochIndex < currentIndex) { const epochData = await getEpochDataFromGqlWithCUFallback({ ao: this.process.ao, arweave: this.arweave, epochIndex: epochIndex, processId: this.process.processId, }); if (!epochData) { throw new Error('Epoch data not found for epoch index ' + epochIndex); } return epochData.prescribedObservers; } const allTags = [ { name: 'Action', value: 'Epoch-Prescribed-Observers' }, { name: 'Epoch-Index', value: currentIndex.toString(), }, ]; return this.process.read({ tags: pruneTags(allTags), }); } async getPrescribedNames(epoch) { const epochIndex = await this.computeEpochIndex(epoch); const currentIndex = await this.computeCurrentEpochIndex(); if (epochIndex !== undefined && epochIndex < currentIndex) { const epochData = await getEpochDataFromGqlWithCUFallback({ arweave: this.arweave, epochIndex: epochIndex, processId: this.process.processId, ao: this.process.ao, }); if (!epochData) { throw new Error('Epoch data not found for epoch index ' + epochIndex); } return epochData.prescribedNames; } const allTags = [ { name: 'Action', value: 'Epoch-Prescribed-Names' }, { name: 'Epoch-Index', value: currentIndex.toString(), }, ]; return this.process.read({ tags: pruneTags(allTags), }); } // we need to find the epoch index for the epoch that is currently being distributed and fetch it from gql async getObservations(epoch) { const epochIndex = await this.computeEpochIndex(epoch); const currentIndex = await this.computeCurrentEpochIndex(); if (epochIndex !== undefined && epochIndex < currentIndex) { const epochData = await getEpochDataFromGqlWithCUFallback({ arweave: this.arweave, epochIndex: epochIndex, processId: this.process.processId, ao: this.process.ao, }); if (!epochData) { throw new Error('Epoch data not found for epoch index ' + epochIndex); } return epochData.observations; } // go to the process epoch and fetch the observations const allTags = [ { name: 'Action', value: 'Epoch-Observations' }, { name: 'Epoch-Index', value: currentIndex.toString(), }, ]; return this.process.read({ tags: pruneTags(allTags), }); } async getDistributions(epoch) { const epochIndex = await this.computeEpochIndex(epoch); const currentIndex = await this.computeCurrentEpochIndex(); if (epochIndex !== undefined && epochIndex < currentIndex) { const epochData = await getEpochDataFromGqlWithCUFallback({ arweave: this.arweave, epochIndex: epochIndex, processId: this.process.processId, ao: this.process.ao, }); if (epochData === undefined) { throw new Error('Epoch data not found for epoch index ' + epochIndex); } return epochData.distributions; } // go to the process epoch and fetch the distributions const allTags = [ { name: 'Action', value: 'Epoch-Distributions' }, { name: 'Epoch-Index', value: currentIndex.toString(), }, ]; return this.process.read({ tags: pruneTags(allTags), }); } async getEligibleEpochRewards(epoch, params) { const epochIndex = await this.computeEpochIndex(epoch); const currentIndex = await this.computeCurrentEpochIndex(); if (epochIndex !== undefined && epochIndex < currentIndex) { const epochData = await getEpochDataFromGqlWithCUFallback({ arweave: this.arweave, epochIndex: epochIndex, processId: this.process.processId, ao: this.process.ao, }); if (!epochData) { throw new Error('Epoch data not found for epoch index ' + epochIndex); } return sortAndPaginateEpochDataIntoEligibleDistributions(epochData, params); } // on current epoch, go to process and fetch the distributions const allTags = [ { name: 'Action', value: 'Epoch-Eligible-Rewards' }, ...paginationParamsToTags(params), ]; return this.process.read({ tags: pruneTags(allTags), }); } async getTokenCost({ intent, type, years, name, quantity, fromAddress, }) { const replacedBuyRecordWithBuyName = intent === 'Buy-Record' ? 'Buy-Name' : intent; const allTags = [ { name: 'Action', value: 'Token-Cost' }, { name: 'Intent', value: replacedBuyRecordWithBuyName, }, { name: 'Name', value: name, }, { name: 'Years', value: years?.toString(), }, { name: 'Quantity', value: quantity?.toString(), }, { name: 'Purchase-Type', value: type, }, ]; return this.process.read({ tags: pruneTags(allTags), fromAddress, }); } // TODO: Can overload this function to refine different types of cost details params async getCostDetails({ intent, type, years, name, quantity, fromAddress, fundFrom, }) { const replacedBuyRecordWithBuyName = intent === 'Buy-Record' ? 'Buy-Name' : intent; if (fundFrom === 'turbo') { const { mARIO, winc } = await this.paymentProvider.getArNSPriceDetails({ intent: replacedBuyRecordWithBuyName, name, quantity, type, years, }); return { tokenCost: mARIO.valueOf(), wincQty: winc, discounts: [], }; } const allTags = [ { name: 'Action', value: 'Cost-Details' }, { name: 'Intent', value: replacedBuyRecordWithBuyName, }, { name: 'Name', value: name, }, { name: 'Years', value: years?.toString(), }, { name: 'Quantity', value: quantity?.toString(), }, { name: 'Purchase-Type', value: type, }, { name: 'Fund-From', value: fundFrom, }, ]; return this.process.read({ tags: pruneTags(allTags), fromAddress, }); } async getRegistrationFees() { return this.process.read({ tags: [{ name: 'Action', value: 'Registration-Fees' }], }); } async getDemandFactor() { return this.process.read({ tags: [{ name: 'Action', value: 'Demand-Factor' }], }); } async getDemandFactorSettings() { return this.process.read({ tags: [{ name: 'Action', value: 'Demand-Factor-Settings' }], }); } async getArNSReturnedNames(params) { return this.process.read({ tags: [ { name: 'Action', value: 'Returned-Names' }, ...paginationParamsToTags(params), ], }); } async getArNSReturnedName({ name, }) { const allTags = [ { name: 'Action', value: 'Returned-Name' }, { name: 'Name', value: name }, ]; return this.process.read({ tags: allTags, }); } async getDelegations(params) { const allTags = [ { name: 'Action', value: 'Paginated-Delegations' }, { name: 'Address', value: params.address }, ...paginationParamsToTags(params), ]; return this.process.read({ tags: pruneTags(allTags), }); } async getAllowedDelegates(params) { return this.getGatewayDelegateAllowList(params); } async getGatewayVaults(params) { return this.process.read({ tags: [ { name: 'Action', value: 'Paginated-Gateway-Vaults' }, { name: 'Address', value: params.address }, ...paginationParamsToTags(params), ], }); } async getPrimaryNameRequest(params) { const allTags = [ { name: 'Action', value: 'Primary-Name-Request' }, { name: 'Initiator', value: params.initiator, }, ]; return this.process.read({ tags: allTags, }); } async getPrimaryNameRequests(params) { return this.process.read({ tags: [ { name: 'Action', value: 'Primary-Name-Requests' }, ...paginationParamsToTags(params), ], }); } async getPrimaryName(params) { const allTags = [ { name: 'Action', value: 'Primary-Name' }, { name: 'Address', value: params?.address, }, { name: 'Name', value: params?.name }, ]; return this.process.read({ tags: pruneTags(allTags), }); } async getPrimaryNames(params) { return this.process.read({ tags: [ { name: 'Action', value: 'Primary-Names' }, ...paginationParamsToTags(params), ], }); } /** * Get current redelegation fee percentage for address * * @param {Object} params - The parameters for fetching redelegation fee * @param {string} params.address - The address to fetch the fee for * @returns {Promise<AoMessageResult>} The redelegation fee result */ async getRedelegationFee(params) { return this.process.read({ tags: [ { name: 'Action', value: 'Redelegation-Fee' }, { name: 'Address', value: params.address }, ], }); } async getGatewayRegistrySettings() { return this.process.read({ tags: [{ name: 'Action', value: 'Gateway-Registry-Settings' }], }); } async getAllDelegates(params) { return this.process.read({ tags: [ { name: 'Action', value: 'All-Paginated-Delegates' }, ...paginationParamsToTags(params), ], }); } async getAllGatewayVaults(params) { return this.process.read({ tags: [ { name: 'Action', value: 'All-Gateway-Vaults' }, ...paginationParamsToTags(params), ], }); } async resolveArNSName({ name, }) { // derive baseName & undername using last underscore const lastUnderscore = name.lastIndexOf('_'); const baseName = lastUnderscore === -1 ? name : name.slice(lastUnderscore + 1); const undername = lastUnderscore === -1 ? '@' : name.slice(0, lastUnderscore); // guard against missing or unregistered ARNS record const nameData = await this.getArNSRecord({ name: baseName }); if (nameData === undefined || nameData.processId === undefined) { throw new Error(`Base ArNS name ${baseName} not found on ARIO contract (${this.process.processId}).`); } const ant = ANT.init({ process: new AOProcess({ ao: this.process.ao, processId: nameData.processId, }), }); const [owner, antRecord] = await Promise.all([ ant.getOwner(), ant.getRecord({ undername }), ]); if (antRecord === undefined) { throw new Error(`Record for ${undername} not found on ANT.`); } if (antRecord.ttlSeconds === undefined || antRecord.transactionId === undefined) { throw new Error(`Invalid record on ANT. Must have ttlSeconds and transactionId. Record: ${JSON.stringify(antRecord)}`); } return { name, owner, txId: antRecord.transactionId, ttlSeconds: antRecord.ttlSeconds, priority: antRecord.priority, // NOTE: we may want return the actual index of the record based on sorting // in case ANT tries to set duplicate priority values to get around undername limits processId: nameData.processId, undernameLimit: nameData.undernameLimit, type: nameData.type, }; } /** * Get all ARNS names associated with an address using the provided ANT registry address. * * By default it will use the mainnet ANT registry address. * * @param {Object} params - The parameters for fetching ARNS names * @param {string} params.address - The address to fetch the ARNS names for * @returns {Promise<AoArNSNameData[]>} The ARNS names associated with the address */ async getArNSRecordsForAddress(params) { const { antRegistryProcessId = ANT_REGISTRY_ID, address } = params; const antRegistry = ANTRegistry.init({ hyperbeamUrl: this.hyperbeamUrl, process: new AOProcess({ ao: this.process.ao, processId: antRegistryProcessId, }), }); // Note: there could be a race condition here if the ACL changes during pagination requests, resulting in different results from the `getArNSRecords`. // This is an unlikely scenario, so to give the client control, and keep this API consistent with other ArNS APIs, we refetch the ACL for each page, and // return paginated results. const { Controlled = [], Owned = [] } = await antRegistry.accessControlList({ address, }); const allProcessIds = new Set([...Controlled, ...Owned]); if (allProcessIds.size === 0) { return { items: [], hasMore: false, nextCursor: undefined, limit: params.limit ?? 1000, totalItems: 0, sortOrder: params.sortOrder ?? 'asc', }; } const currentPage = await this.getArNSRecords({ ...params, filters: { // NOTE: we confirmed that dry-runs are not limited to the same tag limits as data-items. // Should this change, we'll need to batch the requests. processId: Array.from(allProcessIds), }, }); return currentPage; } } export class ARIOWriteable extends ARIOReadable { signer; paymentProvider; constructor({ signer, paymentUrl, ...config }) { if (config === undefined) { super({ process: new AOProcess({ processId: ARIO_MAINNET_PROCESS_ID, }), }); } else { super(config); } this.signer = createAoSigner(signer); this.paymentProvider = TurboArNSPaymentFactory.init({ signer: isTurboArNSSigner(signer) ? signer : undefined, paymentUrl, }); } async transfer({ target, qty, }, options) { const { tags = [] } = options || {}; return this.process.send({ tags: [ ...tags, { name: 'Action', value: 'Transfer' }, { name: 'Recipient', value: target, }, { name: 'Quantity', value: qty.valueOf().toString(), }, ], signer: this.signer, }); } async vaultedTransfer({ recipient, quantity, lockLengthMs, revokable = false, }, options) { const { tags = [] } = options || {}; return this.process.send({ tags: [ ...tags, { name: 'Action', value: 'Vaulted-Transfer' }, { name: 'Recipient', value: recipient }, { name: 'Quantity', value: quantity.toString() }, { name: 'Lock-Length', value: lockLengthMs.toString() }, { name: 'Revokable', value: `${revokable}` }, ], signer: this.signer, }); } async revokeVault({ vaultId, recipient }, options) { const { tags = [] } = options || {}; return this.process.send({ tags: [ ...tags, { name: 'Action', value: 'Revoke-Vault' }, { name: 'Vault-Id', value: vaultId }, { name: 'Recipient', value: recipient }, ], signer: this.signer, }); } async createVault({ lockLengthMs, quantity }, options) { const { tags = [] } = options || {}; return this.process.send({ tags: [ ...tags, { name: 'Action', value: 'Create-Vault' }, { name: 'Lock-Length', value: lockLengthMs.toString() }, { name: 'Quantity', value: quantity.toString() }, ], signer: this.signer, }); } async extendVault({ vaultId, extendLengthMs }, options) { const { tags = [] } = options || {}; return this.process.send({ tags: [ ...tags, { name: 'Action', value: 'Extend-Vault' }, { name: 'Vault-Id', value: vaultId }, { name: 'Extend-Length', value: extendLengthMs.toString() }, ], signer: this.signer, }); } async increaseVault({ vaultId, quantity }, options) { const { tags = [] } = options || {}; return this.process.send({ tags: [ ...tags, { name: 'Action', value: 'Increase-Vault' }, { name: 'Vault-Id', value: vaultId }, { name: 'Quantity', value: quantity.toString() }, ], signer: this.signer, }); } async joinNetwork({ operatorStake, allowDelegatedStaking, allowedDelegates, delegateRewardShareRatio, fqdn, label, minDelegatedStake, note, port, properties, protocol, autoStake, observerAddress, }, options) { const { tags = [] } = options || {}; const allTags = [ ...tags, { name: 'Action', value: 'Join-Network' }, { name: 'Operator-Stake', value: operatorStake.valueOf().toString(), }, { name: 'Allow-Delegated-Staking', value: allowDelegatedStaking?.toString(), }, { name: 'Allowed-Delegates', value: allowedDelegates?.join(','), }, { name: 'Delegate-Reward-Share-Ratio', value: delegateRewardShareRatio?.toString(), }, { name: 'FQDN', value: fqdn, }, { name: 'Label', value: label, }, { name: 'Min-Delegated-Stake', value: minDelegatedStake?.valueOf().toString(), }, { name: 'Note', value: note, }, { name: 'Port', value: port?.toString(), }, { name: 'Properties', value: properties, }, { name: 'Protocol', value: protocol, }, { name: 'Auto-Stake', value: autoStake?.toString(), }, { name: 'Observer-Address', value: observerAddress, }, ]; return this.process.send({ signer: this.signer, tags: pruneTags(allTags), }); } async leaveNetwork(options) { const { tags = [] } = options || {}; return this.process.send({ signer: this.signer, tags: [...tags, { name: 'Action', value: 'Leave-Network' }], }); } async updateGatewaySettings({ allowDelegatedStaking, allowedDelegates, delegateRewardShareRatio, fqdn, label, minDelegatedStake, note, port, properties, protocol, autoStake, observerAddress, }, options) { const { tags = [] } = options || {}; const allTags = [ ...tags, { name: 'Action', value: 'Update-Gateway-Settings' }, { name: 'Label', value: label }, { name: 'Note', value: note }, { name: 'FQDN', value: fqdn }, { name: 'Port', value: port?.toString() }, { name: 'Properties', value: properties }, { name: 'Protocol', value: protocol }, { name: 'Observer-Address', value: observerAddress }, { name: 'Allow-Delegated-Staking', value: allowDelegatedStaking?.toString(), }, { name: 'Allowed-Delegates', value: allowedDelegates?.join(','), }, { name: 'Delegate-Reward-Share-Ratio', value: delegateRewardShareRatio?.toString(), }, { name: 'Min-Delegated-Stake', value: minDelegatedStake?.valueOf().toString(), }, { name: 'Auto-Stake', value: autoStake?.toString() }, ]; return this.process.send({ signer: this.signer, tags: pruneTags(allTags), }); } async delegateStake(params, options) { const { tags = [] } = options || {}; return this.process.send({ signer: this.signer, tags: [ ...tags, { name: 'Action', value: 'Delegate-Stake' }, { name: 'Target', value: params.target }, { name: 'Quantity', value: params.stakeQty.valueOf().toString() }, ], }); } async decreaseDelegateStake(params, options) { const { tags = [] } = options || {}; return this.process.send({ signer: this.signer, tags: [ ...tags, { name: 'Action', value: 'Decrease-Delegate-Stake' }, { name: 'Target', value: params.target }, { name: 'Quantity', value: params.decreaseQty.valueOf().toString() }, { name: 'Instant', value: `${params.instant || false}` }, ], }); } /** * Initiates an instant withdrawal from a gateway. * * @param {Object} params - The parameters for initiating an instant withdrawal * @param {string} params.address - The gateway address of the withdrawal, if not provided, the signer's address will be used * @param {string} params.vaultId - The vault ID of the withdrawal * @returns {Promise<AoMessageResult>} The result of the withdrawal */ async instantWithdrawal(params, options) { const { tags = [] } = options || {}; const allTags = [ ...tags, { name: 'Action', value: 'Instant-Withdrawal' }, { name: 'Vault-Id', value: params.vaultId }, { name: 'Address', value: params.gatewayAddress }, ]; return this.process.send({ signer: this.signer, tags: pruneTags(allTags), }); } async increaseOperatorStake(params, options) { const { tags = [] } = options || {}; return this.process.send({ signer: this.signer, tags: [ ...tags, { name: 'Action', value: 'Increase-Operator-Stake' }, { name: 'Quantity', value: params.increaseQty.valueOf().toString() }, ], }); } async decreaseOperatorStake(params, options) { const { tags = [] } = options || {}; return this.process.send({ signer: this.signer, tags: [ ...tags, { name: 'Action', value: 'Decrease-Operator-Stake' }, { name: 'Quantity', value: params.decreaseQty.valueOf().toString() }, { name: 'Instant', value: `${params.instant || false}` }, ], }); } async saveObservations(params, options) { const { tags = [] } = options || {}; return this.process.send({ signer: this.signer, tags: [ ...tags, { name: 'Action', value: 'Save-Observations' }, { name: 'Report-Tx-Id', value: params.reportTxId, }, { name: 'Failed-Gateways', value: params.failedGateways.join(','), }, ], }); } async buyRecord(params, options) { // spawn a new ANT if not provided if (params.processId === undefined) { try { // if a Name tag is provided, use it. Else, default to the arns name being purchased. const { nameTag, otherTags } = (options?.tags || []).reduce((acc, tag) => { if (tag.name === 'Name') { acc.nameTag = tag; } else { acc.otherTags.push(tag); } return acc; }, { nameTag: { name: 'Name', value: params.name }, otherTags: [] }); params.processId = await ANT.spawn({ signer: this.signer, ao: this.process.ao, logger: this.logger, // This lets AOS set the ArNS name as the Name in lua state tags: [nameTag, ...otherTags], onSigningProgress: options?.onSigningProgress, }); } catch (error) { this.logger.error('Failed to spawn ANT for name purchase.', { error, }); throw error; } } options?.onSigningProgress?.('buying-name', { name: params.name, years: params.years, type: params.type, processId: params.processId, fundFrom: params.fundFrom, referrer: params.referrer, }); // pay with turbo credits if available if (params.fundFrom === 'turbo') { if (!(this.paymentProvider instanceof TurboArNSPaymentProviderAuthenticated)) { throw new Error('Turbo funding is not supported for this payment provider'); } return this.paymentProvider.initiateArNSPurchase({ intent: 'Buy-Name', name: params.name, years: params.years, type: params.type, processId: params.processId, paidBy: params.paidBy, }); } const { tags = [] } = options || {}; const allTags = [ ...tags, { name: 'Action', value: 'Buy-Name' }, { name: 'Name', value: params.name }, { 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 }, { name: 'Referrer', value: params.referrer }, ]; return this.process.send({ signer: this.signer, tags: pruneTags(allTags), }); } /** * Upgrades an existing leased record to a permabuy. * * @param {Object} params - The parameters for upgrading a record * @param {string} params.name - The name of the record to upgrade * @param {Object} [options] - The options for the upgrade * @returns {Promise<AoMessageResult>} The result of the upgrade */ async upgradeRecord(params, options) { if (params.fundFrom === 'turbo') { if (!(this.paymentProvider instanceof TurboArNSPaymentProviderAuthenticated)) { throw new Error('Turbo funding is not supported for this payment provider'); } return this.paymentProvider.initiateArNSPurchase({ intent: 'Upgrade-Name', name: params.name, }); } const { tags = [] } = options || {}; const allTags = [ ...tags, { name: 'Action', value: 'Upgrade-Name' }, { name: 'Name', value: params.name }, { name: 'Fund-From', value: params.fundFrom }, { name: 'Referrer', value: params.referrer }, ]; return this.process.send({ signer: this.signer, tags: pruneTags(allTags), }); } /** * Extends the lease of an existing leased record. * * @param {Object} params - The parameters for extending a lease * @param {string} params.name - The name of the record to extend * @param {number} params.years - The number of years to extend the lease * @param {Object} [options] - The options for the extension * @returns {Promise<AoMessageResult>} The result of the extension */ async extendLease(params, options) { if (params.fundFrom === 'turbo') { if (!(this.paymentProvider instanceof TurboArNSPaymentProviderAuthenticated)) { throw new Error('Turbo funding is not supported for this payment provider'); } return this.paymentProvider.initiateArNSPurchase({ intent: 'Extend-Lease', name: params.name, years: params.years, }); } 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 }, { name: 'Referrer', value: params.referrer }, ]; return this.process.send({ signer: this.signer, tags: pruneTags(allTags), }); } async increaseUndernameLimit(params, options) { if (params.fundFrom === 'turbo') { if (!(this.paymentProvider instanceof TurboArNSPaymentProviderAuthenticated)) { throw new Error('Turbo funding is not supported for this payment provider'); } return this.paymentProvider.initiateArNSPurchase({ intent: 'Increase-Undername-Limit', quantity: params.increaseCount, name: params.name, }); } 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 }, { name: 'Referrer', value: params.referrer }, ]; return this.process.send({ signer: this.signer, tags: pruneTags(allTags), }); } /** * Cancel a withdrawal from a gateway. * * @param {Object} params - The parameters for cancelling a withdrawal * @param {string} [params.address] - The address of the withdrawal (optional). If not provided, the signer's address will be used. * @param {string} params.vaultId - The vault ID of the withdrawal. * @param {Object} [options] - The options for the cancellation * @returns {Promise<AoMessageResult>} The result of the cancellation */ async cancelWithdrawal(params, options) { const { tags = [] } = options || {}; const allTags = [ ...tags, { name: 'Action', value: 'Cancel-Withdrawal' }, { name: 'Vault-Id', value: params.vaultId }, { name: 'Address', value: params.gatewayAddress }, ]; return this.process.send({ signer: this.signer, tags: pruneTags(allTags), }); } async requestPrimaryName(params, options) { if (params.fundFrom === 'turbo') { throw new Error('Turbo funding is not yet supported for primary name requests'); } const { tags = [] } = options || {}; const allTags = [ ...tags, { name: 'Action', value: 'Request-Primary-Name' }, { name: 'Name', value: params.name }, { name: 'Fund-From', value: params.fundFrom }, ]; return this.process.send({ signer: this.signer, tags: pruneTags(allTags), }); } async setPrimaryName(params, options) { options?.onSigningProgress?.('requesting-primary-name', { name: params.name, fundFrom: params.fundFrom, referrer: params.referrer, }); // create the primary name request, if it already exists, get the request and base name owner const requestResult = await this.requestPrimaryName(params, options).catch(async (error) => { // check for the error message, it may be due to the request already being made if (error.message.includes('already exists')) { // parse out the initiator from the error message ` "Primary name request by '" .. initiator .. "' for '" .. name .. "' already exists" const initiator = error.message.match(/by '([^']+)'/)?.[1]; if (initiator === undefined) { throw error; } options?.onSigningProgress?.('request-already-exists', { name: params.name, initiator, }); // get the primary name request const primaryNameRequest = await this.getPrimaryNameRequest({ initiator, }); // check the name exists const arnsRecord = await this.getArNSRecord({ name: params.name }); if (arnsRecord === undefined) { throw new Error(`ARNS name '${params.name}' does not exist`); } if (primaryNameRequest.initiator !== initiator) { throw new Error(`Primary name request for name '${params.name}' was not approved`); } return { id: 'stub-id', // stub-id to indicate that the request already exists result: { // this is a partial stub of the AoCreatePrimaryNameRequest result // we only need the request and base name owner for the approval request: primaryNameRequest, baseNameOwner: arnsRecord.processId, fundingPlan: { address: initiator, }, }, }; } // throw any other errors from the contract throw error; }); // the result is either a new primary name request or an existing one // for new primary name requests the result includes the request, funding plan, and base name owner // for existing primary name requests the result includes just the request and base name owner (see above) const primaryNameRequest = requestResult.result; const antProcessId = primaryNameRequest?.baseNameOwner; const initiator = primaryNameRequest?.fundingPlan?.address; if (primaryNameRequest === undefined || initiator === undefined || antProcessId === undefined) { throw new Error(`Failed to request primary name ${params.name} for ${initiator} owned by ${antProcessId} process`); } options?.onSigningProgress?.('approving-request', { name: params.name, processId: antProcessId, request: primaryNameRequest.request, }); const antClient = ANT.init({ process: new AOProcess({ processId: antProcessId, ao: this.process.ao, }), signer: this.signer, }); // approve the primary name request with the ant const approveResult = await antClient.approvePrimaryNameRequest({ name: params.name, address: initiator, arioProcessId: this.process.processId, }, options); return approveResult; } /** * Redelegate stake from one gateway to another gateway. * * @param {Object} params - The parameters for redelegating stake * @param {string} params.target - The target gateway address * @param {string} params.source - The source gateway address * @param {number} params.stakeQty - The quantity of stake to redelegate * @param {string} params.vaultId - An optional vault ID to redelegate from * @param {Object} [options] - The options for the redelegation * @returns {Promise<AoMessageResult>} The result of the redelegation */ async redelegateStake(params, options) { const { tags = [] } = options || {}; const allTags = [ ...tags, { name: 'Action', value: 'Redelegate-Stake' }, { name: 'Target', value: params.target }, { name: 'Source', value: params.source }, { name: 'Quantity', value: params.stakeQty.valueOf().toString() }, { name: 'Vault-Id', value: params.vaultId }, ]; return this.process.send({ signer: this.signer, tags: pruneTags(allTags), }); } }