Skip to content

Commit

Permalink
feat: add ethereum plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
denizenging committed Oct 8, 2023
1 parent 4fc49d1 commit 76cbb07
Show file tree
Hide file tree
Showing 11 changed files with 1,258 additions and 2 deletions.
1 change: 1 addition & 0 deletions pkg/ethereum/.npmignore
38 changes: 38 additions & 0 deletions pkg/ethereum/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@slangroom/ethereum",
"version": "1.0.0",
"dependencies": {
"@slangroom/core": "workspace:*",
"@slangroom/deps": "workspace:*",
"@slangroom/shared": "workspace:*",
"axios": "^1.5.1",
"web3": "^4.1.2"
},
"repository": "https://github.com/dyne/slangroom",
"license": "AGPL-3.0-only",
"type": "module",
"main": "./build/cjs/src/index.js",
"types": "./build/cjs/src/index.d.ts",
"exports": {
".": {
"import": {
"types": "./build/esm/src/index.d.ts",
"default": "./build/esm/src/index.js"
},
"require": {
"types": "./build/cjs/src/index.d.ts",
"default": "./build/cjs/src/index.js"
}
},
"./*": {
"import": {
"types": "./build/esm/src/*.d.ts",
"default": "./build/esm/src/*.js"
},
"require": {
"types": "./build/cjs/src/*.d.ts",
"default": "./build/cjs/src/*.js"
}
}
}
}
5 changes: 5 additions & 0 deletions pkg/ethereum/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from '@slangroom/ethereum/tokens';
export * from '@slangroom/ethereum/lexer';
export * from '@slangroom/ethereum/parser';
export * from '@slangroom/ethereum/visitor';
export * from '@slangroom/ethereum/plugins';
9 changes: 9 additions & 0 deletions pkg/ethereum/src/lexer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Lexer as L } from '@slangroom/deps/chevrotain';
import { allTokens } from '@slangroom/ethereum';

const Lexer = new L(allTokens);

/**
* Lexes the given line.
*/
export const lex = (line: string) => Lexer.tokenize(line);
158 changes: 158 additions & 0 deletions pkg/ethereum/src/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import {
CstParser,
type CstNode,
type IToken,
createSyntaxDiagramsCode,
} from '@slangroom/deps/chevrotain';
import {
allTokens,
Read,
The,
Ethereum,
Nonce,
Bytes,
Balance,
Transaction,
Id,
After,
Broadcast,
Suggested,
Gas,
Price,
Erc20,
Decimals,
Name,
Symbol_,
Total,
Supply,
Erc721,
In,
Owner,
Asset,
} from '@slangroom/ethereum';
import * as fs from 'node:fs';

export type PhraseCst = CstNode & {
children: {
ethereum: EthereumCst;
erc20: Erc20Cst;
erc721: Erc721Cst;
};
};

export type EthereumCst = CstNode & {
children:
| { Nonce: [IToken] }
| { Bytes: [IToken] }
| { Balance: [IToken] }
| { broadcast: CstNode }
| { gasPrice: CstNode };
};

export type Erc20Cst = CstNode & {
children:
| { Decimals: [IToken] }
| { Name: [IToken] }
| { Symbol: [IToken] }
| { Balance: [IToken] }
| { totalSupply: CstNode };
};

export type Erc721Cst = CstNode & {
children: { id: CstNode } | { Asset: [IToken] } | { Owner: [IToken] };
};

export type KindCst = CstNode & {
children: { Sequential: [IToken] } | { Parallel: [IToken] } | { Same: [IToken] };
};

export type MethodCst = CstNode & {
children: { Get: [IToken] } | { Post: [IToken] };
};

