mirror of
https://github.com/status-im/nim-contract-abi.git
synced 2025-02-19 22:18:28 +00:00
ABI Encoder extracted from nim-nitro module
This commit is contained in:
commit
8184529abd
5
.editorconfig
Normal file
5
.editorconfig
Normal file
@ -0,0 +1,5 @@
|
||||
[*]
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*
|
||||
!*/
|
||||
!*.*
|
1
.tool-versions
Normal file
1
.tool-versions
Normal file
@ -0,0 +1 @@
|
||||
nim 1.6.0
|
45
Readme.md
Normal file
45
Readme.md
Normal file
@ -0,0 +1,45 @@
|
||||
Contract ABI
|
||||
============
|
||||
|
||||
Implements encoding of parameters according to the Ethereum
|
||||
[Contract ABI Specification][1].
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Use the [Nimble][2] package manager to add `contractabi` to an existing project.
|
||||
Add the following to its .nimble file:
|
||||
|
||||
```nim
|
||||
requires "https://github.com/status-im/nim-contract-abi >= 0.1.0 & < 0.2.0"
|
||||
```
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
```nim
|
||||
import contractabi
|
||||
import stint
|
||||
|
||||
# encode unsigned integers, booleans, enums
|
||||
AbiEncoder.encode(42'u8)
|
||||
|
||||
# encode uint256
|
||||
AbiEncoder.encode(42.u256)
|
||||
|
||||
# encode byte arrays and sequences
|
||||
AbiEncoder.encode([1'u8, 2'u8, 3'u8])
|
||||
AbiEncoder.encode(@[1'u8, 2'u8, 3'u8])
|
||||
|
||||
# encode tuples
|
||||
var encoder = AbiEncoder.init()
|
||||
encoder.startTuple()
|
||||
encoder.write(42'u8)
|
||||
encoder.write(@[1'u8, 2'u8, 3'u8])
|
||||
encoder.write(true)
|
||||
encoder.finishTuple()
|
||||
encoder.finish()
|
||||
```
|
||||
|
||||
[1]: https://docs.soliditylang.org/en/latest/abi-spec.html
|
||||
[2]: https://github.com/nim-lang/nimble
|
3
contractabi.nim
Normal file
3
contractabi.nim
Normal file
@ -0,0 +1,3 @@
|
||||
import contractabi/encoding
|
||||
|
||||
export encoding
|
7
contractabi.nimble
Normal file
7
contractabi.nimble
Normal file
@ -0,0 +1,7 @@
|
||||
version = "0.1.0"
|
||||
author = "Contract ABI Authors"
|
||||
description = "ABI Encoding for Ethereum contracts"
|
||||
license = "MIT"
|
||||
|
||||
requires "stint"
|
||||
requires "upraises >= 0.1.0 & < 0.2.0"
|
124
contractabi/encoding.nim
Normal file
124
contractabi/encoding.nim
Normal file
@ -0,0 +1,124 @@
|
||||
import pkg/stint
|
||||
import pkg/upraises
|
||||
|
||||
push: {.upraises:[].}
|
||||
|
||||
type
|
||||
AbiEncoder* = object
|
||||
stack: seq[Tuple]
|
||||
Tuple = object
|
||||
bytes: seq[byte]
|
||||
postponed: seq[Split]
|
||||
dynamic: bool
|
||||
Split = object
|
||||
head: Slice[int]
|
||||
tail: seq[byte]
|
||||
|
||||
func write*[T](encoder: var AbiEncoder, value: T)
|
||||
func encode*[T](_: type AbiEncoder, value: T): seq[byte]
|
||||
|
||||
func init*(_: type AbiEncoder): AbiEncoder =
|
||||
AbiEncoder(stack: @[Tuple()])
|
||||
|
||||
func append(tupl: var Tuple, bytes: openArray[byte]) =
|
||||
tupl.bytes.add(bytes)
|
||||
|
||||
func postpone(tupl: var Tuple, bytes: seq[byte]) =
|
||||
var split: Split
|
||||
split.head.a = tupl.bytes.len
|
||||
tupl.append(AbiEncoder.encode(0'u64))
|
||||
split.head.b = tupl.bytes.high
|
||||
split.tail = bytes
|
||||
tupl.postponed.add(split)
|
||||
|
||||
func finish(tupl: Tuple): seq[byte] =
|
||||
var bytes = tupl.bytes
|
||||
for split in tupl.postponed:
|
||||
let offset = bytes.len
|
||||
bytes[split.head] = AbiEncoder.encode(offset.uint64)
|
||||
bytes.add(split.tail)
|
||||
bytes
|
||||
|
||||
func append(encoder: var AbiEncoder, bytes: openArray[byte]) =
|
||||
encoder.stack[^1].append(bytes)
|
||||
|
||||
func postpone(encoder: var AbiEncoder, bytes: seq[byte]) =
|
||||
if encoder.stack.len > 1:
|
||||
encoder.stack[^1].postpone(bytes)
|
||||
else:
|
||||
encoder.stack[0].append(bytes)
|
||||
|
||||
func setDynamic(encoder: var AbiEncoder) =
|
||||
encoder.stack[^1].dynamic = true
|
||||
|
||||
func startTuple*(encoder: var AbiEncoder) =
|
||||
encoder.stack.add(Tuple())
|
||||
|
||||
func encode(encoder: var AbiEncoder, tupl: Tuple) =
|
||||
if tupl.dynamic:
|
||||
encoder.postpone(tupl.finish())
|
||||
encoder.setDynamic()
|
||||
else:
|
||||
encoder.append(tupl.finish())
|
||||
|
||||
func finishTuple*(encoder: var AbiEncoder) =
|
||||
encoder.encode(encoder.stack.pop())
|
||||
|
||||
func pad(encoder: var AbiEncoder, len: int) =
|
||||
let padlen = (32 - len mod 32) mod 32
|
||||
for _ in 0..<padlen:
|
||||
encoder.append([0'u8])
|
||||
|
||||
func padleft(encoder: var AbiEncoder, bytes: openArray[byte]) =
|
||||
encoder.pad(bytes.len)
|
||||
encoder.append(bytes)
|
||||
|
||||
func padright(encoder: var AbiEncoder, bytes: openArray[byte]) =
|
||||
encoder.append(bytes)
|
||||
encoder.pad(bytes.len)
|
||||
|
||||
func encode(encoder: var AbiEncoder, value: SomeUnsignedInt | StUint) =
|
||||
encoder.padleft(value.toBytesBE)
|
||||
|
||||
func encode(encoder: var AbiEncoder, value: bool) =
|
||||
encoder.encode(cast[uint8](value))
|
||||
|
||||
func encode(encoder: var AbiEncoder, value: enum) =
|
||||
encoder.encode(uint64(ord(value)))
|
||||
|
||||
func encode[I](encoder: var AbiEncoder, bytes: array[I, byte]) =
|
||||
encoder.padright(bytes)
|
||||
|
||||
func encode(encoder: var AbiEncoder, bytes: seq[byte]) =
|
||||
encoder.encode(bytes.len.uint64)
|
||||
encoder.padright(bytes)
|
||||
encoder.setDynamic()
|
||||
|
||||
func encode[I, T](encoder: var AbiEncoder, value: array[I, T]) =
|
||||
encoder.startTuple()
|
||||
for element in value:
|
||||
encoder.write(element)
|
||||
encoder.finishTuple()
|
||||
|
||||
func encode[T](encoder: var AbiEncoder, value: seq[T]) =
|
||||
encoder.encode(value.len.uint64)
|
||||
encoder.startTuple()
|
||||
for element in value:
|
||||
encoder.write(element)
|
||||
encoder.finishTuple()
|
||||
encoder.setDynamic()
|
||||
|
||||
func write*[T](encoder: var AbiEncoder, value: T) =
|
||||
var writer = AbiEncoder.init()
|
||||
writer.encode(value)
|
||||
encoder.encode(writer.stack[0])
|
||||
|
||||
func finish*(encoder: var AbiEncoder): seq[byte] =
|
||||
doAssert encoder.stack.len == 1, "not all tuples were finished"
|
||||
doAssert encoder.stack[0].bytes.len mod 32 == 0, "encoding invariant broken"
|
||||
encoder.stack[0].bytes
|
||||
|
||||
func encode*[T](_: type AbiEncoder, value: T): seq[byte] =
|
||||
var encoder = AbiEncoder.init()
|
||||
encoder.write(value)
|
||||
encoder.finish()
|
25
tests/examples.nim
Normal file
25
tests/examples.nim
Normal file
@ -0,0 +1,25 @@
|
||||
import std/random
|
||||
import std/sequtils
|
||||
import pkg/stint
|
||||
|
||||
randomize()
|
||||
|
||||
proc example*(_: type bool): bool =
|
||||
rand(0'u8..1'u8) == 1
|
||||
|
||||
proc example*[T: SomeInteger](_: type T): T =
|
||||
rand(T)
|
||||
|
||||
proc example*[I: static int, T](_: type array[I, T]): array[I, T] =
|
||||
for i in 0..<I:
|
||||
result[i] = T.example
|
||||
|
||||
proc example*[T](_: type seq[T], len = 0..5): seq[T] =
|
||||
let chosenlen = rand(len)
|
||||
newSeqWith(chosenlen, T.example)
|
||||
|
||||
proc example*(_: type UInt256): UInt256 =
|
||||
UInt256.fromBytes(array[32, byte].example)
|
||||
|
||||
proc example*(_: type UInt128): UInt128 =
|
||||
UInt128.fromBytes(array[16, byte].example)
|
1
tests/nim.cfg
Normal file
1
tests/nim.cfg
Normal file
@ -0,0 +1 @@
|
||||
--path:".."
|
158
tests/testEncoding.nim
Normal file
158
tests/testEncoding.nim
Normal file
@ -0,0 +1,158 @@
|
||||
|
||||
import std/unittest
|
||||
import pkg/stint
|
||||
import contractabi
|
||||
import ./examples
|
||||
|
||||
suite "ABI encoding":
|
||||
|
||||
proc zeroes(amount: int): seq[byte] =
|
||||
newSeq[byte](amount)
|
||||
|
||||
test "encodes uint8":
|
||||
check AbiEncoder.encode(42'u8) == 31.zeroes & 42'u8
|
||||
|
||||
test "encodes booleans":
|
||||
check AbiEncoder.encode(false) == 31.zeroes & 0'u8
|
||||
check AbiEncoder.encode(true) == 31.zeroes & 1'u8
|
||||
|
||||
test "encodes uint16, 32, 64":
|
||||
check AbiEncoder.encode(0xABCD'u16) ==
|
||||
30.zeroes & 0xAB'u8 & 0xCD'u8
|
||||
check AbiEncoder.encode(0x11223344'u32) ==
|
||||
28.zeroes & 0x11'u8 & 0x22'u8 & 0x33'u8 & 0x44'u8
|
||||
check AbiEncoder.encode(0x1122334455667788'u64) ==
|
||||
24.zeroes &
|
||||
0x11'u8 & 0x22'u8 & 0x33'u8 & 0x44'u8 &
|
||||
0x55'u8 & 0x66'u8 & 0x77'u8 & 0x88'u8
|
||||
|
||||
test "encodes ranges":
|
||||
type SomeRange = range[0x0000'u16..0xAAAA'u16]
|
||||
check AbiEncoder.encode(SomeRange(0x1122)) == 30.zeroes & 0x11'u8 & 0x22'u8
|
||||
|
||||
test "encodes enums":
|
||||
type SomeEnum = enum
|
||||
one = 1
|
||||
two = 2
|
||||
check AbiEncoder.encode(one) == 31.zeroes & 1'u8
|
||||
check AbiEncoder.encode(two) == 31.zeroes & 2'u8
|
||||
|
||||
test "encodes stints":
|
||||
let uint256 = UInt256.example
|
||||
check AbiEncoder.encode(uint256) == @(uint256.toBytesBE)
|
||||
let uint128 = UInt128.example
|
||||
check AbiEncoder.encode(uint128) == 16.zeroes & @(uint128.toBytesBE)
|
||||
|
||||
test "encodes byte arrays":
|
||||
let bytes3 = [1'u8, 2'u8, 3'u8]
|
||||
check AbiEncoder.encode(bytes3) == @bytes3 & 29.zeroes
|
||||
let bytes32 = array[32, byte].example
|
||||
check AbiEncoder.encode(bytes32) == @bytes32
|
||||
let bytes33 = array[33, byte].example
|
||||
check AbiEncoder.encode(bytes33) == @bytes33 & 31.zeroes
|
||||
|
||||
test "encodes byte sequences":
|
||||
let bytes3 = @[1'u8, 2'u8, 3'u8]
|
||||
let bytes3len = AbiEncoder.encode(bytes3.len.uint64)
|
||||
check AbiEncoder.encode(bytes3) == bytes3len & bytes3 & 29.zeroes
|
||||
let bytes32 = @(array[32, byte].example)
|
||||
let bytes32len = AbiEncoder.encode(bytes32.len.uint64)
|
||||
check AbiEncoder.encode(bytes32) == bytes32len & bytes32
|
||||
let bytes33 = @(array[33, byte].example)
|
||||
let bytes33len = AbiEncoder.encode(bytes33.len.uint64)
|
||||
check AbiEncoder.encode(bytes33) == bytes33len & bytes33 & 31.zeroes
|
||||
|
||||
test "encodes tuples":
|
||||
let a = true
|
||||
let b = @[1'u8, 2'u8, 3'u8]
|
||||
let c = 0xAABBCCDD'u32
|
||||
let d = @[4'u8, 5'u8, 6'u8]
|
||||
var encoder= AbiEncoder.init()
|
||||
encoder.startTuple()
|
||||
encoder.write(a)
|
||||
encoder.write(b)
|
||||
encoder.write(c)
|
||||
encoder.write(d)
|
||||
encoder.finishTuple()
|
||||
check encoder.finish() ==
|
||||
AbiEncoder.encode(a) &
|
||||
AbiEncoder.encode(4 * 32'u8) & # offset in tuple
|
||||
AbiEncoder.encode(c) &
|
||||
AbiEncoder.encode(6 * 32'u8) & # offset in tuple
|
||||
AbiEncoder.encode(b) &
|
||||
AbiEncoder.encode(d)
|
||||
|
||||
test "encodes nested tuples":
|
||||
let a = true
|
||||
let b = @[1'u8, 2'u8, 3'u8]
|
||||
let c = 0xAABBCCDD'u32
|
||||
let d = @[4'u8, 5'u8, 6'u8]
|
||||
var encoder= AbiEncoder.init()
|
||||
encoder.startTuple()
|
||||
encoder.write(a)
|
||||
encoder.write(b)
|
||||
encoder.startTuple()
|
||||
encoder.write(c)
|
||||
encoder.write(d)
|
||||
encoder.finishTuple()
|
||||
encoder.finishTuple()
|
||||
check encoder.finish() ==
|
||||
AbiEncoder.encode(a) &
|
||||
AbiEncoder.encode(3 * 32'u8) & # offset of b in outer tuple
|
||||
AbiEncoder.encode(5 * 32'u8) & # offset of inner tuple in outer tuple
|
||||
AbiEncoder.encode(b) &
|
||||
AbiEncoder.encode(c) &
|
||||
AbiEncoder.encode(2 * 32'u8) & # offset of d in inner tuple
|
||||
AbiEncoder.encode(d)
|
||||
|
||||
test "encodes arrays":
|
||||
let element1 = seq[byte].example
|
||||
let element2 = seq[byte].example
|
||||
var expected= AbiEncoder.init()
|
||||
expected.startTuple()
|
||||
expected.write(element1)
|
||||
expected.write(element2)
|
||||
expected.finishTuple()
|
||||
check AbiEncoder.encode([element1, element2]) == expected.finish()
|
||||
|
||||
test "encodes sequences":
|
||||
let element1 = seq[byte].example
|
||||
let element2 = seq[byte].example
|
||||
var expected= AbiEncoder.init()
|
||||
expected.write(2'u8)
|
||||
expected.startTuple()
|
||||
expected.write(element1)
|
||||
expected.write(element2)
|
||||
expected.finishTuple()
|
||||
check AbiEncoder.encode(@[element1, element2]) == expected.finish()
|
||||
|
||||
test "encodes sequence as dynamic element":
|
||||
let s = @[42.u256, 43.u256]
|
||||
var encoder= AbiEncoder.init()
|
||||
encoder.startTuple()
|
||||
encoder.write(s)
|
||||
encoder.finishTuple()
|
||||
check encoder.finish() ==
|
||||
AbiEncoder.encode(32'u8) & # offset in tuple
|
||||
AbiEncoder.encode(s)
|
||||
|
||||
test "encodes array of static elements as static element":
|
||||
let a = [[42'u8], [43'u8]]
|
||||
var encoder= AbiEncoder.init()
|
||||
encoder.startTuple()
|
||||
encoder.write(a)
|
||||
encoder.finishTuple()
|
||||
check encoder.finish() == AbiEncoder.encode(a)
|
||||
|
||||
test "encodes array of dynamic elements as dynamic element":
|
||||
let a = [@[42'u8], @[43'u8]]
|
||||
var encoder= AbiEncoder.init()
|
||||
encoder.startTuple()
|
||||
encoder.write(a)
|
||||
encoder.finishTuple()
|
||||
check encoder.finish() ==
|
||||
AbiEncoder.encode(32'u8) & # offset in tuple
|
||||
AbiEncoder.encode(a)
|
||||
|
||||
# https://medium.com/b2expand/abi-encoding-explanation-4f470927092d
|
||||
# https://docs.soliditylang.org/en/v0.8.1/abi-spec.html#formal-specification-of-the-encoding
|
Loading…
x
Reference in New Issue
Block a user