@ar.io/sdk
Version:
[](https://codecov.io/gh/ar-io/ar-io-sdk)
1,312 lines (1,311 loc) • 49.5 kB
JavaScript
/**
* 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),
});
}
}