diff --git a/jest.config.ts b/jest.config.mjs similarity index 82% rename from jest.config.ts rename to jest.config.mjs index 03c33e1b..84cc60a5 100644 --- a/jest.config.ts +++ b/jest.config.mjs @@ -1,13 +1,14 @@ -module.exports = { +export default { preset: 'ts-jest', clearMocks: true, moduleFileExtensions: ['ts', 'js', 'mjs'], - testMatch: ['**/src/**/*.test.ts', '**/tests/**/*.test.ts'], + testMatch: ['**/src/**/*.test.ts'], collectCoverage: true, collectCoverageFrom: ['src/**/*.ts', 'tests/**/*.ts'], testEnvironment: 'node', testTimeout: 120_000, extensionsToTreatAsEsm: ['.ts'], + passWithNoTests: true, transform: { '^.+\\.(ts|js)$': ['ts-jest', { useESM: true }], }, diff --git a/jest.integration.config.ts b/jest.integration.config.mjs similarity index 96% rename from jest.integration.config.ts rename to jest.integration.config.mjs index aa601444..a1cd37a7 100644 --- a/jest.integration.config.ts +++ b/jest.integration.config.mjs @@ -1,4 +1,4 @@ -module.exports = { +export default { preset: 'ts-jest', setupFiles: ['./tests/integration/jest.setup.ts'], globalTeardown: './tests/integration/jest.teardown.ts', diff --git a/package.json b/package.json index 0001d826..5ac3537b 100644 --- a/package.json +++ b/package.json @@ -62,13 +62,14 @@ "build:esm": "yarn tsc -p tsconfig.json", "build:cjs": "yarn tsc -p tsconfig.cjs.json && echo \"{\\\"type\\\": \\\"commonjs\\\"}\" > lib/cjs/package.json", "build": "yarn clean && yarn build:web && yarn build:esm && yarn build:cjs", - "clean": "rimraf [ lib coverage bundles ]", + "clean": "rimraf [ lib coverage bundles tests/contracts tests/wallets ]", "lint": "eslint src", "lint:fix": "eslint src --fix", "format": "prettier --check .", "format:fix": "prettier --write .", - "test": "yarn clean && jest .", - "test:integration": "docker compose up -d && yarn test --config=jest.integration.config.ts && docker compose down", + "test": "yarn test:unit && yarn test:integration", + "test:unit": "yarn clean && jest --config=jest.config.mjs", + "test:integration": "yarn clean && docker compose up -d && jest --config=jest.integration.config.mjs && docker compose down", "prepare": "husky install", "example:mjs": "yarn build:esm && node examples/node/index.mjs", "example:cjs": "yarn build:cjs && node examples/node/index.cjs", @@ -117,7 +118,7 @@ "axios": "1.4.0", "setimmediate": "^1.0.5", "warp-arbundles": "^1.0.4", - "warp-contracts": "^1.4.38", + "warp-contracts": "1.4.39", "warp-contracts-plugin-deploy": "^1.0.13", "winston": "^3.11.0" }, diff --git a/src/common/contracts/warp-contract.ts b/src/common/contracts/warp-contract.ts index d561eb72..9341975c 100644 --- a/src/common/contracts/warp-contract.ts +++ b/src/common/contracts/warp-contract.ts @@ -15,18 +15,18 @@ * along with this program. If not, see . */ import Arweave from 'arweave'; -import { DataItem, Signer } from 'warp-arbundles'; +import { DataItem } from 'warp-arbundles'; import { Contract, + CustomSignature, InteractionResult, LoggerFactory, + Signature, Transaction, Warp, - WarpFactory, - defaultCacheOptions, } from 'warp-contracts'; -import { defaultArweave } from '../../constants.js'; +import { defaultWarp } from '../../constants.js'; import { BaseContract, ContractSigner, @@ -36,6 +36,7 @@ import { WriteContract, WriteParameters, } from '../../types.js'; +import { sha256B64Url, toB64Url } from '../../utils/base64.js'; import { getContractManifest } from '../../utils/smartweave.js'; import { FailedRequestError, WriteInteractionError } from '../error.js'; import { DefaultLogger } from '../logger.js'; @@ -50,23 +51,17 @@ export class WarpContract private contract: Contract; private contractTxId: string; private cacheUrl: string | undefined; - private arweave: Arweave; - private log: Logger; - private signer: ContractSigner | undefined; + private logger: Logger; + private warp: Warp; + // warp compatible signer that uses ContractSigner + private signer: CustomSignature | undefined; constructor({ contractTxId, cacheUrl, - warp = WarpFactory.forMainnet( - { - ...defaultCacheOptions, - inMemory: true, // default to in memory for now, a custom warp implementation can be provided - }, - true, - ), + warp = defaultWarp, signer, - arweave = defaultArweave, - log = new DefaultLogger({ + logger = new DefaultLogger({ level: 'debug', }), }: { @@ -75,13 +70,13 @@ export class WarpContract warp?: Warp; signer?: ContractSigner; arweave?: Arweave; - log?: Logger; + logger?: Logger; }) { this.contractTxId = contractTxId; - this.contract = warp.contract(contractTxId); + this.contract = warp.contract(contractTxId); this.cacheUrl = cacheUrl; - this.arweave = arweave; - this.log = log; + this.warp = warp; + this.logger = logger; if (signer) { this.connect(signer); } @@ -94,13 +89,25 @@ export class WarpContract }; } - // base contract methods connect(signer: ContractSigner) { - // TODO: Update type to use Signer interface - this.signer = signer; - this.contract = this.contract.connect(signer as Signer); + const warpSigner = new Signature(this.warp, { + signer: async (tx: Transaction) => { + const dataToSign = await tx.getSignatureData(); + const signatureBuffer = Buffer.from(await signer.sign(dataToSign)); + const id = sha256B64Url(signatureBuffer); + tx.setSignature({ + id: id, + owner: toB64Url(signer.publicKey), + signature: toB64Url(signatureBuffer), + }); + }, + type: 'arweave', + }); + this.contract = this.contract.connect(warpSigner); + this.signer = warpSigner; return this; } + async getState({ evaluationOptions = {} }: EvaluationParameters): Promise { await this.ensureContractInit(); const evalTo = evaluationOptions?.evalTo; @@ -120,16 +127,16 @@ export class WarpContract } async ensureContractInit(): Promise { - this.log.debug(`Checking contract initialized`, { + this.logger.debug(`Checking contract initialized`, { contractTxId: this.contractTxId, }); // Get contact manifest and sync state - this.log.debug(`Fetching contract manifest`, { + this.logger.debug(`Fetching contract manifest`, { contractTxId: this.contractTxId, }); const { evaluationOptions = {} } = await getContractManifest({ - arweave: this.arweave, + arweave: this.warp.arweave, contractTxId: this.contractTxId, }); this.contract.setEvaluationOptions(evaluationOptions); @@ -138,7 +145,7 @@ export class WarpContract private async syncState() { if (this.cacheUrl !== undefined) { - this.log.debug(`Syncing contract state`, { + this.logger.debug(`Syncing contract state`, { contractTxId: this.contractTxId, remoteCacheUrl: this.cacheUrl, }); @@ -179,7 +186,7 @@ export class WarpContract async writeInteraction({ functionName, inputs, - dryWrite = true, + dryWrite = false, }: EvaluationParameters>): Promise< Transaction | DataItem | InteractionResult > { @@ -189,7 +196,7 @@ export class WarpContract 'Contract not connected - call .connect(signer) to connect a signer for write interactions ', ); } - this.log.debug(`Write interaction: ${functionName}`, { + this.logger.debug(`Write interaction: ${functionName}`, { contractTxId: this.contractTxId, }); // Sync state before writing @@ -207,7 +214,7 @@ export class WarpContract } if (dryWrite) { - this.log.debug(`Dry write interaction successful`, { + this.logger.debug(`Dry write interaction successful`, { contractTxId: this.contractTxId, functionName, }); diff --git a/src/constants.ts b/src/constants.ts index fee4d749..8a2b45f1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -15,6 +15,7 @@ * along with this program. If not, see . */ import Arweave from 'arweave'; +import { WarpFactory, defaultCacheOptions } from 'warp-contracts'; export const ARWEAVE_TX_REGEX = new RegExp('^[a-zA-Z0-9_-]{43}$'); // sortkey: padded blockheight to 12, JS timestamp, hash of transactionID + block hash. Timestamp only applicable to L2 and normally is all zeros. @@ -32,3 +33,12 @@ export const defaultArweave = Arweave.init({ port: 443, protocol: 'https', }); + +export const defaultWarp = WarpFactory.forMainnet( + { + ...defaultCacheOptions, + inMemory: true, + }, + true, + defaultArweave, +); diff --git a/src/utils/base64.ts b/src/utils/base64.ts new file mode 100644 index 00000000..8b00ac3b --- /dev/null +++ b/src/utils/base64.ts @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { bufferTob64Url } from 'arweave/node/lib/utils.js'; +import { createHash } from 'crypto'; + +export function fromB64Url(input: string): Buffer { + const paddingLength = input.length % 4 === 0 ? 0 : 4 - (input.length % 4); + + const base64 = input + .replace(/-/g, '+') + .replace(/_/g, '/') + .concat('='.repeat(paddingLength)); + + return Buffer.from(base64, 'base64'); +} + +export function toB64Url(buffer: Buffer): string { + return bufferTob64Url(buffer); +} + +export function sha256B64Url(input: Buffer): string { + return toB64Url(createHash('sha256').update(input).digest()); +} diff --git a/tests/integration/ant.test.ts b/tests/integration/ant.test.ts index 6b18200f..a1fbaa78 100644 --- a/tests/integration/ant.test.ts +++ b/tests/integration/ant.test.ts @@ -2,6 +2,7 @@ import { ArweaveSigner } from 'arbundles'; import { ANT } from '../../src/common/ant'; import { RemoteContract } from '../../src/common/contracts/remote-contract'; +import { DefaultLogger } from '../../src/common/logger'; import { ANTState } from '../../src/contract-state'; import { arweave, @@ -12,26 +13,19 @@ import { const contractTxId = 'UC2zwawQoTnh0TNd9mYLQS4wObBBeaOU5LPQTNETqA4'; const localCacheUrl = `https://api.arns.app`; describe('ANT contract apis', () => { - const signer = new ArweaveSigner(JSON.parse(process.env.PRIMARY_WALLET_JWK!)); const ant = new ANT({ - signer, contract: new RemoteContract({ cacheUrl: localCacheUrl, contractTxId, + logger: new DefaultLogger({ level: 'none' }), }), }); it('should connect and return a valid instance', async () => { const jwk = await arweave.wallets.generate(); const signer = new ArweaveSigner(jwk); - const connectAnt = new ANT({ - contract: new RemoteContract({ - cacheUrl: localCacheUrl, - contractTxId, - }), - }); - expect(connectAnt.connect(signer)).toBeDefined(); - expect(connectAnt).toBeInstanceOf(ANT); + expect(ant.connect(signer)).toBeDefined(); + expect(ant).toBeInstanceOf(ANT); }); it.each([ @@ -147,10 +141,9 @@ describe('ANT contract apis', () => { }, ); - it('Should get state with warp contract', async () => { + it('should get state with warp contract', async () => { const jwk = await arweave.wallets.generate(); const signer = new ArweaveSigner(jwk); - // connecting updates contract to use warp ant.connect(signer); const state = await ant.getState(); expect(state).toBeDefined(); diff --git a/tests/integration/ar-io.test.ts b/tests/integration/ar-io.test.ts index 7e352025..de84088d 100644 --- a/tests/integration/ar-io.test.ts +++ b/tests/integration/ar-io.test.ts @@ -2,6 +2,7 @@ import { ArweaveSigner } from 'arbundles'; import { ArIO } from '../../src/common/ar-io.js'; import { RemoteContract } from '../../src/common/contracts/remote-contract.js'; +import { DefaultLogger } from '../../src/common/logger.js'; import { ARNS_DEVNET_REGISTRY_TX } from '../../src/constants.js'; import { ArIOState } from '../../src/contract-state.js'; import { @@ -20,6 +21,7 @@ describe('ArIO Client', () => { contract: new RemoteContract({ cacheUrl: localCacheUrl, contractTxId, + logger: new DefaultLogger({ level: 'none' }), }), }); diff --git a/tests/integration/arlocal/ant-contract/index.js b/tests/integration/arlocal/ant-contract/index.js index 32c0ba6b..a9f7bdce 100644 --- a/tests/integration/arlocal/ant-contract/index.js +++ b/tests/integration/arlocal/ant-contract/index.js @@ -1136,7 +1136,6 @@ var setTicker = async (state, { caller, input }) => { const controllers = state.controllers; const { ticker } = input; if (!validateSetTicker(input)) { - console.log(input); throw new ContractError(INVALID_INPUT_MESSAGE); } if (caller !== owner && !controllers.includes(caller)) { diff --git a/tests/integration/jest.setup.ts b/tests/integration/jest.setup.ts index 4fbf4ff1..5783e9e1 100644 --- a/tests/integration/jest.setup.ts +++ b/tests/integration/jest.setup.ts @@ -22,8 +22,8 @@ async function jestGlobalSetup() { // deploy example any contract const [arIOContractDeploy, antContractDeploy] = await Promise.all([ - deployArIOContract({ jwk: wallet, warp }), - deployANTContract({ jwk: wallet, warp }), + deployArIOContract({ jwk: wallet, address, warp }), + deployANTContract({ jwk: wallet, address, warp }), ]); // set in the environment diff --git a/tests/integration/warp-contract.test.ts b/tests/integration/warp-contract.test.ts index 58f564d7..1af10598 100644 --- a/tests/integration/warp-contract.test.ts +++ b/tests/integration/warp-contract.test.ts @@ -1,8 +1,9 @@ import { ArweaveSigner } from 'arbundles'; -import { Transaction } from 'warp-contracts'; +import Transaction from 'arweave/node/lib/transaction'; import { WarpContract } from '../../src/common/contracts/warp-contract'; import { WriteInteractionError } from '../../src/common/error'; +import { DefaultLogger } from '../../src/common/logger'; import { ANTState } from '../../src/contract-state'; import { arweave, localCacheUrl, warp } from '../constants'; @@ -11,13 +12,14 @@ describe('warp-contract client', () => { let contractTxId: string; let contract: WarpContract; - beforeAll(() => { + beforeAll(async () => { contractTxId = process.env.DEPLOYED_ANT_CONTRACT_TX_ID!; signer = new ArweaveSigner(JSON.parse(process.env.PRIMARY_WALLET_JWK!)); contract = new WarpContract({ cacheUrl: localCacheUrl, contractTxId, warp, + logger: new DefaultLogger({ level: 'none' }), }); }); @@ -38,7 +40,6 @@ describe('warp-contract client', () => { console.error(e); return e; }); - expect(tx).toBeDefined(); expect(tx).toBeInstanceOf(Transaction); }); @@ -48,6 +49,7 @@ describe('warp-contract client', () => { cacheUrl: localCacheUrl, contractTxId, arweave, + logger: new DefaultLogger({ level: 'none' }), }).connect(signer); const error = await contract diff --git a/tests/utils.ts b/tests/utils.ts index 53ba6682..054c4a32 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -6,9 +6,11 @@ import { ContractDeploy, Warp } from 'warp-contracts'; export async function deployANTContract({ jwk, + address, warp, }: { jwk: JWKInterface; + address: string; warp: Warp; }): Promise { const src = fs.readFileSync( @@ -24,24 +26,25 @@ export async function deployANTContract({ 'utf8', ), ); - const owner = await warp.arweave.wallets.jwkToAddress(jwk); return await warp.deploy({ wallet: jwk, src: src, initState: JSON.stringify({ ...state, - owner, - controllers: [owner], - balances: { [owner]: 1000000 }, + owner: address, + controllers: [address], + balances: { [address]: 1000000 }, }), }); } export async function deployArIOContract({ jwk, + address, warp, }: { jwk: JWKInterface; + address: string; warp: Warp; }): Promise { const src = fs.readFileSync( @@ -57,14 +60,13 @@ export async function deployArIOContract({ 'utf8', ), ); - const owner = await warp.arweave.wallets.jwkToAddress(jwk); return await warp.deploy({ wallet: jwk, src: src, initState: JSON.stringify({ ...state, - owner, - balances: { [owner]: 1 * 1_000_000 * 1_000_000 }, + owner: address, + balances: { [address]: 1 * 1_000_000 * 1_000_000 }, }), }); } diff --git a/yarn.lock b/yarn.lock index cdbee451..f02c413c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10215,7 +10215,7 @@ warp-contracts-plugin-deploy@^1.0.13: node-pre-gyp "^0.17.0" node-stdlib-browser "^1.2.0" -warp-contracts@^1.4.38: +warp-contracts@1.4.39: version "1.4.39" resolved "https://registry.yarnpkg.com/warp-contracts/-/warp-contracts-1.4.39.tgz#b56f4baa61b633c04d8abba65ee5dfca333ffea5" integrity sha512-Dyk9wjhYceLRSq+U5Ba8h2mTbMPeB1hjdnDhYJP825E7gcc3jGc12GTLynSBqx5eJscOtpGG5Z5SQNxcTOP7/A==