Skip to content

Commit ff6075a

Browse files
authored
Support encode/decode functions on hosted abi (graphprotocol#2348)
* runtime: export `ethereum_encode` * runtime: export `ethereum_decode` * integration-tests: rename `big-decimal` to `host-exports` * integration-tests: add ethereum abi to host-exports * integration-tests: refactor ethereum abi and add another case * integration-tests: bump `graph-ts` to new master branch hash
1 parent fec450a commit ff6075a

File tree

12 files changed

+177
-72
lines changed

12 files changed

+177
-72
lines changed

runtime/wasm/src/host_exports.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use crate::{
33
module::IntoTrap,
44
UnresolvedContractCall,
55
};
6-
use ethabi::{Address, Token};
6+
use ethabi::param_type::Reader;
7+
use ethabi::{decode, encode, Address, Token};
78
use graph::bytes::Bytes;
89
use graph::components::ethereum::*;
910
use graph::components::store::EntityKey;
@@ -812,6 +813,22 @@ pub(crate) fn bytes_to_string(logger: &Logger, bytes: Vec<u8>) -> String {
812813
s.trim_end_matches('\u{0000}').to_string()
813814
}
814815

816+
pub(crate) fn ethereum_encode(token: Token) -> Result<Vec<u8>, anyhow::Error> {
817+
Ok(encode(&[token]))
818+
}
819+
820+
pub(crate) fn ethereum_decode(types: String, data: Vec<u8>) -> Result<Token, anyhow::Error> {
821+
let param_types =
822+
Reader::read(&types).or_else(|e| Err(anyhow::anyhow!("Failed to read types: {}", e)))?;
823+
824+
decode(&[param_types], &data)
825+
// The `.pop().unwrap()` here is ok because we're always only passing one
826+
// `param_types` to `decode`, so the returned `Vec` has always size of one.
827+
// We can't do `tokens[0]` because the value can't be moved out of the `Vec`.
828+
.map(|mut tokens| tokens.pop().unwrap())
829+
.context("Failed to decode")
830+
}
831+
815832
#[test]
816833
fn test_string_to_h160_with_0x() {
817834
assert_eq!(

runtime/wasm/src/module/mod.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,9 @@ impl WasmInstance {
476476
})?;
477477
}
478478

479+
link!("ethereum.encode", ethereum_encode, params_ptr);
480+
link!("ethereum.decode", ethereum_decode, params_ptr, data_ptr);
481+
479482
link!("abort", abort, message_ptr, file_name_ptr, line, column);
480483

481484
link!("store.get", store_get, "host_export_store_get", entity, id);
@@ -1369,6 +1372,31 @@ impl WasmInstanceContext {
13691372
self.ctx.host_exports.log_log(&self.ctx.logger, level, msg)
13701373
}
13711374

1375+
/// function encode(token: ethereum.Value): Bytes | null
1376+
fn ethereum_encode(
1377+
&mut self,
1378+
token_ptr: AscPtr<AscEnum<EthereumValueKind>>,
1379+
) -> Result<AscPtr<Uint8Array>, DeterministicHostError> {
1380+
let data = host_exports::ethereum_encode(self.asc_get(token_ptr)?);
1381+
// return `null` if it fails
1382+
data.map(|bytes| self.asc_new(&*bytes))
1383+
.unwrap_or(Ok(AscPtr::null()))
1384+
}
1385+
1386+
/// function decode(types: String, data: Bytes): ethereum.Value | null
1387+
fn ethereum_decode(
1388+
&mut self,
1389+
types_ptr: AscPtr<AscString>,
1390+
data_ptr: AscPtr<Uint8Array>,
1391+
) -> Result<AscPtr<AscEnum<EthereumValueKind>>, DeterministicHostError> {
1392+
let result =
1393+
host_exports::ethereum_decode(self.asc_get(types_ptr)?, self.asc_get(data_ptr)?);
1394+
// return `null` if it fails
1395+
result
1396+
.map(|param| self.asc_new(&param))
1397+
.unwrap_or(Ok(AscPtr::null()))
1398+
}
1399+
13721400
/// function arweave.transactionData(txId: string): Bytes | null
13731401
fn arweave_transaction_data(
13741402
&mut self,

tests/integration-tests/big-decimal/src/mapping.ts

Lines changed: 0 additions & 65 deletions
This file was deleted.

tests/integration-tests/big-decimal/package.json renamed to tests/integration-tests/host-exports/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
2-
"name": "big-decimal",
2+
"name": "host-exports",
33
"version": "0.1.0",
44
"scripts": {
55
"build-contracts": "../common/build-contracts.sh",
66
"codegen": "graph codegen",
77
"test": "yarn build-contracts && truffle test --compile-none --network test",
8-
"create:test": "graph create test/big-decimal --node $GRAPH_NODE_ADMIN_URI",
9-
"deploy:test": "graph deploy test/big-decimal --ipfs $IPFS_URI --node $GRAPH_NODE_ADMIN_URI"
8+
"create:test": "graph create test/host-exports --node $GRAPH_NODE_ADMIN_URI",
9+
"deploy:test": "graph deploy test/host-exports --ipfs $IPFS_URI --node $GRAPH_NODE_ADMIN_URI"
1010
},
1111
"devDependencies": {
1212
"@graphprotocol/graph-cli": "https://github.com/graphprotocol/graph-cli#master",
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { Trigger } from "../generated/Contract/Contract";
2+
import { Address, BigDecimal, BigInt, ethereum } from "@graphprotocol/graph-ts";
3+
4+
// Test that host exports work in globals.
5+
let one = BigDecimal.fromString("1");
6+
7+
export function handleTrigger(event: Trigger): void {
8+
testBigDecimal();
9+
testEthereumAbi();
10+
}
11+
12+
function testBigDecimal(): void {
13+
// There are 35 digits after the dot.
14+
// big_decimal exponent will be: -35 - 6109 = -6144.
15+
// Minimum exponent is: -6143.
16+
// So 1 digit will be removed, the 8, and the 6 will be rounded to 7.
17+
let small = BigDecimal.fromString("0.99999999999999999999999999999999968");
18+
19+
small.exp -= BigInt.fromI32(6109);
20+
21+
// Round-trip through the node so it truncates.
22+
small = small * new BigDecimal(BigInt.fromI32(1));
23+
24+
assert(small.exp == BigInt.fromI32(-6143), "wrong exp");
25+
26+
// This has 33 nines and the 7 which was rounded from 6.
27+
assert(
28+
small.digits ==
29+
BigDecimal.fromString("9999999999999999999999999999999997").digits,
30+
"wrong digits " + small.digits.toString()
31+
);
32+
33+
// This has 35 nines, but we round to 34 decimal digits.
34+
let big = BigDecimal.fromString("99999999999999999999999999999999999");
35+
36+
// This has 35 zeros.
37+
let rounded = BigDecimal.fromString("100000000000000000000000000000000000");
38+
39+
assert(big == rounded, "big not equal to rounded");
40+
41+
// This has 35 eights, but we round to 34 decimal digits.
42+
let big2 = BigDecimal.fromString("88888888888888888888888888888888888");
43+
44+
// This has 33 eights.
45+
let rounded2 = BigDecimal.fromString("88888888888888888888888888888888890");
46+
47+
assert(big2 == rounded2, "big2 not equal to rounded2 " + big2.toString());
48+
49+
// Test big decimal division.
50+
assert(one / BigDecimal.fromString("10") == BigDecimal.fromString("0.1"));
51+
52+
// Test big int fromString
53+
assert(BigInt.fromString("8888") == BigInt.fromI32(8888));
54+
55+
let bigInt = BigInt.fromString("8888888888888888");
56+
57+
// Test big int bit or
58+
assert(
59+
(bigInt | BigInt.fromI32(42)) == BigInt.fromString("8888888888888890")
60+
);
61+
62+
// Test big int bit and
63+
assert((bigInt & BigInt.fromI32(42)) == BigInt.fromI32(40));
64+
65+
// Test big int left shift
66+
assert(bigInt << 6 == BigInt.fromString("568888888888888832"));
67+
68+
// Test big int right shift
69+
assert(bigInt >> 6 == BigInt.fromString("138888888888888"));
70+
}
71+
72+
function testEthereumAbi(): void {
73+
ethereumAbiSimpleCase();
74+
ethereumAbiComplexCase();
75+
}
76+
77+
function ethereumAbiSimpleCase(): void {
78+
let address = ethereum.Value.fromAddress(Address.fromString("0x0000000000000000000000000000000000000420"));
79+
80+
let encoded = ethereum.encode(address)!;
81+
82+
let decoded = ethereum.decode("address", encoded);
83+
84+
assert(address.toAddress() == decoded.toAddress(), "address ethereum encoded does not equal the decoded value");
85+
}
86+
87+
function ethereumAbiComplexCase(): void {
88+
let address = ethereum.Value.fromAddress(Address.fromString("0x0000000000000000000000000000000000000420"));
89+
let bigInt1 = ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(62));
90+
let bigInt2 = ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(63));
91+
let bool = ethereum.Value.fromBoolean(true);
92+
93+
let fixedSizedArray = ethereum.Value.fromFixedSizedArray([
94+
bigInt1,
95+
bigInt2
96+
]);
97+
98+
let tupleArray: Array<ethereum.Value> = [
99+
fixedSizedArray,
100+
bool
101+
];
102+
103+
let tuple = ethereum.Value.fromTuple(tupleArray as ethereum.Tuple);
104+
105+
let token: Array<ethereum.Value> = [
106+
address,
107+
tuple
108+
];
109+
110+
let encoded = ethereum.encode(ethereum.Value.fromTuple(token as ethereum.Tuple))!;
111+
112+
let decoded = ethereum.decode("(address,(uint256[2],bool))", encoded).toTuple();
113+
114+
let decodedAddress = decoded[0].toAddress();
115+
let decodedTuple = decoded[1].toTuple();
116+
let decodedFixedSizedArray = decodedTuple[0].toArray();
117+
let decodedBigInt1 = decodedFixedSizedArray[0].toBigInt();
118+
let decodedBigInt2 = decodedFixedSizedArray[1].toBigInt();
119+
let decodedBool = decodedTuple[1].toBoolean();
120+
121+
assert(address.toAddress() == decodedAddress, "address ethereum encoded does not equal the decoded value");
122+
assert(bigInt1.toBigInt() == decodedBigInt1, "uint256[0] ethereum encoded does not equal the decoded value");
123+
assert(bigInt2.toBigInt() == decodedBigInt2, "uint256[1] ethereum encoded does not equal the decoded value");
124+
assert(bool.toBoolean() == decodedBool, "boolean ethereum encoded does not equal the decoded value");
125+
}

tests/integration-tests/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
"private": true,
33
"workspaces": [
44
"arweave-and-3box",
5-
"big-decimal",
65
"data-source-context",
76
"data-source-revert",
87
"fatal-error",
98
"ganache-reverts",
9+
"host-exports",
1010
"non-fatal-errors",
1111
"overloaded-contract-functions",
1212
"remove-then-update",

0 commit comments

Comments
 (0)