fix: viewState interaction transaction does not take 'evolve' into account #14

This commit is contained in:
ppedziwiatr
2021-09-08 18:03:47 +02:00
committed by Piotr Pędziwiatr
parent 717c325a36
commit ce5a589b10
13 changed files with 206 additions and 54 deletions

13
.github/workflows/tests.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: CI
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install modules
run: yarn
- name: Run unit tests
run: yarn test:unit
- name: Run integration tests
run: yarn test:integration

View File

@@ -18,6 +18,7 @@ module.exports = {
testPathIgnorePatterns: [
"/.yalc/",
"/data/"
],
testEnvironment: 'node',

View File

@@ -24,7 +24,10 @@
"version": "yarn format && git add -A src",
"postversion": "git push && git push --tags",
"yalc:publish": "yarn build && yalc publish --push",
"test": "jest"
"test": "jest",
"test:unit": "jest ./src/__tests__/unit",
"test:integration": "jest ./src/__tests__/integration",
"test:regression": "jest ./src/__tests__/regression"
},
"license": "MIT",
"author": "Redstone Team <dev@redstone.finance>",

View File

@@ -0,0 +1,66 @@
export function handle(state, action) {
const balances = state.balances;
const canEvolve = state.canEvolve;
const input = action.input;
const caller = action.caller;
if (input.function === 'transfer') {
const target = input.target;
const qty = input.qty;
if (!Number.isInteger(qty)) {
throw new ContractError('Invalid value for "qty". Must be an integer');
}
if (!target) {
throw new ContractError('No target specified');
}
if (qty <= 0 || caller === target) {
throw new ContractError('Invalid token transfer');
}
if (balances[caller] < qty) {
throw new ContractError(`Caller balance not high enough to send ${qty} token(s)!`);
}
// Lower the token balance of the caller
balances[caller] -= qty;
if (target in balances) {
// Wallet already exists in state, add new tokens
balances[target] += qty;
} else {
// Wallet is new, set starting balance
balances[target] = qty;
}
return { state };
}
if (input.function === 'balance') {
const target = input.target;
const ticker = state.ticker;
if (typeof target !== 'string') {
throw new ContractError('Must specify target to get balance for');
}
if (typeof balances[target] !== 'number') {
throw new ContractError('Cannot get balance, target does not exist');
}
return { result: { target, ticker, balance: balances[target] + 555 } };
}
if (input.function === 'evolve' && canEvolve) {
if (state.owner !== caller) {
throw new ContractError('Only the owner can evolve a contract.');
}
state.evolve = input.value;
return { state };
}
throw new ContractError(`No function supplied or function not recognised: "${input.function}"`);
}

View File

@@ -1,56 +1,55 @@
export function handle(state, action) {
const balances = state.balances
const canEvolve = state.canEvolve
const input = action.input
const caller = action.caller
const balances = state.balances;
const canEvolve = state.canEvolve;
const input = action.input;
const caller = action.caller;
if (input.function === 'transfer') {
const target = input.target
const qty = input.qty
const target = input.target;
const qty = input.qty;
if (!Number.isInteger(qty)) {
throw new ContractError('Invalid value for "qty". Must be an integer')
throw new ContractError('Invalid value for "qty". Must be an integer');
}
if (!target) {
throw new ContractError('No target specified')
throw new ContractError('No target specified');
}
if (qty <= 0 || caller === target) {
throw new ContractError('Invalid token transfer')
throw new ContractError('Invalid token transfer');
}
if (balances[caller] < qty) {
throw new ContractError(`Caller balance not high enough to send ${qty} token(s)!`)
throw new ContractError(`Caller balance not high enough to send ${qty} token(s)!`);
}
// Lower the token balance of the caller
balances[caller] -= qty
balances[caller] -= qty;
if (target in balances) {
// Wallet already exists in state, add new tokens
balances[target] += qty
balances[target] += qty;
} else {
// Wallet is new, set starting balance
balances[target] = qty
balances[target] = qty;
}
return { state }
return { state };
}
if (input.function === 'balance') {
const target = input.target
const ticker = state.ticker
const target = input.target;
const ticker = state.ticker;
if (typeof target !== 'string') {
throw new ContractError('Must specify target to get balance for')
throw new ContractError('Must specify target to get balance for');
}
if (typeof balances[target] !== 'number') {
throw new ContractError('Cannot get balance, target does not exist')
throw new ContractError('Cannot get balance, target does not exist');
}
return { result: { target, ticker, balance: balances[target] } }
return { result: { target, ticker, balance: balances[target] } };
}
if (input.function === 'evolve' && canEvolve) {
@@ -58,10 +57,10 @@ export function handle (state, action) {
throw new ContractError('Only the owner can evolve a contract.');
}
state.evolve = input.value
state.evolve = input.value;
return { state }
return { state };
}
throw new ContractError(`No function supplied or function not recognised: "${input.function}"`)
throw new ContractError(`No function supplied or function not recognised: "${input.function}"`);
}

