Last active
April 13, 2019 14:18
-
-
Save masonforest/ea64634eacc2d1715ed237f430c25b25 to your computer and use it in GitHub Desktop.
A Javascript class for interacting with smart contracts written for the [Perlin network](https://www.perlin.net/).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const crypto = require('crypto'); | |
const { StringDecoder } = require("string_decoder"); | |
const fs = require("fs"); | |
const util = require('util'); | |
const writeFile = util.promisify(fs.writeFile); | |
const readFile = util.promisify(fs.readFile); | |
const fileExists = util.promisify(fs.exists); | |
const _ = require("lodash"); | |
const TRANSACTION_ID = new Uint8Array(256); | |
const ALICE = new Uint8Array(256).fill(1); | |
const BOB = new Uint8Array(256).fill(2); | |
const USERS = { | |
"alice": ALICE, | |
"bob": BOB, | |
} | |
const WEB_ASSEMBLY_PAGE_SIZE = 2**16; | |
class SmartContract { | |
constructor(fileName, { | |
stateFileName | |
}) { | |
this.fileName = fileName; | |
this.stateFileName = stateFileName; | |
} | |
async load(sender) { | |
const amount = Int32toBytes(0); | |
this.payload = this.parametersToBytes([TRANSACTION_ID, sender, amount]); | |
let buf = new Uint8Array(await readFile(this.fileName)); | |
let { instance } = await WebAssembly.instantiate(buf, { | |
env: this.env(), | |
}); | |
this.instance = instance; | |
await this.loadState(); | |
} | |
async loadState() { | |
if(await fileExists(this.stateFileName)) { | |
let state = await readFile(this.stateFileName); | |
let growMemoryBy = (state.length - this.instance.exports.memory.buffer.byteLength) / WEB_ASSEMBLY_PAGE_SIZE; | |
this.instance.exports.memory.grow(growMemoryBy); | |
let memory = new Uint8Array(this.instance.exports.memory.buffer, 0, state.length); | |
state.forEach((value, index) => memory[index] = value) | |
// for (let i = 0; i < state.length; i++) { | |
// memory[i] = state[i]; | |
// } | |
} | |
} | |
async writeState() { | |
let state = Buffer.from(this.instance.exports.memory.buffer); | |
await writeFile(this.stateFileName, state); | |
} | |
async call(method, ...parameters) { | |
this.payload = this.parametersToBytes(parameters); | |
return new Promise((resolve, reject) => { | |
this.reject = reject; | |
this.resolve = resolve; | |
this.instance.exports[`_contract_${method}`](); | |
}); | |
} | |
parametersToBytes(parameters) { | |
return new Uint8Array( | |
_.flatten( | |
parameters.map(parameter => { | |
if (typeof parameter === "number") { | |
return Array.from(Int64toBytes(parameter)); | |
} else { | |
return Array.from(Int32toBytes(parameter.length)).concat( | |
Array.from(parameter) | |
); | |
} | |
}) | |
) | |
); | |
} | |
readPointer(pointer, length) { | |
return new Uint8Array(this.instance.exports.memory.buffer, pointer, length); | |
} | |
bytesToString(bytes) { | |
let decoder = new StringDecoder("utf8"); | |
return decoder.write(new Buffer(bytes)); | |
} | |
writeMemory(buffer, offset = 0) { | |
var memory = new Uint8Array( | |
this.instance.exports.memory.buffer, | |
offset, | |
buffer.byteLength | |
); | |
buffer.forEach((value, index) => (memory[index] = value)); | |
} | |
env() { | |
return { | |
_payload_len: () => this.payload.byteLength, | |
_payload: pointer => { | |
this.writeMemory(this.payload, pointer); | |
}, | |
_result: async (pointer, length) => { | |
await this.writeState(); | |
this.resolve(this.readPointer(pointer, length)); | |
}, | |
_log: (pointer, length) => { | |
console.log(this.bytesToString(this.readPointer(pointer, length))); | |
}, | |
...this.exports | |
}; | |
} | |
} | |
function Int64toBytes(num) { | |
var arr = new ArrayBuffer(8); | |
var view = new DataView(arr); | |
view.setUint32(0, num, true); | |
return new Uint8Array(arr); | |
} | |
function Int32toBytes(num) { | |
var arr = new ArrayBuffer(4); | |
var view = new DataView(arr); | |
view.setUint32(0, num, true); | |
return new Uint8Array(arr); | |
} | |
function bytesToInt64(buffer, littleEndian = true) { | |
var arr = new ArrayBuffer(8); | |
var view = new DataView(arr); | |
buffer.forEach((value, index) => view.setUint8(index, value)); | |
const left = view.getUint32(0, littleEndian); | |
const right = view.getUint32(4, littleEndian); | |
const combined = littleEndian | |
? left + 2 ** 32 * right | |
: 2 ** 32 * left + right; | |
if (!Number.isSafeInteger(combined)) | |
console.warn(combined, "exceeds MAX_SAFE_INTEGER. Precision may be lost"); | |
return combined; | |
} | |
async function transfer(token, sender, recipient, amount) { | |
let result = await token.call( | |
"transfer", | |
TRANSACTION_ID, | |
sender, | |
amount, | |
recipient, | |
amount | |
); | |
return result[0] == 1; | |
} | |
async function balanceOf(token, walletAddress) { | |
const amount = Int32toBytes(0); | |
let result = await token.call( | |
"balance", | |
TRANSACTION_ID, | |
ALICE, | |
amount, | |
walletAddress | |
); | |
return bytesToInt64(result); | |
} | |
async function logBalance(token, userName) { | |
let user = USERS[userName]; | |
console.log(`${_.startCase(userName)}'s balance: ` + (await balanceOf(token, user))); | |
} | |
async function logTransfer(token, senderUserName, recipientUserName, amount) { | |
let sender = USERS[senderUserName]; | |
let recipient = USERS[recipientUserName]; | |
let result = await transfer(token, sender, recipient, amount); | |
if (result) { | |
console.log(`Transferred ${amount} from ${_.startCase(senderUserName)} to ${_.startCase(recipientUserName)}`); | |
console.log("New Balances:"); | |
await logBalance(token, senderUserName); | |
await logBalance(token, recipientUserName); | |
} else { | |
console.log(`Tried to transfer ${amount} from ${_.startCase(senderUserName)} but ${_.startCase(senderUserName)} only had ${await balanceOf(token, sender)}`) | |
} | |
} | |
async function run() { | |
let token = new SmartContract( | |
"./target/wasm32-unknown-unknown/release/token.wasm", | |
{ | |
stateFileName: "state.bin" | |
} | |
); | |
await token.load(ALICE); | |
switch(process.argv[2]) { | |
case "balance": { | |
logBalance(token, process.argv[3]); | |
break; | |
} | |
case "transfer": { | |
logTransfer(token, process.argv[3], process.argv[4], parseInt(process.argv[5])); | |
break; | |
} | |
} | |
} | |
run(); | |
/* | |
$ node run.js balance alice | |
Alice's balance: 100000 | |
$ node run.js balance bob | |
Bob's balance: 0 | |
$ node run.js transfer alice bob 100 | |
Transferred 100 from Alice to Bob | |
New Balances: | |
Alice's balance: 99900 | |
Bob's balance: 100 | |
$ node run.js transfer bob alice 101 | |
Tried to transfer 101 from Bob but Bob only had 100 | |
$ node run.js transfer bob alice 50 | |
Transferred 50 from Bob to Alice | |
New Balances: | |
Bob's balance: 50 | |
Alice's balance: 99950 | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment