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)

246 lines (245 loc) 8.94 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 { ArconnectSigner, DataItem, createData } from '@dha-team/arbundles'; import { connect, createDataItemSigner } from '@permaweb/aoconnect'; import { z } from 'zod'; import { defaultArweave } from '../common/arweave.js'; import { AOProcess, Logger } from '../common/index.js'; import { ANT_LUA_ID, ANT_REGISTRY_ID, AOS_MODULE_ID, AO_AUTHORITY, DEFAULT_SCHEDULER_ID, } from '../constants.js'; import { SpawnANTStateSchema } from '../types/ant.js'; import { parseSchemaResult } from './schema.js'; export async function spawnANT({ signer, module = AOS_MODULE_ID, ao = connect({ MODE: 'legacy', }), scheduler = DEFAULT_SCHEDULER_ID, state, antRegistryId = ANT_REGISTRY_ID, logger = Logger.default, authority = AO_AUTHORITY, }) { // TODO: use On-Boot data handler for bootstrapping state instead of initialize-state if (state) { parseSchemaResult(SpawnANTStateSchema, state); } const processId = await ao.spawn({ module, scheduler, signer, data: state ? JSON.stringify(state) : undefined, tags: [ // Required for AOS to initialize the authorities table { name: 'Authority', value: authority, }, { name: 'ANT-Registry-Id', value: antRegistryId, }, ], }); let bootRes; let attempts = 0; while (attempts < 5 && bootRes === undefined) { try { bootRes = await ao.result({ process: processId, message: processId, }); break; } catch (error) { logger.debug('Retrying ANT boot result fetch', { processId, module, scheduler, attempts, error, }); attempts++; await new Promise((resolve) => setTimeout(resolve, 1000 * attempts ** 2)); } } if (bootRes === undefined || bootRes.Messages?.some((m) => m?.Tags?.some((t) => t.value === 'Invalid-Boot-Notice'))) { if (bootRes === undefined) { // … throw new Error('Failed to get boot result'); } const bootError = errorMessageFromOutput(bootRes); logger.error('ANT failed to boot correctly', { processId, module, scheduler, bootRes, bootError, }); throw new Error(`ANT failed to boot correctly: ${bootError}`); } logger.debug(`Spawned ANT`, { processId, module, scheduler, }); return processId; } export async function evolveANT({ signer, processId, luaCodeTxId = ANT_LUA_ID, ao = connect({ MODE: 'legacy', }), logger = Logger.default, arweave = defaultArweave, }) { const aosClient = new AOProcess({ processId, ao, logger, }); //TODO: cache locally and only fetch if not cached // We do not use arweave to get the data because it may throw on l2 tx data const { api: { host, port, protocol }, } = arweave.getConfig(); const luaString = await fetch(`${protocol}://${host}:${port}/${luaCodeTxId}`).then((res) => res.text()); const { id: evolveMsgId } = await aosClient.send({ tags: [ { name: 'Action', value: 'Eval' }, { name: 'App-Name', value: 'ArNS-ANT' }, { name: 'Source-Code-TX-ID', value: luaCodeTxId }, ], data: luaString, signer, }); logger.debug(`Evolved ANT`, { processId, luaCodeTxId, evalMsgId: evolveMsgId, }); return evolveMsgId; } export function isAoSigner(value) { const TagSchema = z.object({ name: z.string(), value: z.union([z.string(), z.number()]), }); const AoSignerSchema = z .function() .args(z.object({ data: z.union([z.string(), z.instanceof(Buffer)]), tags: z.array(TagSchema).optional(), target: z.string().optional(), anchor: z.string().optional(), })) .returns(z.promise(z.object({ id: z.string(), raw: z.instanceof(ArrayBuffer), }))); try { AoSignerSchema.parse(value); return true; } catch { return false; } } export function createAoSigner(signer) { if (isAoSigner(signer)) { return signer; } if (!('publicKey' in signer)) { return createDataItemSigner(signer); } const aoSigner = async ({ data, tags, target, anchor }) => { // ensure appropriate permissions are granted with injected signers. if (signer.publicKey === undefined && 'setPublicKey' in signer && typeof signer.setPublicKey === 'function') { await signer.setPublicKey(); } if (signer instanceof ArconnectSigner) { // Sign using Arconnect signDataItem API const signedDataItem = await signer['signer'].signDataItem({ data, tags, target, anchor, }); const dataItem = new DataItem(Buffer.from(signedDataItem)); return { id: await dataItem.id, raw: await dataItem.getRaw(), }; } const dataItem = createData(data, signer, { tags, target, anchor }); const signedData = dataItem.sign(signer).then(async () => ({ id: await dataItem.id, raw: await dataItem.getRaw(), })); return signedData; }; // eslint-disable-next-line // @ts-ignore Buffer vs ArrayBuffer type mismatch return aoSigner; } export const defaultTargetManifestId = '-k7t8xMoB8hW482609Z9F4bTFMC3MnuW8bTvTyT8pFI'; export const defaultANTLogoId = 'Sie_26dvgyok0PZD_-iQAFOhOd5YxDTkczOLoqTTL_A'; export function initANTStateForAddress({ owner, targetId, ttlSeconds = 3600, keywords = [], controllers = [], description = '', ticker = 'aos', name = 'ANT', logo = defaultANTLogoId, }) { return { ticker, name, description, keywords, owner, controllers: [owner, ...controllers], balances: { [owner]: 1 }, records: { ['@']: { transactionId: targetId ?? defaultTargetManifestId.toString(), ttlSeconds, }, }, logo, }; } /** * Uses zod schema to parse the epoch data */ export function parseAoEpochData(value) { const epochDataSchema = z.object({ startTimestamp: z.number(), startHeight: z.number(), distributions: z.any(), // TODO: add full distributed object type endTimestamp: z.number(), prescribedObservers: z.any(), prescribedNames: z.array(z.string()), observations: z.any(), epochIndex: z.number(), }); return epochDataSchema.parse(value); } export function errorMessageFromOutput(output) { const errorData = output.Error; // Attempt to extract error details from Messages.Tags if Error is undefined const error = errorData ?? output.Messages?.[0]?.Tags?.find((tag) => tag.name === 'Error')?.value; if (error !== undefined) { // Consolidated regex to match and extract line number and AO error message or Error Tags const match = error.match(/\[string "aos"]:(\d+):\s*(.+)/); if (match) { const [, lineNumber, errorMessage] = match; const cleanError = removeUnicodeFromError(errorMessage); return `${cleanError.trim()} (line ${lineNumber.trim()})`.trim(); } // With no match, just remove unicode return removeUnicodeFromError(error); } return undefined; } export function removeUnicodeFromError(error) { //The regular expression /[\u001b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g is designed to match ANSI escape codes used for terminal formatting. These are sequences that begin with \u001b (ESC character) and are often followed by [ and control codes. const ESC = String.fromCharCode(27); // Represents '\u001b' or '\x1b' return error .replace(new RegExp(`${ESC}[\\[\\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]`, 'g'), '') .trim(); }