Skip to content

Commit

Permalink
Merge pull request #26 from ar-io/PE-5753-gar-write
Browse files Browse the repository at this point in the history
feat(PE-5753): implement writeInteraction on WarpContracts class using warp
  • Loading branch information
atticusofsparta authored Mar 27, 2024
2 parents e1d993b + f613b56 commit 0e5129c
Show file tree
Hide file tree
Showing 25 changed files with 23,095 additions and 136 deletions.
3 changes: 1 addition & 2 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ src/**/*.test.ts
lib
**/*.cjs
**/*.mjs
examples
jest.config.ts
examples
8 changes: 7 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
version: '3'

services:
arlocal:
image: textury/arlocal:v1.1.35
ports:
- '1984:1984'
arns-service:
image: ghcr.io/ar-io/arns-service:latest
build: .
ports:
- '3000:3000'
environment:
- LOG_LEVEL=debug
- GATEWAY_HOST=arlocal
- GATEWAY_PORT=1984
- GATEWAY_PROTOCOL=http
- PREFETCH_CONTRACTS=true
- PREFETCH_CONTRACT_IDS=_NctcA2sRy1-J4OmIQZbYFPM17piNcbdBPH2ncX2RL8,UC2zwawQoTnh0TNd9mYLQS4wObBBeaOU5LPQTNETqA4
- BOOTSTRAP_CONTRACTS=false
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
Expand Down
5 changes: 3 additions & 2 deletions jest.config.cjs → jest.config.mjs
Original file line number Diff line number Diff line change
@@ -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 }],
},
Expand Down
19 changes: 19 additions & 0 deletions jest.integration.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export default {
preset: 'ts-jest',
setupFiles: ['./tests/integration/jest.setup.ts'],
globalTeardown: './tests/integration/jest.teardown.ts',
clearMocks: true,
moduleFileExtensions: ['ts', 'js', 'mjs'],
testMatch: ['**/tests/**/*.test.ts'],
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', 'tests/**/*.ts'],
testEnvironment: 'node',
testTimeout: 120_000,
extensionsToTreatAsEsm: ['.ts'],
transform: {
'^.+\\.(ts|js)$': ['ts-jest', { useESM: true }],
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
};
13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 && 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",
Expand Down Expand Up @@ -113,10 +114,12 @@
},
"dependencies": {
"arbundles": "0.11.0",
"arweave": "^1.14.4",
"arweave": "1.14.4",
"axios": "1.4.0",
"setimmediate": "^1.0.5",
"warp-contracts": "^1.4.38",
"warp-arbundles": "^1.0.4",
"warp-contracts": "1.4.39",
"warp-contracts-plugin-deploy": "^1.0.13",
"winston": "^3.11.0"
},
"lint-staged": {
Expand Down
18 changes: 13 additions & 5 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ArconnectSigner, ArweaveSigner } from 'arbundles';
import { DataItem } from 'warp-arbundles';
import { InteractionResult, Transaction } from 'warp-contracts';

