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: [ testPathIgnorePatterns: [
"/.yalc/", "/.yalc/",
"/data/"
], ],
testEnvironment: 'node', testEnvironment: 'node',

View File

@@ -24,7 +24,10 @@
"version": "yarn format && git add -A src", "version": "yarn format && git add -A src",
"postversion": "git push && git push --tags", "postversion": "git push && git push --tags",
"yalc:publish": "yarn build && yalc publish --push", "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", "license": "MIT",
"author": "Redstone Team <dev@redstone.finance>", "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,67 +1,66 @@
export function handle(state, action) {
export function handle (state, action) { const balances = state.balances;
const balances = state.balances const canEvolve = state.canEvolve;
const canEvolve = state.canEvolve const input = action.input;
const input = action.input const caller = action.caller;
const caller = action.caller
if (input.function === 'transfer') { if (input.function === 'transfer') {
const target = input.target const target = input.target;
const qty = input.qty const qty = input.qty;
if (!Number.isInteger(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) { if (!target) {
throw new ContractError('No target specified') throw new ContractError('No target specified');
} }
if (qty <= 0 || caller === target) { if (qty <= 0 || caller === target) {
throw new ContractError('Invalid token transfer') throw new ContractError('Invalid token transfer');
} }
if (balances[caller] < qty) { 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 // Lower the token balance of the caller
balances[caller] -= qty balances[caller] -= qty;
if (target in balances) { if (target in balances) {
// Wallet already exists in state, add new tokens // Wallet already exists in state, add new tokens
balances[target] += qty balances[target] += qty;
} else { } else {
// Wallet is new, set starting balance // Wallet is new, set starting balance
balances[target] = qty balances[target] = qty;
} }
return { state } return { state };
} }
if (input.function === 'balance') { if (input.function === 'balance') {
const target = input.target const target = input.target;
const ticker = state.ticker const ticker = state.ticker;
if (typeof target !== 'string') { 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') { 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 (input.function === 'evolve' && canEvolve) {
if(state.owner !== caller) { if (state.owner !== caller) {
throw new ContractError('Only the owner can evolve a contract.'); 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; let walletAddress: string;
beforeAll(async () => { 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); arlocal = new ArLocal(1985, false);
await arlocal.start(); await arlocal.start();
@@ -58,7 +60,7 @@ describe('Testing the SmartWeave client', () => {
src: contractSrc src: contractSrc
}); });
contract = smartweave.contract(contractTxId) as HandlerBasedContract<ExampleContractState>; contract = smartweave.contract(contractTxId);
contract.connect(wallet); contract.connect(wallet);
await mine(); await mine();

View File

@@ -20,12 +20,14 @@ describe('Testing the Profit Sharing Token', () => {
let initialState: PstState; let initialState: PstState;
beforeAll(async () => { 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(); await arlocal.start();
arweave = Arweave.init({ arweave = Arweave.init({
host: 'localhost', host: 'localhost',
port: 1985, port: 1986,
protocol: 'http' protocol: 'http'
}); });
@@ -42,6 +44,7 @@ describe('Testing the Profit Sharing Token', () => {
initialState = { initialState = {
...stateFromFile, ...stateFromFile,
...{ ...{
owner: walletAddress,
balances: { balances: {
...stateFromFile.balances, ...stateFromFile.balances,
[walletAddress]: 555669 [walletAddress]: 555669
@@ -95,6 +98,21 @@ describe('Testing the Profit Sharing Token', () => {
expect(result.ticker).toEqual('EXAMPLE_PST_TOKEN'); expect(result.ticker).toEqual('EXAMPLE_PST_TOKEN');
expect(result.target).toEqual('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M'); 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() { 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 - * note: calling "interactRead" from withing contract's source code was not previously possible -
* this is a new feature. * 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>( viewStateForTx<Input = unknown, View = unknown>(
input: Input, input: Input,

View File

@@ -30,13 +30,13 @@ import { NetworkInfoInterface } from 'arweave/node/network';
export class HandlerBasedContract<State> implements Contract<State> { export class HandlerBasedContract<State> implements Contract<State> {
private readonly logger = LoggerFactory.INST.create('HandlerBasedContract'); private readonly logger = LoggerFactory.INST.create('HandlerBasedContract');
private wallet?: ArWallet; protected wallet?: ArWallet;
private evaluationOptions: EvaluationOptions = new DefaultEvaluationOptions(); private evaluationOptions: EvaluationOptions = new DefaultEvaluationOptions();
public networkInfo?: NetworkInfoInterface = null; public networkInfo?: NetworkInfoInterface = null;
constructor( constructor(
readonly contractTxId: string, readonly contractTxId: string,
private readonly smartweave: SmartWeave, protected readonly smartweave: SmartWeave,
// note: this will be probably used for creating contract's // note: this will be probably used for creating contract's
// call hierarchy and generating some sort of "stack trace" // call hierarchy and generating some sort of "stack trace"
private readonly callingContract: Contract = null 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 { export interface BalanceResult {
target: string; target: string;
ticker: string; ticker: string;
balance: number; 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; ticker: string;
owner: string; owner: string;
canEvolve: boolean;
balances: { balances: {
[key: string]: number; [key: string]: number;
}; };
} }
/**
* Interface describing data required for making a transfer
*/
export interface TransferInput { export interface TransferInput {
target: string; target: string;
qty: number; qty: number;
@@ -24,7 +53,7 @@ export interface TransferInput {
* A type of {@link Contract} designed specifically for the interaction with * A type of {@link Contract} designed specifically for the interaction with
* Profit Sharing Tokens. * Profit Sharing Tokens.
*/ */
export interface PstContract extends Contract { export interface PstContract extends Contract, EvolvingContract {
currentBalance(target: string): Promise<BalanceResult>; currentBalance(target: string): Promise<BalanceResult>;
currentState(): Promise<PstState>; currentState(): Promise<PstState>;

View File

@@ -1,3 +1,4 @@
import { SmartWeaveTags } from '@smartweave';
import { BalanceResult, HandlerBasedContract, PstContract, PstState, TransferInput } from '@smartweave/contract'; import { BalanceResult, HandlerBasedContract, PstContract, PstState, TransferInput } from '@smartweave/contract';
interface BalanceInput { interface BalanceInput {
@@ -7,7 +8,7 @@ interface BalanceInput {
export class PstContractImpl extends HandlerBasedContract<PstState> implements PstContract { export class PstContractImpl extends HandlerBasedContract<PstState> implements PstContract {
async currentBalance(target: string): Promise<BalanceResult> { 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') { if (interactionResult.type !== 'ok') {
throw Error(interactionResult.errorMessage); throw Error(interactionResult.errorMessage);
} }
@@ -19,6 +20,27 @@ export class PstContractImpl extends HandlerBasedContract<PstState> implements P
} }
async transfer(transfer: TransferInput): Promise<string | null> { 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, executionContext.contractDefinition.txId,
requestedBlockHeight requestedBlockHeight
)) as BlockHeightCacheResult<EvalStateResult<State>>; )) as BlockHeightCacheResult<EvalStateResult<State>>;
logger.trace('Retrieving value from cache', benchmark.elapsed()); this.cLogger.trace('Retrieving value from cache', benchmark.elapsed());
if (cachedState != null) { if (cachedState != null) {
this.cLogger.debug(`Cached state for ${executionContext.contractDefinition.txId}`, { this.cLogger.debug(`Cached state for ${executionContext.contractDefinition.txId}`, {

View File

@@ -1,5 +1,6 @@
import { import {
DefinitionLoader, DefinitionLoader,
EvolveState,
ExecutionContext, ExecutionContext,
ExecutionContextModifier, ExecutionContextModifier,
ExecutorFactory, ExecutorFactory,
@@ -9,12 +10,6 @@ import {
SmartWeaveErrorType SmartWeaveErrorType
} from '@smartweave'; } 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. ...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. 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) { if (!state) {
return false; return false;
} }
@@ -90,17 +85,18 @@ export class Evolve implements ExecutionContextModifier {
const newContractDefinition = await this.definitionLoader.load<State>(contractTxId, evolve); const newContractDefinition = await this.definitionLoader.load<State>(contractTxId, evolve);
const newHandler = (await this.executorFactory.create<State>(newContractDefinition)) as HandlerApi<State>; const newHandler = (await this.executorFactory.create<State>(newContractDefinition)) as HandlerApi<State>;
const modifiedContext = { //FIXME: side-effect...
...executionContext, executionContext.contractDefinition = newContractDefinition;
contractDefinition: newContractDefinition, executionContext.handler = newHandler;
handler: newHandler
};
this.logger.debug('evolved to:', { this.logger.debug('evolved to:', {
txId: modifiedContext.contractDefinition.txId, evolve: evolve,
srcTxId: modifiedContext.contractDefinition.srcTxId newSrcTxId: executionContext.contractDefinition.srcTxId,
current: currentSrcTxId,
txId: executionContext.contractDefinition.txId,
}); });
return modifiedContext; return executionContext;
} catch (e) { } catch (e) {
throw new SmartWeaveError(SmartWeaveErrorType.CONTRACT_NOT_FOUND, { throw new SmartWeaveError(SmartWeaveErrorType.CONTRACT_NOT_FOUND, {
message: `Contract having txId: ${contractTxId} not found`, message: `Contract having txId: ${contractTxId} not found`,