Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A Javascript class for interacting with smart contracts written for the [Perlin network](https://www.perlin.net/).
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
You can’t perform that action at this time.