fix: viewState interaction transaction does not take 'evolve' into account #14
This commit is contained in:
committed by
Piotr Pędziwiatr
parent
717c325a36
commit
ce5a589b10
13
.github/workflows/tests.yml
vendored
Normal file
13
.github/workflows/tests.yml
vendored
Normal 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
|
||||
@@ -18,6 +18,7 @@ module.exports = {
|
||||
|
||||
testPathIgnorePatterns: [
|
||||
"/.yalc/",
|
||||
"/data/"
|
||||
],
|
||||
|
||||
testEnvironment: 'node',
|
||||
|
||||
@@ -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>",
|
||||
|
||||
66
src/__tests__/integration/data/token-evolve.js
Normal file
66
src/__tests__/integration/data/token-evolve.js
Normal 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}"`);
|
||||
}
|
||||
@@ -1,67 +1,66 @@
|
||||
|
||||
export function handle (state, action) {
|
||||
const balances = state.balances
|
||||
const canEvolve = state.canEvolve
|
||||
const input = action.input
|
||||
const caller = action.caller
|
||||
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
|
||||
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) {
|
||||
if(state.owner !== caller) {
|
||||
if (input.function === 'evolve' && canEvolve) {
|
||||
if (state.owner !== caller) {
|
||||
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}"`);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`, {
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user