const Parser = new (class extends CstParser {
constructor() {
super(allTokens);
this.performSelfAnalysis();
}

phrase = this.RULE('phrase', () => {
this.OR([
{ ALT: () => this.SUBRULE(this.#ethereum) },
{ ALT: () => this.SUBRULE(this.#erc20) },
{ ALT: () => this.SUBRULE(this.#erc721) },
]);
});

#ethereum = this.RULE('ethereum', () => {
this.CONSUME(Read);
this.CONSUME(The);
this.CONSUME(Ethereum);
this.OR([
{ ALT: () => this.CONSUME(Nonce) },
{ ALT: () => this.CONSUME(Bytes) },
{ ALT: () => this.CONSUME(Balance) },
{ ALT: () => this.SUBRULE(this.#broadcast) },
{ ALT: () => this.SUBRULE(this.#gasPrice) },
]);
});

#broadcast = this.RULE('broadcast', () => {
this.CONSUME(Transaction);
this.CONSUME(Id);
this.CONSUME(After);
this.CONSUME(Broadcast);
});

#gasPrice = this.RULE('gasPrice', () => {
this.CONSUME(Suggested);
this.CONSUME(Gas);
this.CONSUME(Price);
});

#erc20 = this.RULE('erc20', () => {
this.CONSUME(Erc20);
this.OR([
{ ALT: () => this.CONSUME(Decimals) },
{ ALT: () => this.CONSUME(Name) },
{ ALT: () => this.CONSUME(Symbol_) },
{ ALT: () => this.CONSUME(Balance) },
{ ALT: () => this.SUBRULE(this.#totalSupply) },
]);
});

#totalSupply = this.RULE('totalSupply', () => {
this.CONSUME(Total);
this.CONSUME(Supply);
});

#erc721 = this.RULE('erc721', () => {
this.CONSUME(Erc721);
this.OR([
{ ALT: () => this.SUBRULE(this.#id) },
{ ALT: () => this.CONSUME(Owner) },
{ ALT: () => this.CONSUME(Asset) },
]);
});

#id = this.RULE('id', () => {
this.CONSUME(Id);
this.CONSUME(In);
this.CONSUME(Transaction);
});
})();

export const CstVisitor = Parser.getBaseCstVisitorConstructor();

export const parse = (tokens: IToken[]) => {
Parser.input = tokens;
return {
cst: Parser.phrase(),
errors: Parser.errors,
};
};

// TODO: remove
const serializedGrammar = Parser.getSerializedGastProductions();
const htmlText = createSyntaxDiagramsCode(serializedGrammar);
fs.writeFileSync('./generated_diagrams_eth.html', htmlText);
73 changes: 73 additions & 0 deletions pkg/ethereum/src/plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { PluginContext, PluginResult } from '@slangroom/core/plugin';
import { lex, parse, visit, EthereumRequestKind, type PhraseCst } from '@slangroom/ethereum';
import { Web3 } from 'web3';

/**
* @internal
*/
export const astify = (text: string) => {
const lexed = lex(text);
if (lexed.errors.length) return { errors: lexed.errors };

const parsed = parse(lexed.tokens);
if (parsed.errors.length) return { errors: parsed.errors };

return { ast: visit(parsed.cst as PhraseCst) };
};

/**
* @internal
*/
export const execute = async (
ctx: PluginContext,
kind: EthereumRequestKind
): Promise<PluginResult> => {
const web3 = new Web3(ctx.fetchConnect()[0]);

if (kind == EthereumRequestKind.EthereumNonce) {
const address = ctx.fetch('address') as string;
const nonce = await web3.eth.getTransactionCount(address);
return ctx.pass(nonce.toString());
}

if (kind == EthereumRequestKind.EthereumGasPrice) {
const gasPrice = await web3.eth.getGasPrice();
return ctx.pass(gasPrice.toString());
}

if (kind == EthereumRequestKind.EthereumBalance) {
// TODO: different statement for string and array
const address = ctx.fetch('address');
if (Array.isArray(address)) {
const balances = await Promise.all(
address.map((addr) => web3.eth.getBalance(addr as string))
);
return ctx.pass(balances.map((b) => b.toString()));
} else {
return ctx.pass((await web3.eth.getBalance(address as string)).toString());
}
}
// if (kind == EthereumRequestKind.EthereumBytes) {
// const tag = ctx.fetch('transaction_id') as string;
// const receipt = await web3.eth.getTransactionReceipt(
// tag.startsWith('0x') ? tag : '0x' + tag
// );
// if (!receipt) return ctx.fail("Transaction id doesn't exist");
// if (!receipt.status) return ctx.fail('Failed transaction');
// try {
// const dataRead = receipt.logs[0]?.data?.slice(2);
// return ctx.pass(dataRead);
// } catch (e) {
// return ctx.fail('Empty transaction');
// }
// }
return ctx.fail('Should not be here');
};

const EthereumPlugin = async (ctx: PluginContext): Promise<PluginResult> => {
const { ast, errors } = astify(ctx.phrase);
if (!ast) return ctx.fail(errors);
return execute(ctx, ast);
};

export const ethereumPlugins = new Set([EthereumPlugin]);
Loading

0 comments on commit 76cbb07

Please sign in to comment.