Skip to content

Commit

Permalink
fix(write): update write methods on warp
Browse files Browse the repository at this point in the history
  • Loading branch information
Atticus committed Mar 25, 2024
1 parent 970bdef commit 9c0540b
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 73 deletions.
5 changes: 2 additions & 3 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,10 @@ export type EvaluationParameters<T = NonNullable<unknown>> = {
} & T;

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

export interface BaseContract<T> {
Expand Down
107 changes: 70 additions & 37 deletions src/common/contracts/warp-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { DataItem, Signer, Transaction } from 'arbundles';
import Arweave from 'arweave';
import {
Contract,
EvaluationOptions,
LoggerFactory,
Warp,
WarpFactory,
Expand All @@ -32,7 +34,9 @@ import {
WriteParameters,
} from '../../types.js';
import { isDataItem, isTransaction } from '../../utils/arweave.js';
import { getContractManifest } from '../../utils/smartweave.js';
import { FailedRequestError, WriteInteractionError } from '../error.js';
import { DefaultLogger } from '../logger.js';

LoggerFactory.INST.setOptions({
logLevel: 'fatal',
Expand All @@ -44,6 +48,15 @@ export class WarpContract<T>
private contract: Contract<T>;
private contractTxId: string;
private cacheUrl: string | undefined;
private arweave = Arweave.init({
host: 'arweave.net',
port: 443,
protocol: 'https',
});
private warpEvaluationOptions: Partial<EvaluationOptions> | undefined;
private log = new DefaultLogger({
level: 'debug',
});

constructor({
contractTxId,
Expand All @@ -55,15 +68,18 @@ export class WarpContract<T>
},
true,
),
warpEvaluationOptions,
}: {
contractTxId: string;
cacheUrl?: string;
warp?: Warp;
signer?: ContractSigner;
warpEvaluationOptions?: Partial<EvaluationOptions>;
}) {
this.contractTxId = contractTxId;
this.contract = warp.contract<T>(contractTxId);
this.cacheUrl = cacheUrl;
this.warpEvaluationOptions = warpEvaluationOptions;
}

configuration(): { contractTxId: string; cacheUrl: string | undefined } {
Expand All @@ -80,7 +96,7 @@ export class WarpContract<T>
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 @@ -97,8 +113,26 @@ export class WarpContract<T>
return evaluationResult.cachedValue.state as T;
}

async ensureContractInit(): Promise<void> {
this.log.debug(`Checking contract initialized`, {
contractTxId: this.contractTxId,
});
// Get contact manifest and sync state
if (this.warpEvaluationOptions === undefined) {
this.log.debug(`Contract not initialized - syncing state and manifest`, {
contractTxId: this.contractTxId,
});
const { evaluationOptions = {} } = await getContractManifest({
arweave: this.arweave,
contractTxId: this.contractTxId,
});
this.contract.setEvaluationOptions(evaluationOptions);
this.warpEvaluationOptions = evaluationOptions;
await this.syncState();
}
}

private async syncState() {
// TODO: get contract manifest and set it before evaluating
if (this.cacheUrl !== undefined) {
await this.contract.syncState(
`${this.cacheUrl}/v1/contract/${this.contractTxId}`,
Expand Down Expand Up @@ -137,49 +171,48 @@ export class WarpContract<T>
async writeInteraction<Input>({
functionName,
inputs,
syncState = true,
dryRun = true,
abortSignal,
}: EvaluationParameters<WriteParameters<Input>>): Promise<
Transaction | DataItem
> {
let writeResult: unknown;

while (!abortSignal || !abortSignal.aborted) {
// Sync state before writing
if (syncState) await this.syncState();

if (dryRun) {
const { errorMessage, type } = await this.contract.dryWrite<Input>({
function: functionName,
...inputs,
});
// type is ok, error, exception
if (type !== 'ok') {
throw new WriteInteractionError(
`Failed to dry run contract interaction ${functionName} with error: ${errorMessage}`,
);
}
}
const { interactionTx } =
(await this.contract.writeInteraction<Input>({
function: functionName,
...inputs,
})) ?? {};

if (!interactionTx) {
this.log.debug(`Write interaction: ${functionName}`, {
contractTxId: this.contractTxId,
});
// Sync state before writing
await this.ensureContractInit();

if (dryRun) {
const { errorMessage, type } = await this.contract.dryWrite<Input>({
function: functionName,
...inputs,
});
// type is ok, error, exception
if (type !== 'ok') {
throw new WriteInteractionError(
`Failed to write contract interaction ${functionName}`,
`Failed to dry run contract interaction ${functionName} with error: ${errorMessage}`,
);
}
writeResult = interactionTx;
}

// Flexible way to return information on the transaction, aids in caching and redoployment if desired by simply refetching tx anchor and resigning.
if (isTransaction(writeResult)) {
return writeResult as Transaction;
} else if (isDataItem(writeResult)) {
return writeResult as DataItem;
const { interactionTx } =
(await this.contract.writeInteraction<Input>({
function: functionName,
...inputs,
})) ?? {};

// Flexible way to return information on the transaction, aids in caching and re-deployment if desired by simply refetching tx anchor and resigning.
if (
(interactionTx && isTransaction(interactionTx)) ||
(interactionTx && isDataItem(interactionTx))
) {
this.log.debug(`Write interaction succesful`, {
contractTxId: this.contractTxId,
functionName,
interactionTx: {
id: interactionTx.id,
tags: interactionTx.tags,
},
});
return interactionTx;
}

throw new WriteInteractionError(
Expand Down
24 changes: 4 additions & 20 deletions src/common/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +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 {
constructor(message: string) {
super(message);
}
}
export class WriteInteractionError extends BaseError {}
61 changes: 48 additions & 13 deletions src/utils/arweave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { DataItem, Transaction } from 'arbundles';
import Arweave from 'arweave';
import { Tag } from 'warp-contracts/web';

import { BlockHeight } from '../common.js';
import { ARWEAVE_TX_REGEX } from '../constants.js';
Expand All @@ -28,18 +28,53 @@ export function isBlockHeight(height: string | number): height is BlockHeight {
return height !== undefined && !isNaN(parseInt(height.toString()));
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isTransaction = (tx: any): tx is Transaction => {
const testTx = Arweave.init({}).createTransaction({ data: 'test' });
const testTxKeys = Object.keys(testTx);
const txKeys = Object.keys(tx);
return txKeys.every((key) => testTxKeys.includes(key));
export const dummyTransaction: Transaction = new Transaction({
attributes: {
format: 2,
id: 'dummy',
last_tx: 'dummy',
owner: 'dummy',
tags: [],
target: 'dummy',
quantity: 'dummy',
data: Buffer.from('dummy'),
reward: 'dummy',
signature: 'dummy',
data_size: 'dummy',
data_root: 'dummy',
}, // deps unnecesaary for testing and type checking
deps: {} as any, // eslint-disable-line @typescript-eslint/no-explicit-any
});

export const dummyDataItem: DataItem = new DataItem(Buffer.from('dummy'));

export const isTransaction = (tx: object): tx is Transaction => {
try {
const testTxKeys = Object.keys(dummyTransaction);
const txKeys = Object.keys(tx);
return txKeys.every((key) => testTxKeys.includes(key));
} catch (error: unknown) {
return false;
}
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isDataItem = (item: any): item is DataItem => {
const testItem = new DataItem(Buffer.from('test'));
const testItemKeys = Object.keys(testItem);
const itemKeys = Object.keys(item);
return itemKeys.every((key) => testItemKeys.includes(key));
export const isDataItem = (item: object): item is DataItem => {
try {
const testItemKeys = Object.keys(dummyDataItem);
const itemKeys = Object.keys(item);
return itemKeys.every((key) => testItemKeys.includes(key));
} catch (error) {
return false;
}
};

export function tagsToObject(tags: Tag[]): {
[x: string]: string;
} {
return tags.reduce((decodedTags: { [x: string]: string }, tag) => {
const key = tag.get('name', { decode: true, string: true });
const value = tag.get('value', { decode: true, string: true });
decodedTags[key] = value;
return decodedTags;
}, {});
}
19 changes: 19 additions & 0 deletions src/utils/smartweave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@
* 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 Arweave from 'arweave';
import { EvaluationManifest } from 'warp-contracts/web';

import { SortKey } from '../common.js';
import { SORT_KEY_REGEX } from '../constants.js';
import { tagsToObject } from './arweave.js';

export function isSortKey(sortKey: string): sortKey is SortKey {
return SmartWeaveSortKey.validate(sortKey);
Expand Down Expand Up @@ -52,3 +56,18 @@ export class SmartWeaveSortKey {
return this.parts()[2];
}
}

export async function getContractManifest({
arweave,
contractTxId,
}: {
arweave: Arweave;
contractTxId: string;
}): Promise<EvaluationManifest> {
const { tags: encodedTags } = await arweave.transactions.get(contractTxId);
const decodedTags = tagsToObject(encodedTags);
const contractManifestString = decodedTags['Contract-Manifest'] ?? '{}';
// TODO throw if manifest is missing
const contractManifest = JSON.parse(contractManifestString);
return contractManifest;
}

0 comments on commit 9c0540b

Please sign in to comment.