@ar.io/sdk
Version:
[](https://codecov.io/gh/ar-io/ar-io-sdk)
477 lines (476 loc) • 16.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 { EthereumSigner } from '@dha-team/arbundles';
import { connect } from '@permaweb/aoconnect';
import { program } from 'commander';
import { readFileSync } from 'fs';
import prompts from 'prompts';
import { ANT, AOProcess, ARIO, ARIOToken, ARIO_DEVNET_PROCESS_ID, ARIO_MAINNET_PROCESS_ID, ARIO_TESTNET_PROCESS_ID, ArweaveSigner, Logger, createAoSigner, fromB64Url, fundFromOptions, initANTStateForAddress, isValidFundFrom, isValidIntent, mARIOToken, sha256B64Url, validIntents, } from '../node/index.js';
import { globalOptions } from './options.js';
export const defaultTtlSecondsCLI = 3600;
export function stringifyJsonForCLIDisplay(json) {
return JSON.stringify(json, null, 2);
}
function logCommandOutput(output) {
console.log(stringifyJsonForCLIDisplay(output));
}
function exitWithErrorLog(error, debug = false) {
let errorLog;
if (error instanceof Error) {
errorLog = error.message;
if (debug && error.stack !== undefined) {
errorLog = error.stack;
}
}
else {
errorLog = stringifyJsonForCLIDisplay(error);
}
console.error(errorLog);
process.exit(1);
}
export async function runCommand(command, action) {
const options = command.optsWithGlobals();
try {
const output = await action(options);
logCommandOutput(output);
process.exit(0);
}
catch (error) {
exitWithErrorLog(error, options.debug);
}
}
export function applyOptions(command, options) {
[...options].forEach((option) => {
command.option(option.alias, option.description, option.default);
});
return command;
}
export function makeCommand({ description, name, options = [], action, }) {
const command = program.command(name).description(description);
const appliedCommand = applyOptions(command, [...options, ...globalOptions]);
if (action !== undefined) {
appliedCommand.action(() => runCommand(appliedCommand, action));
}
return appliedCommand;
}
export function arioProcessIdFromOptions({ arioProcessId, devnet, testnet, }) {
if (arioProcessId !== undefined) {
return arioProcessId;
}
if (devnet) {
return ARIO_DEVNET_PROCESS_ID;
}
if (testnet) {
return ARIO_TESTNET_PROCESS_ID;
}
return ARIO_MAINNET_PROCESS_ID;
}
function walletFromOptions({ privateKey, walletFile, }) {
if (privateKey !== undefined) {
return JSON.parse(privateKey);
}
if (walletFile !== undefined) {
return JSON.parse(readFileSync(walletFile, 'utf-8'));
}
return undefined;
}
export function requiredJwkFromOptions(options) {
const jwk = walletFromOptions(options);
if (jwk === undefined) {
throw new Error('No JWK provided for signing!\nPlease provide a stringified JWK with `--private-key` or the file path of a jwk.json file with `--wallet-file`');
}
return jwk;
}
export function jwkToAddress(jwk) {
return sha256B64Url(fromB64Url(jwk.n));
}
function setLoggerIfDebug(options) {
if (options.debug) {
Logger.default.setLogLevel('debug');
}
}
export function getLoggerFromOptions(options) {
setLoggerIfDebug(options);
return Logger.default;
}
function aoProcessFromOptions(options) {
return new AOProcess({
processId: arioProcessIdFromOptions(options),
ao: connect({
MODE: 'legacy',
CU_URL: options.cuUrl,
}),
});
}
export function readARIOFromOptions(options) {
setLoggerIfDebug(options);
return ARIO.init({
process: aoProcessFromOptions({
cuUrl: 'https://cu.ardrive.io', // default to ardrive cu for ARIO process
...options,
}),
paymentUrl: options.paymentUrl,
});
}
export function contractSignerFromOptions(options) {
const wallet = walletFromOptions(options);
if (wallet === undefined) {
return undefined;
}
const token = options.token ?? 'arweave';
if (token === 'ethereum') {
const signer = new EthereumSigner(wallet);
// For EthereumSigner, we need to convert the JWK to a string
return { signer, signerAddress: signer.publicKey.toString('hex') };
}
// TODO: Support other wallet types
const signer = new ArweaveSigner(wallet);
return { signer, signerAddress: jwkToAddress(wallet) };
}
export function requiredContractSignerFromOptions(options) {
const contractSigner = contractSignerFromOptions(options);
if (contractSigner === undefined) {
throw new Error('No signer provided for signing!\nPlease provide a stringified JWK or Ethereum private key with `--private-key` or the file path of an arweave.jwk.json or eth.private.key.txt file with `--wallet-file`');
}
return contractSigner;
}
export function requiredAoSignerFromOptions(options) {
return createAoSigner(requiredContractSignerFromOptions(options).signer);
}
export function writeARIOFromOptions(options) {
const { signer, signerAddress } = requiredContractSignerFromOptions(options);
setLoggerIfDebug(options);
return {
ario: ARIO.init({
process: aoProcessFromOptions(options),
signer,
paymentUrl: options.paymentUrl,
}),
signerAddress,
};
}
export function formatARIOWithCommas(value) {
const [integerPart, decimalPart] = value.toString().split('.');
const integerWithCommas = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
if (decimalPart === undefined) {
return integerWithCommas;
}
return integerWithCommas + '.' + decimalPart;
}
export function formatMARIOToARIOWithCommas(value) {
return formatARIOWithCommas(value.toARIO());
}
/** helper to get address from --address option first, then check wallet options */
export function addressFromOptions(options) {
if (options.address !== undefined) {
return options.address;
}
const signer = contractSignerFromOptions(options);
if (signer !== undefined) {
return signer.signerAddress;
}
return undefined;
}
export function requiredAddressFromOptions(options) {
const address = addressFromOptions(options);
if (address !== undefined) {
return address;
}
throw new Error('No address provided. Use --address or --wallet-file');
}
const defaultCliPaginationLimit = 10; // more friendly UX than 100
export function paginationParamsFromOptions(options) {
const { cursor, limit, sortBy, sortOrder } = options;
if (sortOrder !== undefined && !['asc', 'desc'].includes(sortOrder)) {
throw new Error(`Invalid sort order: ${sortOrder}, must be "asc" or "desc"`);
}
const numberLimit = limit !== undefined ? +limit : defaultCliPaginationLimit;
if (isNaN(numberLimit) || numberLimit <= 0) {
throw new Error(`Invalid limit: ${numberLimit}, must be a positive number`);
}
return {
cursor,
limit: numberLimit,
sortBy: sortBy,
sortOrder,
};
}
export function epochInputFromOptions(options) {
if (options.epochIndex !== undefined) {
return { epochIndex: +options.epochIndex };
}
if (options.timestamp !== undefined) {
return { timestamp: +options.timestamp };
}
return undefined;
}
export function requiredInitiatorFromOptions(options) {
if (options.initiator !== undefined) {
return options.initiator;
}
return requiredAddressFromOptions(options);
}
export function customTagsFromOptions(options) {
if (options.tags === undefined) {
return {};
}
if (!Array.isArray(options.tags)) {
throw new Error('Tags must be an array');
}
if (options.tags.length === 0) {
return {};
}
if (options.tags.length % 2 !== 0) {
throw new Error('Tags must be an array of key-value pairs');
}
const tags = [];
for (let i = 0; i < options.tags.length; i += 2) {
tags.push({
name: options.tags[i],
value: options.tags[i + 1],
});
}
return {
tags,
};
}
export function gatewaySettingsFromOptions({ allowDelegatedStaking, autoStake, delegateRewardShareRatio, fqdn, label, minDelegatedStake, note, observerAddress, port, properties, allowedDelegates, }) {
return {
observerAddress,
allowDelegatedStaking,
autoStake,
delegateRewardShareRatio: delegateRewardShareRatio !== undefined
? +delegateRewardShareRatio
: undefined,
allowedDelegates,
fqdn,
label,
minDelegatedStake: minDelegatedStake !== undefined ? +minDelegatedStake : undefined,
note,
port: port !== undefined ? +port : undefined,
properties,
};
}
export function requiredTargetAndQuantityFromOptions(options) {
if (options.target === undefined) {
throw new Error('No target provided. Use --target');
}
if (options.quantity === undefined) {
throw new Error('No quantity provided. Use --quantity');
}
return {
target: options.target,
arioQuantity: new ARIOToken(+options.quantity),
};
}
export function redelegateParamsFromOptions(options) {
const { target, arioQuantity: aRIOQuantity } = requiredTargetAndQuantityFromOptions(options);
const source = options.source;
if (source === undefined) {
throw new Error('No source provided. Use --source');
}
return {
target,
source,
vaultId: options.vaultId,
stakeQty: aRIOQuantity.toMARIO(),
};
}
export function recordTypeFromOptions(options) {
options.type ??= 'lease';
if (options.type !== 'lease' && options.type !== 'permabuy') {
throw new Error(`Invalid type. Valid types are: lease, permabuy`);
}
return options.type;
}
export function requiredMARIOFromOptions(options, key) {
if (options[key] === undefined) {
throw new Error(`No ${key} provided. Use --${key} denominated in ARIO`);
}
return new ARIOToken(+options[key]).toMARIO();
}
export async function assertEnoughBalanceForArNSPurchase({ ario, address, costDetailsParams, }) {
if (costDetailsParams.fundFrom === 'turbo') {
// TODO: Get turbo balance and assert it is enough -- retain paid-by from balance result and pass to CLI logic
return;
}
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, }) {
if (typeof mARIOQuantity === 'number') {
mARIOQuantity = new mARIOToken(mARIOQuantity);
}
const balance = await ario.getBalance({ address });
if (balance < mARIOQuantity.valueOf()) {
throw new Error(`Insufficient ARIO balance for action. Balance available: ${formatMARIOToARIOWithCommas(new mARIOToken(balance))} ARIO`);
}
}
export async function confirmationPrompt(message) {
const { confirm } = await prompts({
type: 'confirm',
name: 'confirm',
message,
});
return confirm;
}
export async function assertConfirmationPrompt(message, options) {
if (options.skipConfirmation) {
return true;
}
return confirmationPrompt(message);
}
export function requiredProcessIdFromOptions(o) {
if (o.processId === undefined) {
throw new Error('--process-id is required');
}
return o.processId;
}
function ANTProcessFromOptions(options) {
return new AOProcess({
processId: requiredProcessIdFromOptions(options),
ao: connect({
MODE: 'legacy',
CU_URL: options.cuUrl,
}),
});
}
export function readANTFromOptions(options) {
return ANT.init({
process: ANTProcessFromOptions(options),
});
}
export function writeANTFromOptions(options, signer) {
signer ??= requiredContractSignerFromOptions(options).signer;
return ANT.init({
process: ANTProcessFromOptions(options),
signer,
});
}
export function booleanFromOptions(options, key) {
return !!options[key];
}
export function requiredStringFromOptions(options, key) {
const value = options[key];
if (value === undefined) {
throw new Error(`--${key} is required`);
}
return value;
}
export function stringArrayFromOptions(options, key) {
const value = options[key];
if (value === undefined) {
return undefined;
}
if (!Array.isArray(value)) {
throw new Error(`--${key} must be an array`);
}
return value;
}
export function requiredStringArrayFromOptions(options, key) {
const value = stringArrayFromOptions(options, key);
if (value === undefined) {
throw new Error(`--${key} is required`);
}
return value;
}
export function positiveIntegerFromOptions(options, key) {
const value = options[key];
if (value === undefined) {
return undefined;
}
const numberValue = +value;
if (isNaN(numberValue) || numberValue <= 0) {
throw new Error(`Invalid ${key}: ${value}, must be a positive number`);
}
return numberValue;
}
export function requiredPositiveIntegerFromOptions(options, key) {
const value = positiveIntegerFromOptions(options, key);
if (value === undefined) {
throw new Error(`--${key} is required`);
}
return value;
}
export function getANTStateFromOptions(options) {
return initANTStateForAddress({
owner: requiredAddressFromOptions(options),
targetId: options.target,
controllers: options.controllers,
description: options.description,
ticker: options.ticker,
name: options.name,
keywords: options.keywords,
logo: options.logo,
ttlSeconds: options.ttlSeconds !== undefined
? +options.ttlSeconds
: defaultTtlSecondsCLI,
});
}
export function getTokenCostParamsFromOptions(o) {
o.intent ??= 'Buy-Name';
o.type ??= 'lease';
o.years ??= '1';
if (!isValidIntent(o.intent)) {
throw new Error(`Invalid intent. Valid intents are: ${validIntents.join(', ')}`);
}
if (o.type !== 'lease' && o.type !== 'permabuy') {
throw new Error(`Invalid type. Valid types are: lease, permabuy`);
}
return {
type: o.type,
quantity: o.quantity !== undefined ? +o.quantity : undefined,
years: +o.years,
intent: o.intent,
name: requiredStringFromOptions(o, 'name'),
fromAddress: addressFromOptions(o),
};
}
export function fundFromFromOptions(o) {
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';
}
export function referrerFromOptions(o) {
return o.referrer;
}
export function assertLockLengthInRange(lockLengthMs, assertMin = true) {
const minLockLengthMs = 1209600000; // 14 days
const maxLockLengthMs = 378432000000; // ~12 years
if (lockLengthMs > maxLockLengthMs) {
throw new Error(`Lock length must be at most 12 years (378432000000 ms). Provided lock length: ${lockLengthMs} ms`);
}
if (!assertMin) {
return;
}
if (lockLengthMs < minLockLengthMs) {
throw new Error(`Lock length must be at least 14 days (1209600000 ms). Provided lock length: ${lockLengthMs} ms`);
}
}