diff --git a/package.json b/package.json index ce9c65bc..252a2dd1 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,8 @@ "axios-retry": "^4.3.0", "eventemitter3": "^5.0.1", "plimit-lit": "^3.0.1", - "winston": "^3.13.0" + "winston": "^3.13.0", + "zod": "^3.23.8" }, "lint-staged": { "**/*.{ts,js,mjs,cjs,md,json}": [ diff --git a/src/common.ts b/src/common.ts index cab11022..d7aedd7a 100644 --- a/src/common.ts +++ b/src/common.ts @@ -25,6 +25,8 @@ import { } from '@permaweb/aoconnect'; import { Signer } from 'arbundles'; +import { AoSigner } from './token.js'; + export type BlockHeight = number; export type SortKey = string; export type Timestamp = number; @@ -33,7 +35,7 @@ export type TransactionId = string; export type ProcessId = string; // TODO: append this with other configuration options (e.g. local vs. remote evaluation) -export type ContractSigner = Signer | Window['arweaveWallet']; +export type ContractSigner = Signer | Window['arweaveWallet'] | AoSigner; export type WithSigner> = { signer: ContractSigner; } & T; // TODO: optionally allow JWK in place of signer diff --git a/src/utils/ao.ts b/src/utils/ao.ts index 3232de6b..639ff9fe 100644 --- a/src/utils/ao.ts +++ b/src/utils/ao.ts @@ -16,6 +16,7 @@ */ import { connect, createDataItemSigner } from '@permaweb/aoconnect'; import { createData } from 'arbundles'; +import { z } from 'zod'; import { defaultArweave } from '../common/arweave.js'; import { AOProcess } from '../common/index.js'; @@ -157,7 +158,43 @@ export async function evolveANT({ return id; } +export function isAoSigner(value: unknown): value is AoSigner { + 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: ContractSigner): AoSigner { + if (isAoSigner(signer)) { + return signer; + } + if (!('publicKey' in signer)) { return createDataItemSigner(signer) as AoSigner; } diff --git a/tests/e2e/cjs/index.test.js b/tests/e2e/cjs/index.test.js index 05ab78a3..b740c656 100644 --- a/tests/e2e/cjs/index.test.js +++ b/tests/e2e/cjs/index.test.js @@ -1,10 +1,30 @@ -const { describe, it } = require('node:test'); +const { describe, it, before } = require('node:test'); const assert = require('node:assert/strict'); +const fs = require('node:fs'); /** * Ensure that npm link has been ran prior to running these tests * (simply running npm run test:integration will ensure npm link is ran) */ -const { IO, ioDevnetProcessId, ANTRegistry } = require('@ar.io/sdk'); +const { + IO, + ioDevnetProcessId, + ANTRegistry, + ANT, + createAoSigner, + ArweaveSigner, + IOWriteable, + AoANTWriteable, + AoANTRegistryWriteable, +} = require('@ar.io/sdk'); + +const testWalletJSON = fs.readFileSync('../test-wallet.json', { + encoding: 'utf-8', +}); +const testWallet = JSON.parse(testWalletJSON); +const signers = [ + new ArweaveSigner(testWallet), + createAoSigner(new ArweaveSigner(testWallet)), +]; const io = IO.init({ processId: ioDevnetProcessId, @@ -265,6 +285,14 @@ describe('IO', async () => { }); assert.ok(tokenCost); }); + + it('should be able to create IOWriteable with valid signers', async () => { + for (const signer of signers) { + const io = IO.init({ signer }); + + assert(io instanceof IOWriteable); + } + }); }); describe('ANTRegistry', async () => { @@ -276,4 +304,26 @@ describe('ANTRegistry', async () => { assert(Array.isArray(affiliatedAnts.Owned)); assert(Array.isArray(affiliatedAnts.Controlled)); }); + + it('should be able to create AoANTRegistryWriteable with valid signers', async () => { + for (const signer of signers) { + const registry = ANTRegistry.init({ + signer, + }); + assert(registry instanceof AoANTRegistryWriteable); + } + }); +}); + +describe('ANT', async () => { + it('should be able to create ANTWriteable with valid signers', async () => { + for (const signer of signers) { + const ant = ANT.init({ + processId: 'aWI_dq1JH7facsulLuas1X3l5dkKuWtixcZDYMw9mpg', + signer, + }); + + assert(ant instanceof AoANTWriteable); + } + }); }); diff --git a/tests/e2e/esm/index.test.js b/tests/e2e/esm/index.test.js index 98feda2e..704f3a52 100644 --- a/tests/e2e/esm/index.test.js +++ b/tests/e2e/esm/index.test.js @@ -1,7 +1,28 @@ -import { ANTRegistry, IO, ioDevnetProcessId } from '@ar.io/sdk'; +import { + ANT, + ANTRegistry, + AoANTRegistryWriteable, + AoANTWriteable, + ArweaveSigner, + IO, + IOWriteable, + createAoSigner, + ioDevnetProcessId, +} from '@ar.io/sdk'; import { strict as assert } from 'node:assert'; +import fs from 'node:fs'; import { describe, it } from 'node:test'; +const testWalletJSON = fs.readFileSync('../test-wallet.json', { + encoding: 'utf-8', +}); + +const testWallet = JSON.parse(testWalletJSON); +const signers = [ + new ArweaveSigner(testWallet), + createAoSigner(new ArweaveSigner(testWallet)), +]; + /** * Ensure that npm link has been ran prior to running these tests * (simply running npm run test:integration will ensure npm link is ran) @@ -10,6 +31,7 @@ import { describe, it } from 'node:test'; const io = IO.init({ processId: ioDevnetProcessId, }); + describe('IO', async () => { it('should be able to get the process information', async () => { const epoch = await io.getInfo(); @@ -266,6 +288,14 @@ describe('IO', async () => { }); assert.ok(tokenCost); }); + + it('should be able to create IOWriteable with valid signers', async () => { + for (const signer of signers) { + const io = IO.init({ signer }); + + assert(io instanceof IOWriteable); + } + }); }); describe('ANTRegistry', async () => { @@ -277,4 +307,26 @@ describe('ANTRegistry', async () => { assert(Array.isArray(affiliatedAnts.Owned)); assert(Array.isArray(affiliatedAnts.Controlled)); }); + + it('should be able to create AoANTRegistryWriteable with valid signers', async () => { + for (const signer of signers) { + const registry = ANTRegistry.init({ + signer, + }); + assert(registry instanceof AoANTRegistryWriteable); + } + }); +}); + +describe('ANT', async () => { + it('should be able to create ANTWriteable with valid signers', async () => { + for (const signer of signers) { + const ant = ANT.init({ + processId: 'aWI_dq1JH7facsulLuas1X3l5dkKuWtixcZDYMw9mpg', + signer, + }); + + assert(ant instanceof AoANTWriteable); + } + }); }); diff --git a/tests/e2e/test-wallet.json b/tests/e2e/test-wallet.json new file mode 100644 index 00000000..c1243469 --- /dev/null +++ b/tests/e2e/test-wallet.json @@ -0,0 +1,11 @@ +{ + "kty": "RSA", + "n": "s-Whr7NsoyY0x5nFZDAXIzLymEQvK-9ZW_-cH7TKfnxJn24Z9KndUl8fuE_7QRP2VUmlcTM-YjeS7maw68CZrk_2VKbxnN1Gcvj1SqfuvdnZ0aDgD-FQVEpnOWtAToAEZrV-mwiXBHGEIr8_ntmw4TZJwXmNjVUEhA4rVekKDGHD2k-mooc1wliNG-WMS5e-6Qbor_WNbI0G7HI1WJ46qgMZr4aK2OV1WkwL7Z-5xtSUKLJJJ9Uw1BJvjxaOPo3Eu6zJGxnTGMt3xKSD4ZnJV5H16W9609OTURLezVoIQ8VxmF5W_2sawueAp3g2x4dAwaBIhrfQVjn-Fq7bpTKFg1tlfuzIruh0KUXqftYfWpD3qJxHZLnUJl90oCLUw5oDs7Fvx23TToy92ouZdeMhHjIkNRDBGMAuOzrE5qz-f973LDR0Hz88wzBZ7mZDvSA_UEPKVoRZ99ON_w79BU-Pw1XlvYRAAEDrC1zWojVxMGCqRaWR4OMbQE5eWGTE-5tlpb2zuc970ihdwEmS54V6SLzx9mVAy6WfgWMLxYyqjz30538D8ZbUn8mGk4-WVoaXgncPU2F9bVs3bNaTuAWZZxAPZ7E139NTIrW-MbH_nTgch09vq7giI8XEWryWzRSGmZl-IoRN4Nnn2FyoX8nuk7QyZC2j-6y5j4jWXvkJABE", + "e": "AQAB", + "d": "PE0s9YhfUhDsgDu7Puof11yslP3GEiQZAA2ed8JSXjOrOhXd_XUzCvl32IB26EmYuN4G5vsWXjXiuqcRhvT4jsWe_KE5PCuwAboR_wRrspfju7EBalFMa_TExSp-U9H1p7gOyEkI1iR29m7FFKpD8DoSXxgvqsBk0x8sx49mHuBmljc81B4elxa3tjIr5Orow5PdS54z2b8sIvXli97-Kx7-7SdcQ3gm7i2vkeeIjm2TfFDG1ONRisTjQEN_StiaqY4xmzP83sLVUsUSr_ys0P3MQINt2LODHhoFNTCItK7qdPiqkNOGFO8k4P4a3qcnvb_Mj9vtqfmmglP6rFVTCzPHWSSZi2HOZlPGQc53Zx33ZS4moNNTo63hwsa4VKqWTNvZoZF9MafuBQYn_GovkaJkKB542VnhGoT-NsA3HISDhffT6U0q-vWqUOIZmgf8Ut3ghS9XiFUjnBn_EdDix_ryxnmrwpWwUk4q15gDCJ-Ych05uzUbtNPk0LdLRPofpcOJ0uPSv9-5r6ouYJoR92ODxXFhcWGq6GFVT9s9ETNMehw8A54J72n5XfnePF8D5qPe4YZw8DgcIAzmrr42jmNI5YSt3lBVfOoe8meYc_a77t0WmTsIia9nnBpgP4dl12s6EAentsFZfvLRU3cvCy1gsk2En3vPMdiRXJUzrkE", + "p": "9nbx4FZClWKyHFygc3djqRyKff44KD7KFMgOSEKbxvioop1lynSLjTsRMMzT31_htXq0COSLBrUmbzoI5iO8GUmU9F_ht1j4sbwcsYmyyLdNsURWZwYCb00LPDyqvqn87J5IuTifJ4CZB8e1EE3H8HrbnB-uKZJGiEhaIiCCs1-t0sGKY9QtuE2Ovly9RGkj6fi9yqIZyF6ySHopWtLQ90GtVh8El4eIL-PeGPBvahbffUNOwEpfc0QIjOBkrHT8wdlXuU3rWFSi7aKd2mVj6QFiq_pF-Z8ESc0NAQlLIY0kbKKv29blcpY9sgKBpTAYMYRj93AdQstNoddRZ5fdyw", + "q": "utth1reY8OFEDIHe48K_pyyPmoMsRCdFhtXEqBljRNaQKQk6ijajPco7ycYTucef1_TlIBbSfm6WropbRyi9e9ebaA0kWNqPYaef9vtYEwdq3f9YfrHXaVXSGmfWXw-fp81QGm6IDRN3YlRn-_6rWB7MynkvYdJSh7K_GRw39TUkcWIrKNbFmPi1InKi-i9VDTQJL1REkQmAduPScjRzsNGmM3Ngqt3PNfejhronSICSGrwbUJ9-C5sauGAIJo090KS2f8p15ljGQ_X0WT9JWnsZ59K8nL5cLOUVZpKc5U0AoS_HqR7O7BIFLJo8BEWiJd_Jmg-Ln6UCOzxWBZleEw", + "dp": "47PYm66WLXXVoCZjhsoSpTbdLLImJ-h6wuBhcZk4Wod5JWPNm1I5a-3aX_-c746h9Qy8MEsVtsi-DZzMg_MX4TT-DRhYbRAiE_L7f3r8Vjwj51Z_jQccUMAZVTmndieOqP1DqvwI7nH88BctzTZPNnoLUx5hxb6Cs35E56qplhcbfM-aj8iDxPbCnlUH96A7sfpBPmis8VWr2RIkCukibo2bGynlECoDRFt94gSgqp8fM5dvtm__53o_fAgEeuBKdL3cMjMu75iiPnIy7Icn2ymQg9rhs1GaoKR1EzQG2aSQtl1HpA_SRB9SOJfgN0FL2NO7l-tY3VD_FDrd1puUKQ", + "dq": "t3YSl9jnpwnl4Ena59EcjyznShOkcL4GO57DWTCkEMCCBmhzO6TtngtjrHZ4g52GSWi_VkRSI6S8-V4KxNExSdilUwIkP8FHqeAE5WBeV0CfIpxE7Q_7qgaDJT3ycp9KaFzjWzBPEFeejcLF3dtrrDeBZwKZDPiN44ISsrrMDktBHrn-GjjVBZ6badkYP4Adh7shkYCxWZ30rcZ9p3fsZx1Qi4-qx9jES_56Zht72mmyCeHLB9uwzABbuc8_8WoX2TT_onTMUX-0GqHwaXgDs3zOMJjuaw9UPRgnbPBib5itF5Vr-ZawH4SJ5AMDDka4L2uL62F7-yDuEe7pntG4VQ", + "qi": "PYzb643Ko2kHMwP7z44xAoldwzvKfKy-5rKjifzpJ-oX3Nsec99goV5JfDXDDmZhJmfqX93L9znEIFmA17dwzwJ6Gho2ReqDacze-SQNyKywuE77b5XXvCx-lVx1DWS0DTR7ykUkf8K6Pl6JKHNpc0FAeK8olk0PdqVf-TvXE8NjHc1vDsxwK2woiUkizyd-Xn054U1Abe_sy7_gwFFCNo7nUFJxk6AG4v4yinbxn8dAmAATh37sPnB3U4Ac4yNfFYSqf6HZzCMyt2UlPZsLKLZP2OtHoYaHWJWoZjfyChbaEleXTvaDIGzrgrnsOH5fsCvGie1zZAs1X7DhvCiTTQ" +} diff --git a/tests/utils.ts b/tests/utils.ts deleted file mode 100644 index 6e8d25ab..00000000 --- a/tests/utils.ts +++ /dev/null @@ -1,186 +0,0 @@ -import Arweave from 'arweave'; -import { JWKInterface } from 'arweave/node/lib/wallet'; -import * as fs from 'fs'; -import path from 'path'; -import { ContractDeploy, SourceType, Warp } from 'warp-contracts'; - -import { WeightedObserver } from '../src/contract-state.js'; - -const oneYearSeconds = 60 * 60 * 24 * 365; - -export async function deployANTContract({ - jwk, - address, - warp, -}: { - jwk: JWKInterface; - address: string; - warp: Warp; -}): Promise { - const src = fs.readFileSync( - path.join(__dirname, '/integration/arlocal/ant-contract/index.js'), - 'utf8', - ); - const state = JSON.parse( - fs.readFileSync( - path.join( - __dirname, - '/integration/arlocal/ant-contract/initial-state.json', - ), - 'utf8', - ), - ); - return warp.deploy({ - wallet: jwk, - src: src, - initState: JSON.stringify({ - ...state, - owner: address, - controllers: [address], - balances: { [address]: 1000000 }, - }), - evaluationManifest: { - evaluationOptions: { - sourceType: SourceType.ARWEAVE, - }, - }, - }); -} - -export async function deployArIOContract({ - jwk, - address, - warp, - arweave, -}: { - jwk: JWKInterface; - address: string; - warp: Warp; - arweave: Arweave; -}): Promise { - const currentBlockTimestamp = (await arweave.blocks.getCurrent()).timestamp; - const src = fs.readFileSync( - path.join(__dirname, '/integration/arlocal/ar-io-contract/index.js'), - 'utf8', - ); - const state = JSON.parse( - fs.readFileSync( - path.join( - __dirname, - '/integration/arlocal/ar-io-contract/initial-state.json', - ), - 'utf8', - ), - ); - - // add the wallet owner as a prescribed observer and as a gateway - const prescribedObservers: WeightedObserver[] = - state.prescribedObservers['0']; - const lastObserver: WeightedObserver = - prescribedObservers.pop() as WeightedObserver; - const newPrescribedObserver: WeightedObserver = { - ...lastObserver, - gatewayAddress: address, - observerAddress: address, - }; - const updatedPrescribedObservers = [ - ...prescribedObservers, - newPrescribedObserver, - ]; - - return warp.deploy({ - wallet: jwk, - src: src, - initState: JSON.stringify({ - ...state, - records: { - ...state.records, - 'test-record': { - contractTxId: 'I-cxQhfh0Zb9UqQNizC9PiLC41KpUeA9hjiVV02rQRw', - endTimestamp: currentBlockTimestamp + oneYearSeconds, - purchasePrice: 0, - startTimestamp: currentBlockTimestamp, - type: 'lease', - undernames: 10, - }, - 'test-extend': { - contractTxId: 'I-cxQhfh0Zb9UqQNizC9PiLC41KpUeA9hjiVV02rQRw', - endTimestamp: currentBlockTimestamp + oneYearSeconds, - purchasePrice: 0, - startTimestamp: currentBlockTimestamp, - type: 'lease', - undernames: 10, - }, - 'test-undername': { - contractTxId: 'I-cxQhfh0Zb9UqQNizC9PiLC41KpUeA9hjiVV02rQRw', - endTimestamp: currentBlockTimestamp + oneYearSeconds, - purchasePrice: 0, - startTimestamp: currentBlockTimestamp, - type: 'lease', - undernames: 10, - }, - }, - owner: address, - balances: { [address]: 100_000_000_000_000 }, - prescribedObservers: { - 0: updatedPrescribedObservers, - }, - }), - evaluationManifest: { - evaluationOptions: { - sourceType: SourceType.ARWEAVE, - }, - }, - }); -} - -export async function createLocalWallet( - arweave: Arweave, - amount = 10_000_000_000_000, -): Promise<{ wallet: JWKInterface; address: string }> { - // ~~ Generate wallet and add funds ~~ - const wallet = await arweave.wallets.generate(); - const address = await arweave.wallets.jwkToAddress(wallet); - // mint some tokens - await arweave.api.get(`/mint/${address}/${amount}`); - - const walletDir = path.join(__dirname, './wallets'); - const walletPath = path.join(walletDir, `${address}.json`); - // save it to local directory - if (!fs.existsSync(walletPath)) { - fs.writeFileSync(walletPath, JSON.stringify(wallet)); - } - - return { - wallet, - address, - }; -} - -export function removeDirectories() { - ['./wallets', './contracts'].forEach((dir) => { - const dirPath = path.join(__dirname, dir); - if (fs.existsSync(dirPath)) { - fs.rmSync(dirPath, { recursive: true }); - } - }); -} - -export function createDirectories() { - ['./wallets', './contracts'].forEach((dir) => { - const dirPath = path.join(__dirname, dir); - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath); - } - }); -} - -export function mineBlocks({ - arweave, - blocks = 1, -}: { - arweave: Arweave; - blocks?: number; -}) { - return arweave.api.get('/mine/' + blocks); -}