import {
ANTRecord,
Expand Down Expand Up @@ -68,6 +70,13 @@ export type EvaluationParameters<T = NonNullable<unknown>> = {
evaluationOptions?: EvaluationOptions | Record<string, never> | undefined;
} & T;

export type WriteParameters<Input> = {
functionName: string;
inputs: Input;
dryWrite?: boolean;
// TODO: add syncState and abortSignal options
};

export interface BaseContract<T> {
getState(params: EvaluationParameters): Promise<T>;
connect(signer: ContractSigner): this;
Expand All @@ -85,14 +94,13 @@ export interface ReadContract {
}

export interface WriteContract {
writeInteraction<Input, State>({
writeInteraction<Input>({
functionName,
inputs,
evaluationOptions,
}: EvaluationParameters<{
functionName: string;
inputs: Input;
}>): Promise<State>;
}: EvaluationParameters<WriteParameters<Input>>): Promise<
Transaction | DataItem | InteractionResult<unknown, unknown>
>;
}

export interface SmartWeaveContract<T> {
Expand Down
2 changes: 1 addition & 1 deletion src/common/ar-io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export class ArIO implements ArIOContract, BaseContract<ArIOState> {
evaluationOptions,
}: {
evaluationOptions?: EvaluationOptions | Record<string, never> | undefined;
}): Promise<Record<string, ArNSAuctionData>> {
} = {}): Promise<Record<string, ArNSAuctionData>> {
const { auctions } = await this.contract.getState({
evaluationOptions,
});
Expand Down
143 changes: 125 additions & 18 deletions src/common/contracts/warp-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,51 +14,72 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Signer } from 'arbundles';
import Arweave from 'arweave';
import { DataItem } from 'warp-arbundles';
import {
Contract,
CustomSignature,
InteractionResult,
LoggerFactory,
Signature,
Transaction,
Warp,
WarpFactory,
defaultCacheOptions,
} from 'warp-contracts';

import { defaultWarp } from '../../constants.js';
import {
BaseContract,
ContractSigner,
EvaluationParameters,
Logger,
ReadContract,
WriteContract,
WriteParameters,
} from '../../types.js';
import { FailedRequestError } from '../error.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';

LoggerFactory.INST.setOptions({
logLevel: 'fatal',
});

export class WarpContract<T> implements BaseContract<T>, ReadContract {
export class WarpContract<T>
implements BaseContract<T>, ReadContract, WriteContract
{
private contract: Contract<T>;
private contractTxId: string;
private cacheUrl: string | 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,
logger = new DefaultLogger({
level: 'debug',
}),
}: {
contractTxId: string;
cacheUrl?: string;
warp?: Warp;
signer?: ContractSigner;
arweave?: Arweave;
logger?: Logger;
}) {
this.contractTxId = contractTxId;
this.contract = warp.contract<T>(contractTxId);
this.contract = warp.contract(contractTxId);
this.cacheUrl = cacheUrl;
this.warp = warp;
this.logger = logger;
if (signer) {
this.connect(signer);
}
}

configuration(): { contractTxId: string; cacheUrl: string | undefined } {
Expand All @@ -68,14 +89,27 @@ export class WarpContract<T> implements BaseContract<T>, ReadContract {
};
}

// base contract methods
connect(signer: ContractSigner) {
// TODO: Update type to use Signer interface
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<T> {
await this.syncState();
await this.ensureContractInit();
const evalTo = evaluationOptions?.evalTo;
let sortKeyOrBlockHeight: string | number | undefined;
if (evalTo && 'sortKey' in evalTo) {
Expand All @@ -92,9 +126,29 @@ export class WarpContract<T> implements BaseContract<T>, ReadContract {
return evaluationResult.cachedValue.state as T;
}

async ensureContractInit(): Promise<void> {
this.logger.debug(`Checking contract initialized`, {
contractTxId: this.contractTxId,
});

// Get contact manifest and sync state
this.logger.debug(`Fetching contract manifest`, {
contractTxId: this.contractTxId,
});
const { evaluationOptions = {} } = await getContractManifest({
arweave: this.warp.arweave,
contractTxId: this.contractTxId,
});
this.contract.setEvaluationOptions(evaluationOptions);
await this.syncState();
}

private async syncState() {
// TODO: get contract manifest and set it before evaluating
if (this.cacheUrl !== undefined) {
this.logger.debug(`Syncing contract state`, {
contractTxId: this.contractTxId,
remoteCacheUrl: this.cacheUrl,
});
await this.contract.syncState(
`${this.cacheUrl}/v1/contract/${this.contractTxId}`,
{
Expand Down Expand Up @@ -128,4 +182,57 @@ export class WarpContract<T> implements BaseContract<T>, ReadContract {

return evaluationResult.result;
}

async writeInteraction<Input>({
functionName,
inputs,
dryWrite = false,
}: EvaluationParameters<WriteParameters<Input>>): Promise<
Transaction | DataItem | InteractionResult<unknown, unknown>
> {
try {
if (!this.signer) {
throw new Error(
'Contract not connected - call .connect(signer) to connect a signer for write interactions ',
);
}
this.logger.debug(`Write interaction: ${functionName}`, {
contractTxId: this.contractTxId,
});
// Sync state before writing
await this.ensureContractInit();

// run dry write before actual write
const result = await this.contract.dryWrite<Input>({
function: functionName,
...inputs,
});
if (result.type !== 'ok') {
throw new Error(
`Failed to dry write contract interaction ${functionName}: ${result.errorMessage}`,
);
}

if (dryWrite) {
this.logger.debug(`Dry write interaction successful`, {
contractTxId: this.contractTxId,
functionName,
});
return result;
}

const writeResult = await this.contract.writeInteraction<Input>({
function: functionName,
...inputs,
});

if (!writeResult?.interactionTx) {
throw new Error(`Failed to write contract interaction ${functionName}`);
}

return writeResult.interactionTx;
} catch (error) {
throw new WriteInteractionError(error.message);
}
}
}
20 changes: 5 additions & 15 deletions src/common/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,16 @@ export class BaseError extends Error {
}
}

export class NotFound extends BaseError {
constructor(message: string) {
super(message);
}
}
export class NotFound extends BaseError {}

export class BadRequest extends BaseError {
constructor(message: string) {
super(message);
}
}
export class BadRequest extends BaseError {}

export class FailedRequestError extends BaseError {
constructor(status: number, message: string) {
super(`Failed request: ${status}: ${message}`);
}
}

export class UnknownError extends BaseError {
constructor(message: string) {
super(message);
}
}
export class UnknownError extends BaseError {}

export class WriteInteractionError extends BaseError {}
Loading

0 comments on commit 0e5129c

Please sign in to comment.