Skip to content

Commit abbd66e

Browse files
committed
Add example on how to run a Solidity contract
1 parent df5a1b5 commit abbd66e

File tree

4 files changed

+274
-0
lines changed

4 files changed

+274
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Running a Solidity smart contract using ethereumjs-vm
2+
3+
This directory contains an example on how to run smart contracts written in Solidity.
4+
5+
The example does these things:
6+
7+
1. Compiles the contract `contracts/Greeter.sol`
8+
1. Instantiates a VM
9+
1. Creates an account
10+
1. Funds the account with 1 ETH
11+
1. Deploys the Greeter contract
12+
1. Calls a constant function
13+
1. Sends a transaction to the contract, modifying its state
14+
1. Calls a constant function to verify the state change
15+
16+
## Installation
17+
18+
To install this example you should
19+
20+
1. Run `npm install` in the root of this project
21+
1. Run `npm install` in this directory
22+
23+
## Running the example
24+
25+
1. Run `npm run build:dist` in the root of this project
26+
1. Run `npm run example` in this directory
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
pragma solidity ^0.5.10;
2+
3+
contract Greeter {
4+
5+
string greeting;
6+
7+
constructor(string memory _greeting) public {
8+
greeting = _greeting;
9+
}
10+
11+
function setGreeting(string memory _greeting) public {
12+
greeting = _greeting;
13+
}
14+
15+
function greet() public view returns (string memory) {
16+
return greeting;
17+
}
18+
19+
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import VM from '../../dist'
2+
3+
import * as assert from 'assert'
4+
import * as path from 'path'
5+
import * as fs from 'fs'
6+
import { promisify } from 'util'
7+
import * as util from 'ethereumjs-util'
8+
import Account from 'ethereumjs-account'
9+
const Transaction = require('ethereumjs-tx') // Change when https://github.com/ethereumjs/ethereumjs-vm/pull/541 gets merged
10+
const abi = require('ethereumjs-abi')
11+
const solc = require('solc')
12+
13+
const INITIAL_GREETING = 'Hello, World!'
14+
const SECOND_GREETING = 'Hola, Mundo!'
15+
16+
/**
17+
* This function creates the input for the Solidity compiler.
18+
*
19+
* For more info about it, go to https://solidity.readthedocs.io/en/v0.5.10/using-the-compiler.html#compiler-input-and-output-json-description
20+
*/
21+
function getSolcInput() {
22+
return {
23+
language: 'Solidity',
24+
sources: {
25+
'contracts/Greeter.sol': {
26+
content: fs.readFileSync(path.join(__dirname, 'contracts', 'Greeter.sol'), 'utf8'),
27+
},
28+
// If more contracts were to be compiled, they should have their own entries here
29+
},
30+
settings: {
31+
optimizer: {
32+
enabled: true,
33+
runs: 200,
34+
},
35+
evmVersion: 'petersburg',
36+
outputSelection: {
37+
'*': {
38+
'*': ['abi', 'evm.bytecode'],
39+
},
40+
},
41+
},
42+
}
43+
}
44+
45+
/**
46+
* This function compiles all the contracts in `contracts/` and returns the Solidity Standard JSON
47+
* output. If the compilation fails, it returns `undefined`.
48+
*
49+
* To learn about the output format, go to https://solidity.readthedocs.io/en/v0.5.10/using-the-compiler.html#compiler-input-and-output-json-description
50+
*/
51+
function compileContracts() {
52+
const input = getSolcInput()
53+
const output = JSON.parse(solc.compile(JSON.stringify(input)))
54+
55+
let compilationFailed = false
56+
57+
if (output.errors) {
58+
for (const error of output.errors) {
59+
if (error.severity === 'error') {
60+
console.error(error.formattedMessage)
61+
compilationFailed = true
62+
} else {
63+
console.warn(error.formattedMessage)
64+
}
65+
}
66+
}
67+
68+
if (compilationFailed) {
69+
return undefined
70+
}
71+
72+
return output
73+
}
74+
75+
function getGreeterDeploymentBytecode(solcOutput: any): any {
76+
return solcOutput.contracts['contracts/Greeter.sol'].Greeter.evm.bytecode.object
77+
}
78+
79+
async function getAccountNonce(vm: VM, accountPrivateKey: Buffer) {
80+
const account = (await promisify(vm.stateManager.getAccount.bind(vm.stateManager))(
81+
util.privateToAddress(accountPrivateKey),
82+
)) as Account
83+
84+
return account.nonce
85+
}
86+
87+
async function deployContract(
88+
vm: VM,
89+
senderPrivateKey: Buffer,
90+
deploymentBytecode: Buffer,
91+
greeting: string,
92+
): Promise<Buffer> {
93+
// Contracts are deployed by sending their deployment bytecode to the address 0
94+
// The contract params should be abi-encoded and appended to the deployment bytecode.
95+
const params = abi.rawEncode(['string'], [greeting])
96+
97+
const tx = new Transaction({
98+
to: null,
99+
value: 0,
100+
gas: 2000000, // We assume that 2M is enough,
101+
gasPrice: 1,
102+
data: '0x' + deploymentBytecode + params.toString('hex'),
103+
nonce: await getAccountNonce(vm, senderPrivateKey),
104+
})
105+
106+
tx.sign(senderPrivateKey)
107+
108+
const deploymentResult = await vm.runTx({ tx })
109+
110+
if (deploymentResult.vm.exception === 0) {
111+
throw deploymentResult.vm.exceptionError
112+
}
113+
114+
return deploymentResult.createdAddress!
115+
}
116+
117+
async function setGreeting(
118+
vm: VM,
119+
senderPrivateKey: Buffer,
120+
contractAddress: Buffer,
121+
greeting: string,
122+
) {
123+
const params = abi.rawEncode(['string'], [greeting])
124+
125+
const tx = new Transaction({
126+
to: contractAddress,
127+
value: 0,
128+
gas: 2000000, // We assume that 2M is enough,
129+
gasPrice: 1,
130+
data: '0x' + abi.methodID('setGreeting', ['string']).toString('hex') + params.toString('hex'),
131+
nonce: await getAccountNonce(vm, senderPrivateKey),
132+
})
133+
134+
tx.sign(senderPrivateKey)
135+
136+
const setGreetingResult = await vm.runTx({ tx })
137+
138+
if (setGreetingResult.vm.exception === 0) {
139+
throw setGreetingResult.vm.exceptionError
140+
}
141+
}
142+
143+
async function getGreeting(vm: VM, contractAddress: Buffer, caller: Buffer) {
144+
const greetResult = await vm.runCall({
145+
to: contractAddress,
146+
caller: caller,
147+
origin: caller, // The tx.origin is also the caller here
148+
data: abi.methodID('greet', []),
149+
})
150+
151+
if (greetResult.vm.exception === 0) {
152+
throw greetResult.vm.exceptionError
153+
}
154+
155+
const results = abi.rawDecode(['string'], greetResult.vm.return)
156+
157+
return results[0]
158+
}
159+
160+
async function main() {
161+
const accountPk = new Buffer(
162+
'e331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109',
163+
'hex',
164+
)
165+
166+
const accountAddress = util.privateToAddress(accountPk)
167+
168+
console.log('Account:', util.bufferToHex(accountAddress))
169+
170+
const account = new Account({ balance: 1e18 })
171+
172+
const vm = new VM()
173+
await promisify(vm.stateManager.putAccount.bind(vm.stateManager))(accountAddress, account)
174+
175+
console.log('Set account a balance of 1 ETH')
176+
177+
console.log('Compiling...')
178+
179+
const solcOutput = compileContracts()
180+
if (solcOutput === undefined) {
181+
throw new Error('Compilation failed')
182+
} else {
183+
console.log('Compiled the contract')
184+
}
185+
186+
const bytecode = getGreeterDeploymentBytecode(solcOutput)
187+
188+
console.log('Deploying the contract...')
189+
190+
const contractAddress = await deployContract(vm, accountPk, bytecode, INITIAL_GREETING)
191+
192+
console.log('Contract address:', util.bufferToHex(contractAddress))
193+
194+
const greeting = await getGreeting(vm, contractAddress, accountAddress)
195+
196+
console.log('Greeting:', greeting)
197+
198+
assert.equal(greeting, INITIAL_GREETING)
199+
200+
console.log('Changing greeting...')
201+
202+
await setGreeting(vm, accountPk, contractAddress, SECOND_GREETING)
203+
204+
const greeting2 = await getGreeting(vm, contractAddress, accountAddress)
205+
206+
console.log('Greeting:', greeting2)
207+
208+
assert.equal(greeting2, SECOND_GREETING)
209+
210+
console.log('Everything run correctly!')
211+
}
212+
213+
main()
214+
.then(() => process.exit(0))
215+
.catch(err => {
216+
console.error(err)
217+
process.exit(1)
218+
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "run-solidity-contract",
3+
"main": "index.js",
4+
"scripts": {
5+
"example": "ts-node index.ts"
6+
},
7+
"dependencies": {
8+
"ethereumjs-abi": "^0.6.7",
9+
"solc": "^0.5.10"
10+
}
11+
}

0 commit comments

Comments
 (0)