movement-sdk
Version:
Movement SDK
371 lines (333 loc) • 12.9 kB
text/typescript
import fs from "fs";
import {MvInfo, MvTable} from "./comm_interface";
import {logger} from "./logger";
interface Input {
internalType: string;
name: string;
type: string;
}
interface Output {
internalType: string;
name: string;
type: string;
}
interface FunctionDefinition {
inputs: Input[];
name: string;
outputs: Output[];
stateMutability: string;
type: string;
opcodes: string[];
}
interface ContractDefinition {
owner: string;
name: string;
functions: FunctionDefinition[];
}
const EVM_TO_MOVE_TYPE = new Map()
EVM_TO_MOVE_TYPE.set("uint64", 3) // u64
const EVM_TO_MOVE_VISIBILITY = new Map()
EVM_TO_MOVE_VISIBILITY.set("view", 1) // public
EVM_TO_MOVE_VISIBILITY.set("pure", 1)// public
const SUPPORT_CODE = ["ADD", "RETURN", "SUB"]
function getMoveSignature(evmType: string): string {
const t = EVM_TO_MOVE_TYPE.get(evmType)
if (!t) throw new Error(`parse signature error, now not support ${evmType}`)
return t
}
function getFunVisibility(evmType: string): string {
const t = EVM_TO_MOVE_VISIBILITY.get(evmType)
if (!t) throw new Error(`parse visibility error, now not support ${evmType}`)
return t
}
function normalizeAddress(address: string): string {
let ret = address
if (address.startsWith("0x"))
ret = address.slice(2)
return ret.toLowerCase();
}
function numToHex(num: number): string {
let str = num.toString(16)
if (str.length % 2 !== 0) {
str = `0${str}`
}
return str
}
function strToHex(str: string): string {
let hexStr = Buffer.from(str, "utf8").toString("hex")
if (hexStr.length % 2 !== 0) {
hexStr = `0${hexStr}`
}
return hexStr
}
export class BuildCode {
private schema: ContractDefinition;
private info: MvInfo
private code: string = ""
constructor(schema: ContractDefinition) {
this.schema = schema;
this.info = {magicValue: "", selfModule: 0, tableCount: 0, tables: [], version: 0}
}
private static getBaseTable(): MvTable {
return {
kind: "00",
offset: 0,
size: 0,
content: "",
name: "",
index: [],
payload: []
}
}
build() {
// We must maintain the following function call order
this.header();
this.module_handles()
this.function_handles()
this.signature()
this.identifiers()
this.address_identifiers()
this.metadata()
this.function_definitions()
this.parse_fun()
this.inner_build();
}
private inner_build() {
logger.info("movement build bytecode processing")
const buf = Buffer.alloc(4)
buf.writeUint32LE(this.info.version, 0)
this.code = this.info.magicValue.concat(
buf.toString("hex"),
numToHex(this.info.tableCount))
const base = this.info.tables[0]
base.offset = 0
Object.values<number>(base.index).forEach((it) => {
base.content += numToHex(it)
base.size += 1
})
this.code += base.kind.concat(numToHex(base.offset), numToHex(base.size))
for (let i = 1; i < this.info.tables.length; i += 1) {
const table = this.info.tables[i]
const preTable = this.info.tables[i - 1]
if (table.kind === "03") {
table.payload.forEach((it: any) => {
table.content += numToHex(it.module).concat(
numToHex(it.name),
numToHex(it.parameters),
numToHex(it.return),
numToHex(it.type_parameters.length),
)
// TODO add type_parameters content
})
}
if (table.kind === "05") {
table.payload.forEach((it: any) => {
table.content += numToHex(it.length)
it.forEach((el: any) => {
table.content += numToHex(el)
})
})
}
if (table.kind === "07") {
table.payload.forEach((it: any) => {
const content = strToHex(it)
table.content += numToHex(content.length / 2).concat(content)
})
}
if (table.kind === "08") {
table.content = table.payload.join("")
}
if (table.kind === "10") {
const key = strToHex(table.payload.key)
const {value} = table.payload
table.content += numToHex(key.length / 2).concat(key)
table.content += numToHex(value.length / 2).concat(value)
}
if (table.kind === "0c") {
const doubleBytes = ["0b"]
table.payload.forEach((it: any) => {
table.content += numToHex(it.function_handle).concat(
numToHex(it.visibility),
numToHex(it.is_entry),
numToHex(it.acquires_global_resources.length), // TODO:add the array content
numToHex(it.locals),
numToHex(it.bytecode.filter((x: string) => !doubleBytes.includes(x)).length),
it.bytecode.join("")
)
})
}
table.offset = preTable.offset + preTable.size
table.size = table.content.length / 2;
this.code += table.kind.concat(numToHex(table.offset), numToHex(table.size))
}
for (let i = 0; i < this.info.tables.length; i += 1) {
this.code += this.info.tables[i].content
}
// Todo why?
const selfModuleIndex = "00"
this.code = this.code.concat(selfModuleIndex)
logger.success("movement build bytecode successfully")
}
save(p: string) {
fs.writeFileSync(p, Buffer.from(this.code, "hex"));
logger.success(`movement save bytecode to ${p} successfully`)
}
private header() {
logger.info("movement start build bytecode header")
// now this is fixed value
this.info.magicValue = "a11ceb0b"
this.info.version = 6
this.info.tableCount = 7
}
private module_handles() {
logger.info("movement start build bytecode module_handles")
// now we think this table is fixed
const table = BuildCode.getBaseTable();
table.kind = "01"
table.name = "MODULE_HANDLES"
table.offset = 0
table.index = {address: 0, name: 0}
this.info.tables.push(table)
}
private function_handles() {
logger.info("movement start build bytecode function_handles")
const table = BuildCode.getBaseTable()
table.kind = "03"
table.name = "FUNCTION_HANDLES"
table.offset = 0
this.info.tables.push(table)
}
private signature() {
logger.info("movement start build bytecode signature")
const table = BuildCode.getBaseTable()
table.kind = "05"
table.name = "SIGNATURES"
// this always exist empty parameters or return values, so add to this table at the first
table.payload.push([])
this.info.tables.push(table)
}
private identifiers() {
logger.info("movement start build bytecode identifiers")
const table = BuildCode.getBaseTable()
table.kind = "07"
table.name = "IDENTIFIERS"
table.payload.push(this.schema.name)
this.info.tables.push(table)
}
private address_identifiers() {
logger.info("movement start build bytecode address_identifiers")
// now we think this table is fixed
const table = BuildCode.getBaseTable()
table.kind = "08"
table.name = "ADDRESS_IDENTIFIERS"
table.payload.push(normalizeAddress(this.schema.owner))
this.info.tables.push(table)
}
private metadata() {
logger.info("movement start build bytecode metadata")
const table = BuildCode.getBaseTable()
table.kind = "10"
table.name = "METADATA"
// now we think this table is fixed
table.payload = {
key: "aptos::metadata_v1",
value: ""
}
this.info.tables.push(table)
}
private function_definitions() {
logger.info("movement start build bytecode function_definitions")
const table = BuildCode.getBaseTable()
table.kind = "0c"
table.name = "FUNCTION_DEFINITIONS"
this.info.tables.push(table)
}
private parse_fun() {
logger.info("movement start parse bytecode function")
const tableIdentifiers = this.info.tables.find(it => it.kind === "07")!
const tableMetadata = this.info.tables.find(it => it.kind === "10")!
const tableSignatures = this.info.tables.find(it => it.kind === "05")!
const tableFunHandler = this.info.tables.find(it => it.kind === "03")!
const tableFunDefinitions = this.info.tables.find(it => it.kind === "0c")!
function signatureIndex(t: string[]) {
return tableSignatures.payload.findIndex((it: any) => JSON.stringify(it) === JSON.stringify(t))
}
const functions = this.schema.functions.filter(it => it.type === "function")
for (let i = 0; i < functions.length; i += 1) {
const fun = functions[i]
tableIdentifiers.payload.push(fun.name)
const funHex = strToHex(fun.name)
tableMetadata.payload.value += `000001${numToHex(funHex.length / 2)}${funHex}010100`
let parametersIndex = 0;
let returnIndex = 0;
const funInputType = fun.inputs.map(it => getMoveSignature(it.type))
if (funInputType.length > 0) {
const index = signatureIndex(funInputType)
if (index > -1) {
parametersIndex = index
} else {
parametersIndex = tableSignatures.payload.push(funInputType) - 1
}
}
const funOutputType = fun.outputs.map(it => getMoveSignature(it.type))
if (funOutputType.length > 0) {
const index = signatureIndex(funOutputType)
if (index > -1) {
returnIndex = index
} else {
returnIndex = tableSignatures.payload.push(funOutputType) - 1
}
}
tableFunHandler.payload.push({
module: 0, // now we think it is fixed value,
name: i + 1, // as the module name is first, so add 1,
type_parameters: [], // TODO decode more type parameters,
parameters: parametersIndex,
return: returnIndex,
})
const def: {
function_handle: any,
visibility: string,
is_entry: number,
acquires_global_resources: [],
locals: number,
bytecode: string[]
} = {
function_handle: tableFunHandler.payload.length - 1,
visibility: getFunVisibility(fun.stateMutability),
is_entry: 0,// TODO parse why ?
acquires_global_resources: [], // TODO parse why ?,
// ULEB128 index into the SIGNATURES table for the types of the locals of the function,
// this should parse according to code ,but now we just set it as fixed value
locals: 0,
bytecode: []
}
const codes = fun.opcodes;
for (let i1 = 0; i1 < codes.length; i1 += 1) {
const code = codes[i1];
if (!SUPPORT_CODE.includes(code)) {
throw new Error(`current not support ${code}`)
}
if (code === "ADD") {
for (let j = 0; j < funInputType.length; j += 1) {
def.bytecode.push("0b")
def.bytecode.push(`0${j}`)
}
def.bytecode.push("16")
}
if (code === "SUB") {
for (let j = 0; j < funInputType.length; j += 1) {
def.bytecode.push("0b")
def.bytecode.push(`0${j}`)
}
def.bytecode.push("17")
}
if (code === "RETURN") {
def.bytecode.push("02")
}
}
tableFunDefinitions.payload.push(def)
logger.success("movement start parse bytecode function successfully")
}
}
}