feat: enable evolve for all contracts

This commit is contained in:
asiaziola
2022-06-13 09:18:11 +02:00
parent 6847f96064
commit 621d86d7db
10 changed files with 272 additions and 235 deletions

View File

@@ -112,7 +112,7 @@ describe('Testing the Profit Sharing Token', () => {
const newSource = fs.readFileSync(path.join(__dirname, '../data/token-evolve.js'), 'utf8');
const newSrcTxId = await pst.saveNewSource(newSource);
const newSrcTxId = await pst.saveSource({ src: newSource });
await mineBlock(arweave);
await pst.evolve(newSrcTxId);

View File

@@ -131,7 +131,7 @@ describe('Testing the Profit Sharing Token', () => {
const newSource = fs.readFileSync(path.join(__dirname, '../data/token-evolve.js'), 'utf8');
const newSrcTxId = await pst.saveNewSource(newSource);
const newSrcTxId = await pst.saveSource({ src: newSource });
await mineBlock(arweave);
await pst.evolve(newSrcTxId);

View File

@@ -6,7 +6,8 @@ import {
EvaluationOptions,
GQLNodeInterface,
InteractionResult,
Tags
Tags,
SaveSource
} from '@smartweave';
import { NetworkInfoInterface } from 'arweave/node/network';
import Transaction from 'arweave/node/lib/transaction';
@@ -16,11 +17,27 @@ export type BenchmarkStats = { gatewayCommunication: number; stateEvaluation: nu
export type SigningFunction = (tx: Transaction) => Promise<void>;
/**
* Interface describing state for all Evolve-compatible contracts.
*/
export interface EvolveState {
settings: any[] | unknown | null;
/**
* whether contract is allowed to evolve. seems to default to true..
*/
canEvolve: boolean;
/**
* the transaction id of the Arweave transaction with the updated source code.
*/
evolve: string;
}
/**
* A base interface to be implemented by SmartWeave Contracts clients
* - contains "low-level" methods that allow to interact with any contract
*/
export interface Contract<State = unknown> {
export interface Contract<State = unknown> extends SaveSource {
/**
* Returns the Arweave transaction id of this contract.
*/
@@ -217,4 +234,14 @@ export interface Contract<State = unknown> {
* @param nodeAddress - distributed execution network node address
*/
syncState(nodeAddress: string): Promise<Contract>;
/**
* Evolve is a feature that allows to change contract's source
* code, without having to deploy a new contract.
* This method effectively evolves the contract to the source.
* This requires the {@link saveSource} to be called first
* and its transaction to be confirmed by the network.
* @param newSrcTxId - result of the {@link saveSource} method call.
*/
evolve(newSrcTxId: string): Promise<string | null>;
}

View File

@@ -29,14 +29,15 @@ import {
SmartWeave,
SmartWeaveTags,
SourceType,
Tags
Tags,
SaveSourceImpl,
SaveSourceData
} from '@smartweave';
import { TransactionStatusResponse } from 'arweave/node/transactions';
import { NetworkInfoInterface } from 'arweave/node/network';
import stringify from 'safe-stable-stringify';
import * as crypto from 'crypto';
import Transaction from 'arweave/node/lib/transaction';
import { options } from 'tsconfig-paths/lib/options';
/**
* An implementation of {@link Contract} that is backwards compatible with current style
@@ -765,4 +766,20 @@ export class HandlerBasedContract<State> implements Contract<State> {
return this;
}
async evolve(newSrcTxId: string): Promise<string | null> {
return await this.writeInteraction<any>({ function: 'evolve', value: newSrcTxId });
}
async saveSource(saveSourceData: SaveSourceData): Promise<any> {
if (!this.signer) {
throw new Error("Wallet not connected. Use 'connect' method first.");
}
const { arweave } = this.smartweave;
const source = new SaveSourceImpl(arweave);
const srcTx = await source.saveSource(saveSourceData, this.signer);
return srcTx.id;
}
}

View File

@@ -1,4 +1,5 @@
import { Contract } from '@smartweave';
import { EvolveState } from './Contract';
/**
* The result from the "balance" view method on the PST Contract.
@@ -9,44 +10,6 @@ export interface BalanceResult {
balance: number;
}
/**
* Interface for all contracts the implement the {@link Evolve} feature.
* Evolve is a feature that allows to change contract's source
* code, without having to deploy a new contract.
* See ({@link Evolve})
*/
export interface EvolvingContract {
/**
* allows to post new contract source on Arweave
* @param newContractSource - new contract source...
*/
saveNewSource(newContractSource: string): Promise<string | null>;
/**
* effectively evolves the contract to the source.
* This requires the {@link saveNewSource} to be called first
* and its transaction to be confirmed by the network.
* @param newSrcTxId - result of the {@link saveNewSource} method call.
*/
evolve(newSrcTxId: string): Promise<string | null>;
}
/**
* Interface describing state for all Evolve-compatible contracts.
*/
export interface EvolveState {
settings: any[] | unknown | null;
/**
* whether contract is allowed to evolve. seems to default to true..
*/
canEvolve: boolean;
/**
* the transaction id of the Arweave transaction with the updated source code.
*/
evolve: string;
}
/**
* Interface describing base state for all PST contracts.
*/
@@ -70,7 +33,7 @@ export interface TransferInput {
* A type of {@link Contract} designed specifically for the interaction with
* Profit Sharing Token contract.
*/
export interface PstContract extends Contract<PstState>, EvolvingContract {
export interface PstContract extends Contract<PstState> {
/**
* return the current balance for the given wallet
* @param target - wallet address

View File

@@ -1,4 +1,3 @@
import { SmartWeaveTags } from '@smartweave';
import { BalanceResult, HandlerBasedContract, PstContract, PstState, TransferInput } from '@smartweave/contract';
interface BalanceInput {
@@ -22,25 +21,4 @@ export class PstContractImpl extends HandlerBasedContract<PstState> implements P
async transfer(transfer: TransferInput): Promise<string | null> {
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.signer) {
throw new Error("Wallet not connected. Use 'connect' method first.");
}
const { arweave } = this.smartweave;
const tx = await arweave.createTransaction({ data: newContractSource });
tx.addTag(SmartWeaveTags.APP_NAME, 'SmartWeaveContractSource');
tx.addTag(SmartWeaveTags.APP_VERSION, '0.3.0');
tx.addTag('Content-Type', 'application/javascript');
await this.signer(tx);
await arweave.transactions.post(tx);
return tx.id;
}
}

View File

@@ -0,0 +1,15 @@
import { ArWallet } from '@smartweave/core';
import { SigningFunction } from './Contract';
import { SaveSourceData } from './SaveSourceImpl';
export interface SaveSource {
/**
* allows to post contract source on Arweave
* @param newContractSource - new contract source...
*/
saveSource(
contractSource: SaveSourceData,
signer: ArWallet | SigningFunction,
useBundler?: boolean
): Promise<string | null>;
}

View File

@@ -0,0 +1,186 @@
import { ArWallet, ContractData, ContractType, SmartWeaveTags } from '@smartweave/core';
import { LoggerFactory } from '@smartweave/logging';
import { SaveSource, SigningFunction } from '@smartweave';
import metering from 'redstone-wasm-metering';
import Arweave from 'arweave';
import { Go } from '../core/modules/impl/wasm/go-wasm-imports';
import fs, { PathOrFileDescriptor } from 'fs';
import { matchMutClosureDtor } from '../core/modules/impl/wasm/wasm-bindgen-tools';
const wasmTypeMapping: Map<number, string> = new Map([
[1, 'assemblyscript'],
[2, 'rust'],
[3, 'go']
/*[4, 'swift'],
[5, 'c']*/
]);
export interface SaveSourceData {
src: string | Buffer;
wasmSrcCodeDir?: string;
wasmGlueCode?: string;
}
export class SaveSourceImpl implements SaveSource {
private readonly logger = LoggerFactory.INST.create('SaveSource');
constructor(private readonly arweave: Arweave) {}
async saveSource(contractData: SaveSourceData, signer: ArWallet | SigningFunction, useBundler = false): Promise<any> {
this.logger.debug('Creating new contract');
const { src, wasmSrcCodeDir, wasmGlueCode } = contractData;
const contractType: ContractType = src instanceof Buffer ? 'wasm' : 'js';
let srcTx;
let wasmLang = null;
let wasmVersion = null;
const metadata = {};
const data: Buffer[] = [];
if (contractType == 'wasm') {
const meteredWasmBinary = metering.meterWASM(src, {
meterType: 'i32'
});
data.push(meteredWasmBinary);
const wasmModule = await WebAssembly.compile(src as Buffer);
const moduleImports = WebAssembly.Module.imports(wasmModule);
let lang: number;
if (this.isGoModule(moduleImports)) {
const go = new Go(null);
const module = new WebAssembly.Instance(wasmModule, go.importObject);
// DO NOT await here!
go.run(module);
lang = go.exports.lang();
wasmVersion = go.exports.version();
} else {
const module: WebAssembly.Instance = await WebAssembly.instantiate(src, dummyImports(moduleImports));
// @ts-ignore
if (!module.instance.exports.lang) {
throw new Error(`No info about source type in wasm binary. Did you forget to export "lang" function?`);
}
// @ts-ignore
lang = module.instance.exports.lang();
// @ts-ignore
wasmVersion = module.instance.exports.version();
if (!wasmTypeMapping.has(lang)) {
throw new Error(`Unknown wasm source type ${lang}`);
}
}
wasmLang = wasmTypeMapping.get(lang);
if (wasmSrcCodeDir == null) {
throw new Error('No path to original wasm contract source code');
}
const zippedSourceCode = await this.zipContents(wasmSrcCodeDir);
data.push(zippedSourceCode);
if (wasmLang == 'rust') {
if (!wasmGlueCode) {
throw new Error('No path to generated wasm-bindgen js code');
}
const wasmBindgenSrc = fs.readFileSync(wasmGlueCode, 'utf-8');
const dtor = matchMutClosureDtor(wasmBindgenSrc);
metadata['dtor'] = parseInt(dtor);
data.push(Buffer.from(wasmBindgenSrc));
}
}
const allData = contractType == 'wasm' ? this.joinBuffers(data) : src;
if (typeof signer == 'function') {
srcTx = await this.arweave.createTransaction({ data: allData });
} else {
srcTx = await this.arweave.createTransaction({ data: allData }, signer);
}
srcTx.addTag(SmartWeaveTags.APP_NAME, 'SmartWeaveContractSource');
// TODO: version should be taken from the current package.json version.
srcTx.addTag(SmartWeaveTags.APP_VERSION, '0.3.0');
srcTx.addTag(SmartWeaveTags.SDK, 'RedStone');
srcTx.addTag(SmartWeaveTags.CONTENT_TYPE, contractType == 'js' ? 'application/javascript' : 'application/wasm');
if (contractType == 'wasm') {
srcTx.addTag(SmartWeaveTags.WASM_LANG, wasmLang);
srcTx.addTag(SmartWeaveTags.WASM_LANG_VERSION, wasmVersion);
srcTx.addTag(SmartWeaveTags.WASM_META, JSON.stringify(metadata));
}
if (typeof signer == 'function') {
await signer(srcTx);
} else {
await this.arweave.transactions.sign(srcTx, signer);
}
this.logger.debug('Posting transaction with source');
// note: in case of useBundler = true, we're posting both
// src tx and contract tx in one request.
let responseOk = true;
if (!useBundler) {
const response = await this.arweave.transactions.post(srcTx);
responseOk = response.status === 200 || response.status === 208;
}
if (responseOk) {
return srcTx;
} else {
throw new Error(`Unable to write Contract Source`);
}
}
private isGoModule(moduleImports: WebAssembly.ModuleImportDescriptor[]) {
return moduleImports.some((moduleImport) => {
return moduleImport.module == 'env' && moduleImport.name.startsWith('syscall/js');
});
}
private joinBuffers(buffers: Buffer[]): Buffer {
const length = buffers.length;
const result = [];
result.push(Buffer.from(length.toString()));
result.push(Buffer.from('|'));
buffers.forEach((b) => {
result.push(Buffer.from(b.length.toString()));
result.push(Buffer.from('|'));
});
result.push(...buffers);
return result.reduce((prev, b) => Buffer.concat([prev, b]));
}
private async zipContents(source: PathOrFileDescriptor): Promise<Buffer> {
const archiver = require('archiver'),
streamBuffers = require('stream-buffers');
const outputStreamBuffer = new streamBuffers.WritableStreamBuffer({
initialSize: 1000 * 1024, // start at 1000 kilobytes.
incrementAmount: 1000 * 1024 // grow by 1000 kilobytes each time buffer overflows.
});
const archive = archiver('zip', {
zlib: { level: 9 } // Sets the compression level.
});
archive.on('error', function (err: any) {
throw err;
});
archive.pipe(outputStreamBuffer);
archive.directory(source.toString(), source.toString());
await archive.finalize();
outputStreamBuffer.end();
return outputStreamBuffer.getContents();
}
}
function dummyImports(moduleImports: WebAssembly.ModuleImportDescriptor[]) {
const imports = {};
moduleImports.forEach((moduleImport) => {
if (!Object.prototype.hasOwnProperty.call(imports, moduleImport.module)) {
imports[moduleImport.module] = {};
}
imports[moduleImport.module][moduleImport.name] = function () {};
});
return imports;
}

View File

@@ -3,3 +3,5 @@ export * from './HandlerBasedContract';
export * from './PstContract';
export * from './PstContractImpl';
export * from './InnerWritesEvaluator';
export * from './SaveSource';
export * from './SaveSourceImpl';

View File

@@ -2,21 +2,8 @@
import { ContractData, ContractType, CreateContract, FromSrcTxContractData, SmartWeaveTags } from '@smartweave/core';
import Arweave from 'arweave';
import { LoggerFactory } from '@smartweave/logging';
import { Go } from './wasm/go-wasm-imports';
import metering from 'redstone-wasm-metering';
import fs, { PathOrFileDescriptor } from 'fs';
import { matchMutClosureDtor } from './wasm/wasm-bindgen-tools';
import { parseInt } from 'lodash';
import Transaction from 'arweave/node/lib/transaction';
import stringify from 'safe-stable-stringify';
const wasmTypeMapping: Map<number, string> = new Map([
[1, 'assemblyscript'],
[2, 'rust'],
[3, 'go']
/*[4, 'swift'],
[5, 'c']*/
]);
import { SaveSourceImpl } from '@smartweave/contract';
export class DefaultCreateContract implements CreateContract {
private readonly logger = LoggerFactory.INST.create('DefaultCreateContract');
@@ -26,95 +13,13 @@ export class DefaultCreateContract implements CreateContract {
}
async deploy(contractData: ContractData, useBundler = false): Promise<string> {
const { wallet, initState, tags, transfer } = contractData;
const source = new SaveSourceImpl(this.arweave);
const srcTx = await source.saveSource(contractData, wallet, useBundler);
this.logger.debug('Creating new contract');
const { wallet, src, initState, tags, transfer, wasmSrcCodeDir, wasmGlueCode } = contractData;
const contractType: ContractType = src instanceof Buffer ? 'wasm' : 'js';
let srcTx;
let wasmLang = null;
let wasmVersion = null;
const metadata = {};
const data: Buffer[] = [];
if (contractType == 'wasm') {
const meteredWasmBinary = metering.meterWASM(src, {
meterType: 'i32'
});
data.push(meteredWasmBinary);
const wasmModule = await WebAssembly.compile(src as Buffer);
const moduleImports = WebAssembly.Module.imports(wasmModule);
let lang;
if (this.isGoModule(moduleImports)) {
const go = new Go(null);
const module = new WebAssembly.Instance(wasmModule, go.importObject);
// DO NOT await here!
go.run(module);
lang = go.exports.lang();
wasmVersion = go.exports.version();
} else {
const module: WebAssembly.Instance = await WebAssembly.instantiate(src, dummyImports(moduleImports));
// @ts-ignore
if (!module.instance.exports.lang) {
throw new Error(`No info about source type in wasm binary. Did you forget to export "lang" function?`);
}
// @ts-ignore
lang = module.instance.exports.lang();
// @ts-ignore
wasmVersion = module.instance.exports.version();
if (!wasmTypeMapping.has(lang)) {
throw new Error(`Unknown wasm source type ${lang}`);
}
}
wasmLang = wasmTypeMapping.get(lang);
if (wasmSrcCodeDir == null) {
throw new Error('No path to original wasm contract source code');
}
const zippedSourceCode = await this.zipContents(wasmSrcCodeDir);
data.push(zippedSourceCode);
if (wasmLang == 'rust') {
if (!wasmGlueCode) {
throw new Error('No path to generated wasm-bindgen js code');
}
const wasmBindgenSrc = fs.readFileSync(wasmGlueCode, 'utf-8');
const dtor = matchMutClosureDtor(wasmBindgenSrc);
metadata['dtor'] = parseInt(dtor);
data.push(Buffer.from(wasmBindgenSrc));
}
}
const allData = contractType == 'wasm' ? this.joinBuffers(data) : src;
srcTx = await this.arweave.createTransaction({ data: allData }, wallet);
srcTx.addTag(SmartWeaveTags.APP_NAME, 'SmartWeaveContractSource');
// TODO: version should be taken from the current package.json version.
srcTx.addTag(SmartWeaveTags.APP_VERSION, '0.3.0');
srcTx.addTag(SmartWeaveTags.SDK, 'RedStone');
srcTx.addTag(SmartWeaveTags.CONTENT_TYPE, contractType == 'js' ? 'application/javascript' : 'application/wasm');
if (contractType == 'wasm') {
srcTx.addTag(SmartWeaveTags.WASM_LANG, wasmLang);
srcTx.addTag(SmartWeaveTags.WASM_LANG_VERSION, wasmVersion);
srcTx.addTag(SmartWeaveTags.WASM_META, JSON.stringify(metadata));
}
await this.arweave.transactions.sign(srcTx, wallet);
this.logger.debug('Posting transaction with source');
// note: in case of useBundler = true, we're posting both
// src tx and contract tx in one request.
let responseOk = true;
if (!useBundler) {
const response = await this.arweave.transactions.post(srcTx);
responseOk = response.status === 200 || response.status === 208;
}
if (responseOk) {
return await this.deployFromSourceTx(
{
srcTxId: srcTx.id,
@@ -126,9 +31,6 @@ export class DefaultCreateContract implements CreateContract {
useBundler,
srcTx
);
} else {
throw new Error(`Unable to write Contract Source`);
}
}
async deployFromSourceTx(
@@ -210,57 +112,4 @@ export class DefaultCreateContract implements CreateContract {
throw new Error(`Error while posting contract ${response.statusText}`);
}
}
private isGoModule(moduleImports: WebAssembly.ModuleImportDescriptor[]) {
return moduleImports.some((moduleImport) => {
return moduleImport.module == 'env' && moduleImport.name.startsWith('syscall/js');
});
}
private joinBuffers(buffers: Buffer[]): Buffer {
const length = buffers.length;
const result = [];
result.push(Buffer.from(length.toString()));
result.push(Buffer.from('|'));
buffers.forEach((b) => {
result.push(Buffer.from(b.length.toString()));
result.push(Buffer.from('|'));
});
result.push(...buffers);
return result.reduce((prev, b) => Buffer.concat([prev, b]));
}
private async zipContents(source: PathOrFileDescriptor): Promise<Buffer> {
const archiver = require('archiver'),
streamBuffers = require('stream-buffers');
const outputStreamBuffer = new streamBuffers.WritableStreamBuffer({
initialSize: 1000 * 1024, // start at 1000 kilobytes.
incrementAmount: 1000 * 1024 // grow by 1000 kilobytes each time buffer overflows.
});
const archive = archiver('zip', {
zlib: { level: 9 } // Sets the compression level.
});
archive.on('error', function (err) {
throw err;
});
archive.pipe(outputStreamBuffer);
archive.directory(source.toString(), source.toString());
await archive.finalize();
outputStreamBuffer.end();
return outputStreamBuffer.getContents();
}
}
function dummyImports(moduleImports: WebAssembly.ModuleImportDescriptor[]) {
const imports = {};
moduleImports.forEach((moduleImport) => {
if (!Object.prototype.hasOwnProperty.call(imports, moduleImport.module)) {
imports[moduleImport.module] = {};
}
imports[moduleImport.module][moduleImport.name] = function () {};
});
return imports;
}