View File

@@ -32,6 +32,8 @@ describe('Testing the SmartWeave client', () => {
let walletAddress: string;
beforeAll(async () => {
// note: each tests suit (i.e. file with tests that Jest is running concurrently
// with another files has to have ArLocal set to a different port!)
arlocal = new ArLocal(1985, false);
await arlocal.start();
@@ -58,7 +60,7 @@ describe('Testing the SmartWeave client', () => {
src: contractSrc
});
contract = smartweave.contract(contractTxId) as HandlerBasedContract<ExampleContractState>;
contract = smartweave.contract(contractTxId);
contract.connect(wallet);
await mine();

View File

@@ -20,12 +20,14 @@ describe('Testing the Profit Sharing Token', () => {
let initialState: PstState;
beforeAll(async () => {
arlocal = new ArLocal(1985, false);
// note: each tests suit (i.e. file with tests that Jest is running concurrently
// with another files has to have ArLocal set to a different port!)
arlocal = new ArLocal(1986, false);
await arlocal.start();
arweave = Arweave.init({
host: 'localhost',
port: 1985,
port: 1986,
protocol: 'http'
});
@@ -42,6 +44,7 @@ describe('Testing the Profit Sharing Token', () => {
initialState = {
...stateFromFile,
...{
owner: walletAddress,
balances: {
...stateFromFile.balances,
[walletAddress]: 555669
@@ -95,6 +98,21 @@ describe('Testing the Profit Sharing Token', () => {
expect(result.ticker).toEqual('EXAMPLE_PST_TOKEN');
expect(result.target).toEqual('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M');
});
it("should properly evolve contract's source code", async () => {
expect((await pst.currentState()).balances[walletAddress]).toEqual(555114);
const newSource = fs.readFileSync(path.join(__dirname, 'data/token-evolve.js'), 'utf8');
const newSrcTxId = await pst.saveNewSource(newSource);
await mine();
await pst.evolve(newSrcTxId);
await mine();
// note: the evolved balance always adds 555 to the result
expect((await pst.currentBalance(walletAddress)).balance).toEqual(555114 + 555);
});
});
async function mine() {

View File

@@ -61,6 +61,9 @@ export interface Contract<State = unknown> {
*
* note: calling "interactRead" from withing contract's source code was not previously possible -
* this is a new feature.
*
* TODO: this should not be exposed in a public API - as it is supposed
* to be used only by Handler code.
*/
viewStateForTx<Input = unknown, View = unknown>(
input: Input,

View File

@@ -30,13 +30,13 @@ import { NetworkInfoInterface } from 'arweave/node/network';
export class HandlerBasedContract<State> implements Contract<State> {
private readonly logger = LoggerFactory.INST.create('HandlerBasedContract');
private wallet?: ArWallet;
protected wallet?: ArWallet;
private evaluationOptions: EvaluationOptions = new DefaultEvaluationOptions();
public networkInfo?: NetworkInfoInterface = null;
constructor(
readonly contractTxId: string,
private readonly smartweave: SmartWeave,
protected readonly smartweave: SmartWeave,
// note: this will be probably used for creating contract's
// call hierarchy and generating some sort of "stack trace"
private readonly callingContract: Contract = null

View File

@@ -1,20 +1,49 @@
import { Contract, InteractionResult } from '@smartweave';
import { Contract } from '@smartweave';
/**
* The result from the "balance" view method on the PST Contract.
*/
export interface BalanceResult {
target: string;
ticker: string;
balance: number;
}
export interface PstState {
/**
* Interface for all contracts the implement the {@link Evolve} feature
*/
export interface EvolvingContract {
saveNewSource(newContractSource: string): Promise<string | null>;
evolve(newSrcTxId: string): Promise<string | null>;
}
/**
* Interface describing state for all Evolve-compatible contracts.
* Evolve is a feature that allows to change contract's source
* code, without deploying a new contract.
* See ({@link Evolve})
*/
export interface EvolveState {
settings: any[] | {} | null;
canEvolve: boolean; // whether contract is allowed to evolve. seems to default to true..
evolve: string; // the transaction id of the Arweave transaction with the updated source code. odd naming convention..
}
/**
* Interface describing state for all PST contracts.
*/
export interface PstState extends EvolveState {
ticker: string;
owner: string;
canEvolve: boolean;
balances: {
[key: string]: number;
};
}
/**
* Interface describing data required for making a transfer
*/
export interface TransferInput {
target: string;
qty: number;
@@ -24,7 +53,7 @@ export interface TransferInput {
* A type of {@link Contract} designed specifically for the interaction with
* Profit Sharing Tokens.
*/
export interface PstContract extends Contract {
export interface PstContract extends Contract, EvolvingContract {
currentBalance(target: string): Promise<BalanceResult>;
currentState(): Promise<PstState>;

View File

@@ -1,3 +1,4 @@
import { SmartWeaveTags } from '@smartweave';
import { BalanceResult, HandlerBasedContract, PstContract, PstState, TransferInput } from '@smartweave/contract';
interface BalanceInput {
@@ -7,7 +8,7 @@ interface BalanceInput {
export class PstContractImpl extends HandlerBasedContract<PstState> implements PstContract {
async currentBalance(target: string): Promise<BalanceResult> {
const interactionResult = await super.viewState<BalanceInput, BalanceResult>({ function: 'balance', target });
const interactionResult = await this.viewState<BalanceInput, BalanceResult>({ function: 'balance', target });
if (interactionResult.type !== 'ok') {
throw Error(interactionResult.errorMessage);
}
@@ -19,6 +20,27 @@ export class PstContractImpl extends HandlerBasedContract<PstState> implements P
}
async transfer(transfer: TransferInput): Promise<string | null> {
return await super.writeInteraction<any>({ function: 'transfer', ...transfer });
return await this.writeInteraction<any>({ function: 'transfer', ...transfer });
}
async evolve(newSrcTxId: string): Promise<string | null> {
return await this.writeInteraction<any>({ function: 'evolve', value: newSrcTxId });
}
async saveNewSource(newContractSource: string): Promise<string | null> {
if (!this.wallet) {
throw new Error("Wallet not connected. Use 'connect' method first.");
}
const { arweave } = this.smartweave;
const tx = await arweave.createTransaction({ data: newContractSource }, this.wallet);
tx.addTag(SmartWeaveTags.APP_NAME, 'SmartWeaveContractSource');
tx.addTag(SmartWeaveTags.APP_VERSION, '0.3.0');
tx.addTag('Content-Type', 'application/javascript');
await arweave.transactions.sign(tx, this.wallet);
await arweave.transactions.post(tx);
return tx.id;
}
}

View File

@@ -47,7 +47,7 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
executionContext.contractDefinition.txId,
requestedBlockHeight
)) as BlockHeightCacheResult<EvalStateResult<State>>;
logger.trace('Retrieving value from cache', benchmark.elapsed());
this.cLogger.trace('Retrieving value from cache', benchmark.elapsed());
if (cachedState != null) {
this.cLogger.debug(`Cached state for ${executionContext.contractDefinition.txId}`, {

View File

@@ -1,5 +1,6 @@
import {
DefinitionLoader,
EvolveState,
ExecutionContext,
ExecutionContextModifier,
ExecutorFactory,
@@ -9,12 +10,6 @@ import {
SmartWeaveErrorType
} from '@smartweave';
export interface EvolveCompatibleState {
settings: any[]; // some..erm..settings?
canEvolve: boolean; // whether contract is allowed to evolve. seems to default to true..
evolve: string; // the transaction id of the Arweave transaction with the updated source code. odd naming convention..
}
/*
...I'm still not fully convinced to the whole "evolve" idea.
@@ -33,7 +28,7 @@ without the need of hard-coding contract's txId in the client's source code.
This also makes it easier to audit given contract - as you keep all its versions in one place.
*/
function isEvolveCompatible(state: any): state is EvolveCompatibleState {
function isEvolveCompatible(state: any): state is EvolveState {
if (!state) {
return false;
}
@@ -90,17 +85,18 @@ export class Evolve implements ExecutionContextModifier {
const newContractDefinition = await this.definitionLoader.load<State>(contractTxId, evolve);
const newHandler = (await this.executorFactory.create<State>(newContractDefinition)) as HandlerApi<State>;
const modifiedContext = {
...executionContext,
contractDefinition: newContractDefinition,
handler: newHandler
};
//FIXME: side-effect...
executionContext.contractDefinition = newContractDefinition;
executionContext.handler = newHandler;
this.logger.debug('evolved to:', {
txId: modifiedContext.contractDefinition.txId,
srcTxId: modifiedContext.contractDefinition.srcTxId
evolve: evolve,
newSrcTxId: executionContext.contractDefinition.srcTxId,
current: currentSrcTxId,
txId: executionContext.contractDefinition.txId,
});
return modifiedContext;
return executionContext;
} catch (e) {
throw new SmartWeaveError(SmartWeaveErrorType.CONTRACT_NOT_FOUND, {
message: `Contract having txId: ${contractTxId} not found`,