feat: remove contract tx usage from constructor and fix smartweave global after calling constructor

This commit is contained in:
Michał Konopka
2023-03-17 15:56:00 +01:00
committed by just_ppe
parent a7558fd866
commit c1d8fbece5
5 changed files with 109 additions and 53 deletions

View File

@@ -8,6 +8,7 @@ import { WarpFactory } from '../../../core/WarpFactory';
import { LoggerFactory } from '../../../logging/LoggerFactory'; import { LoggerFactory } from '../../../logging/LoggerFactory';
import { DeployPlugin } from 'warp-contracts-plugin-deploy'; import { DeployPlugin } from 'warp-contracts-plugin-deploy';
import { mineBlock } from '../_helpers'; import { mineBlock } from '../_helpers';
import { WriteInteractionResponse } from '../../../contract/Contract';
describe('Constructor', () => { describe('Constructor', () => {
let contractSrc: string; let contractSrc: string;
@@ -145,7 +146,7 @@ describe('Constructor', () => {
await contract.readState(); await contract.readState();
const { cachedValue: kv } = await contract.getStorageValues(['__init']); const { cachedValue: kv } = await contract.getStorageValues(['__init']);
expect(kv.get('__init')).toEqual(contract.txId()); expect(kv.get('__init')).toEqual('KV welcome to');
}); });
}); });
@@ -185,18 +186,61 @@ describe('Constructor', () => {
const contract = await deployContract({ addToState: { accessBlock: true } }); const contract = await deployContract({ addToState: { accessBlock: true } });
await expect(contract.readState()).rejects.toThrowError( await expect(contract.readState()).rejects.toThrowError(
'ConstructorError: SmartWeave.block object is not accessible in constructor' 'ConstructorError: SmartWeave.block.timestamp is not accessible in constructor context'
); );
}); });
it('should fail to access vrf data', async () => { it('should fail to access vrf data in constructor', async () => {
const contract = await deployContract({ addToState: { accessVrf: true } }); const contract = await deployContract({ addToState: { accessVrf: true } });
await expect(contract.readState()).rejects.toThrowError( await expect(contract.readState()).rejects.toThrowError(
'ConstructorError: SmartWeave.vrf object is not accessible in constructor' 'ConstructorError: SmartWeave.vrf.data is not accessible in constructor context'
); );
}); });
it('should fail to access transaction data in constructor', async () => {
const contract = await deployContract({ addToState: { accessTx: true } });
await expect(contract.readState()).rejects.toThrowError(
'ConstructorError: SmartWeave.transaction.id is not accessible in constructor context'
);
});
it('should be able to access SmartWeave global after constructor', async () => {
const contract = await deployContract({});
const contractB = await deployContract({});
const { originalTxId } = (await contract.writeInteraction({
function: 'callMe',
contractB: (contractB as any)._contractTxId
})) as WriteInteractionResponse;
const {
cachedValue: { state }
} = await contract.readState();
expect(state.blockId).toBeDefined();
expect(state.txId).toBe(originalTxId);
expect(state.vrf).toBe(undefined);
});
it('XX should be able to access SmartWeave internal writes after constructor', async () => {
const foreignContract = await deployContract({ withKv: false, src: contractIrSrc });
const contract = await deployContract({
withKv: false,
src: contractIrSrc,
addToState: {
foreignContract: foreignContract.txId()
}
});
console.log((foreignContract as any)._contractTxId);
const response = await contract.viewState({
function: 'read'
});
expect(response.type).toEqual('ok');
});
describe('Internal writes', () => { describe('Internal writes', () => {
it('should throw when using internal writes in contract in __init', async () => { it('should throw when using internal writes in contract in __init', async () => {
const writesInConstructorContract = await deployContract({ const writesInConstructorContract = await deployContract({

View File

@@ -65,7 +65,6 @@ describe('Testing the Profit Sharing Token', () => {
afterAll(async () => { afterAll(async () => {
await arlocal.stop(); await arlocal.stop();
fs.rmSync(`${DEFAULT_LEVEL_DB_LOCATION}/kv/ldb/${contractTxId}`, { recursive: true });
}); });
it('should initialize', async () => { it('should initialize', async () => {

View File

@@ -6,7 +6,7 @@ export async function handle(state, action) {
if (action.input.function == '__init') { if (action.input.function == '__init') {
state.caller = action.caller; state.caller = action.caller;
state.caller2 = SmartWeave.caller; state.caller2 = SmartWeave.caller;
await SmartWeave.kv.put("__init", SmartWeave.transaction.id); await SmartWeave.kv.put("__init", 'KV welcome to');
state.counter = action.input.args.counter + 1; state.counter = action.input.args.counter + 1;
@@ -21,6 +21,22 @@ export async function handle(state, action) {
if (action.input.args.accessVrf) { if (action.input.args.accessVrf) {
SmartWeave.vrf.data; SmartWeave.vrf.data;
} }
if (action.input.args.accessTx) {
SmartWeave.transaction.id;
}
}
if (action.input.function == 'callMe') {
await SmartWeave.kv.put("okey", "okey");
return {
state: {
...state,
blockId: SmartWeave.block.height,
txId: SmartWeave.transaction.id,
vrf: SmartWeave.vrf.data
}
}
} }
return { state } return { state }

View File

@@ -2,7 +2,7 @@ import { GQLNodeInterface } from 'legacy/gqlResult';
import { ContractDefinition } from '../../../../core/ContractDefinition'; import { ContractDefinition } from '../../../../core/ContractDefinition';
import { ExecutionContext } from '../../../../core/ExecutionContext'; import { ExecutionContext } from '../../../../core/ExecutionContext';
import { EvalStateResult } from '../../../../core/modules/StateEvaluator'; import { EvalStateResult } from '../../../../core/modules/StateEvaluator';
import { SmartWeaveGlobal } from '../../../../legacy/smartweave-global'; import { SWBlock, SmartWeaveGlobal, SWTransaction, SWVrf } from '../../../../legacy/smartweave-global';
import { deepCopy, timeout } from '../../../../utils/utils'; import { deepCopy, timeout } from '../../../../utils/utils';
import { ContractInteraction, InteractionData, InteractionResult } from '../HandlerExecutorFactory'; import { ContractInteraction, InteractionData, InteractionResult } from '../HandlerExecutorFactory';
import { genesisSortKey } from '../LexicographicalInteractionsSorter'; import { genesisSortKey } from '../LexicographicalInteractionsSorter';
@@ -57,16 +57,18 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
caller: this.contractDefinition.owner caller: this.contractDefinition.owner
}; };
const interactionTx = (await this.makeInteractionTxFromContractTx( const interactionTx = {
this.contractDefinition.contractTx, owner: { address: executionContext.caller, key: null },
this.contractDefinition.owner sortKey: genesisSortKey
)) as GQLNodeInterface; } as GQLNodeInterface;
const interactionData: InteractionData<Input> = { interaction, interactionTx }; const interactionData: InteractionData<Input> = { interaction, interactionTx };
this.setupSwGlobal(interactionData); this.setupSwGlobal(interactionData);
this.configureSwGlobalForConstructor(); const cleanUpSwGlobal = this.configureSwGlobalForConstructor();
const result = await this.runContractFunction(executionContext, interaction, {} as State); const result = await this.runContractFunction(executionContext, interaction, {} as State);
cleanUpSwGlobal();
if (result.type !== 'ok') { if (result.type !== 'ok') {
throw new Error(`Exception while calling constructor: ${JSON.stringify(interaction)}:\n${result.errorMessage}`); throw new Error(`Exception while calling constructor: ${JSON.stringify(interaction)}:\n${result.errorMessage}`);
} }
@@ -76,21 +78,6 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
} }
} }
private async makeInteractionTxFromContractTx(
contractTx: ContractDefinition<unknown>['contractTx'],
owner: string
): Promise<Omit<GQLNodeInterface, 'anchor' | 'signature' | 'parent' | 'bundledIn' | 'data' | 'block'>> {
return {
id: contractTx.id,
tags: contractTx.tags,
recipient: contractTx.target,
owner: { address: owner, key: null },
quantity: { winston: contractTx.quantity, ar: null },
fee: { winston: contractTx.fee, ar: null },
sortKey: genesisSortKey
};
}
private assertNotConstructorCall<Input>(interaction: ContractInteraction<Input>) { private assertNotConstructorCall<Input>(interaction: ContractInteraction<Input>) {
if ( if (
this.contractDefinition.manifest?.evaluationOptions?.useConstructor && this.contractDefinition.manifest?.evaluationOptions?.useConstructor &&
@@ -101,27 +88,37 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
} }
private configureSwGlobalForConstructor() { private configureSwGlobalForConstructor() {
// disable internal writes const handler = (prop) => ({
const templateErrorMessage = (op) => get: (target, property) =>
`Can't ${op} foreign contract state: Internal writes feature is not available in constructor`; throwErrorWithName(
'ConstructorError',
`SmartWeave.${prop}.${String(property)} is not accessible in constructor context`
)
});
this.swGlobal.contracts.readContractState = () => this.swGlobal.contracts.readContractState = () =>
throwErrorWithName('ConstructorError', templateErrorMessage('readContractState')); throwErrorWithName('ConstructorError', 'Internal writes feature is not available in constructor');
this.swGlobal.contracts.write = () => throwErrorWithName('ConstructorError', templateErrorMessage('write'));
this.swGlobal.contracts.refreshState = () =>
throwErrorWithName('ConstructorError', templateErrorMessage('refreshState'));
this.swGlobal.contracts.viewContractState = () => this.swGlobal.contracts.viewContractState = () =>
throwErrorWithName('ConstructorError', templateErrorMessage('viewContractState')); throwErrorWithName('ConstructorError', 'Internal writes feature is not available in constructor');
this.swGlobal.contracts.refreshState = () =>
throwErrorWithName('ConstructorError', 'Internal writes feature is not available in constructor');
this.swGlobal.contracts.write = () =>
throwErrorWithName('ConstructorError', 'Internal writes feature is not available in constructor');
const disabledVrf = new Proxy(this.swGlobal.vrf, { const originalBlock = new SWBlock(this.swGlobal);
get: () => throwErrorWithName('ConstructorError', `SmartWeave.vrf object is not accessible in constructor`) this.swGlobal.block = new Proxy(this.swGlobal.block, handler('block'));
});
const disabledBlock = new Proxy(this.swGlobal.block, { const originalVrf = new SWVrf(this.swGlobal);
get: () => throwErrorWithName('ConstructorError', 'SmartWeave.block object is not accessible in constructor') this.swGlobal.vrf = new Proxy(this.swGlobal.vrf, handler('vrf'));
});
this.swGlobal.vrf = disabledVrf; const originalTransaction = new SWTransaction(this.swGlobal);
this.swGlobal.block = disabledBlock; this.swGlobal.transaction = new Proxy(this.swGlobal.vrf, handler('transaction'));
return () => {
this.swGlobal.block = originalBlock;
this.swGlobal.vrf = originalVrf;
this.swGlobal.transaction = originalTransaction;
};
} }
private async runContractFunction<Input>( private async runContractFunction<Input>(

View File

@@ -33,9 +33,9 @@ import { BatchDBOp, CacheKey, PutBatch, SortKeyCache } from '../cache/SortKeyCac
export class SmartWeaveGlobal { export class SmartWeaveGlobal {
gasUsed: number; gasUsed: number;
gasLimit: number; gasLimit: number;
transaction: Transaction; transaction: SWTransaction;
block: Block; block: SWBlock;
vrf: Vrf; vrf: SWVrf;
evaluationOptions: EvaluationOptions; evaluationOptions: EvaluationOptions;
arweave: Pick<Arweave, 'ar' | 'wallets' | 'utils' | 'crypto'>; arweave: Pick<Arweave, 'ar' | 'wallets' | 'utils' | 'crypto'>;
contract: { contract: {
@@ -78,8 +78,8 @@ export class SmartWeaveGlobal {
this.evaluationOptions = evaluationOptions; this.evaluationOptions = evaluationOptions;
this.contract = contract; this.contract = contract;
this.transaction = new Transaction(this); this.transaction = new SWTransaction(this);
this.block = new Block(this); this.block = new SWBlock(this);
this.contracts = { this.contracts = {
readContractState: (contractId: string, height?: number, returnValidity?: boolean) => { readContractState: (contractId: string, height?: number, returnValidity?: boolean) => {
throw new Error('Not implemented - should be set by HandlerApi implementor'); throw new Error('Not implemented - should be set by HandlerApi implementor');
@@ -97,7 +97,7 @@ export class SmartWeaveGlobal {
throw new Error('Not implemented - should be set by HandlerApi implementor'); throw new Error('Not implemented - should be set by HandlerApi implementor');
} }
}; };
this.vrf = new Vrf(this); this.vrf = new SWVrf(this);
this.useGas = this.useGas.bind(this); this.useGas = this.useGas.bind(this);
this.getBalance = this.getBalance.bind(this); this.getBalance = this.getBalance.bind(this);
@@ -141,7 +141,7 @@ export class SmartWeaveGlobal {
} }
// tslint:disable-next-line: max-classes-per-file // tslint:disable-next-line: max-classes-per-file
class Transaction { export class SWTransaction {
constructor(private readonly smartWeaveGlobal: SmartWeaveGlobal) {} constructor(private readonly smartWeaveGlobal: SmartWeaveGlobal) {}
get id() { get id() {
@@ -202,7 +202,7 @@ class Transaction {
} }
// tslint:disable-next-line: max-classes-per-file // tslint:disable-next-line: max-classes-per-file
class Block { export class SWBlock {
constructor(private readonly smartWeaveGlobal: SmartWeaveGlobal) {} constructor(private readonly smartWeaveGlobal: SmartWeaveGlobal) {}
get height() { get height() {
@@ -227,7 +227,7 @@ class Block {
} }
} }
class Vrf { export class SWVrf {
constructor(private readonly smartWeaveGlobal: SmartWeaveGlobal) {} constructor(private readonly smartWeaveGlobal: SmartWeaveGlobal) {}
get data(): VrfData { get data(): VrfData {
@@ -257,7 +257,7 @@ class Vrf {
export class KV { export class KV {
private _kvBatch: BatchDBOp<any>[] = []; private _kvBatch: BatchDBOp<any>[] = [];
constructor(private readonly _storage: SortKeyCache<any> | null, private readonly _transaction: Transaction) {} constructor(private readonly _storage: SortKeyCache<any> | null, private readonly _transaction: SWTransaction) {}
async put(key: string, value: any): Promise<void> { async put(key: string, value: any): Promise<void> {
this.checkStorageAvailable(); this.checkStorageAvailable();