Skip to content

Commit 8c6561c

Browse files
refactor: update transaction value handling in rofl-client and rofl-appd
test: add unit tests for parse_u256_string, tx value handling chore: add TypeScript configuration and Jest setup for rofl-client
1 parent fd077e6 commit 8c6561c

File tree

15 files changed

+260
-46
lines changed

15 files changed

+260
-46
lines changed

docs/rofl/features/appd.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Subcall.roflEnsureAuthorizedOrigin(roflAppID);
110110
"data": {
111111
"gas_limit": 200000,
112112
"to": "1234845aaB7b6CD88c7fAd9E9E1cf07638805b20",
113-
"value": 0,
113+
"value": "0",
114114
"data": "dae1ee1f00000000000000000000000000000000000000000000000000002695a9e649b2"
115115
}
116116
}
@@ -123,7 +123,9 @@ Subcall.roflEnsureAuthorizedOrigin(roflAppID);
123123
supported (as defined by the `kind` field):
124124

125125
- Ethereum-compatible calls (`eth`) use standard fields (`gas_limit`, `to`,
126-
`value` and `data`) to define the transaction content.
126+
`value` and `data`) to define the transaction content. `value` must be a
127+
decimal string (or `0x` hex string) representing a non-negative integer
128+
up to 256 bits.
127129

128130
- Oasis SDK calls (`std`) support CBOR-serialized hex-encoded `Transaction`s
129131
to be specified.

rofl-appd/src/routes/tx.rs

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,48 @@ pub enum Transaction {
5656
gas_limit: u64,
5757
#[serde_as(as = "serde_with::hex::Hex")]
5858
to: Vec<u8>,
59-
value: u128,
59+
value: TransactionValue,
6060
#[serde_as(as = "serde_with::hex::Hex")]
6161
data: Vec<u8>,
6262
},
6363
}
6464

65+
/// Value representation that accepts either a string (decimal or 0x hex) or a JSON number.
66+
#[derive(Clone, Debug, serde::Deserialize)]
67+
#[serde(untagged)]
68+
pub enum TransactionValue {
69+
String(String),
70+
Number(u128),
71+
}
72+
73+
impl TransactionValue {
74+
fn into_u256(self) -> Result<evm::types::U256, String> {
75+
match self {
76+
TransactionValue::Number(value) => Ok(value.into()),
77+
TransactionValue::String(value) => parse_u256_string(value),
78+
}
79+
}
80+
}
81+
82+
fn parse_u256_string(value: String) -> Result<evm::types::U256, String> {
83+
let trimmed = value.trim();
84+
if trimmed.is_empty() {
85+
return Err("transaction value string must not be empty".to_string());
86+
}
87+
let (radix, digits) = match trimmed
88+
.strip_prefix("0x")
89+
.or_else(|| trimmed.strip_prefix("0X"))
90+
{
91+
Some(rest) => (16, rest),
92+
None => (10, trimmed),
93+
};
94+
if digits.is_empty() {
95+
return Err("transaction value string must contain digits".to_string());
96+
}
97+
evm::types::U256::from_str_radix(digits, radix)
98+
.map_err(|_| "transaction value string is not a valid unsigned integer".to_string())
99+
}
100+
65101
/// Transaction signing and submission request.
66102
#[serde_as]
67103
#[derive(Clone, Debug, serde::Deserialize)]
@@ -115,12 +151,13 @@ pub async fn sign_and_submit(
115151
value,
116152
data,
117153
} => {
154+
let value = value.into_u256().map_err(|err| (Status::BadRequest, err))?;
118155
let (method, body) = if to.is_empty() {
119156
// Create.
120157
(
121158
"evm.Create",
122159
cbor::to_value(evm::types::Create {
123-
value: value.into(),
160+
value,
124161
init_code: data,
125162
}),
126163
)
@@ -135,7 +172,7 @@ pub async fn sign_and_submit(
135172
"evm.Call",
136173
cbor::to_value(evm::types::Call {
137174
address,
138-
value: value.into(),
175+
value,
139176
data,
140177
}),
141178
)
@@ -191,3 +228,43 @@ pub async fn sign_and_submit(
191228

192229
Ok(Json(response))
193230
}
231+
232+
#[cfg(test)]
233+
mod tests {
234+
use super::{evm, *};
235+
236+
#[test]
237+
fn parse_u256_string_supports_decimal_and_hex() {
238+
let decimal = parse_u256_string("42".to_string()).unwrap();
239+
assert_eq!(decimal, evm::types::U256::from(42u32));
240+
241+
let hex_lower = parse_u256_string("0x2a".to_string()).unwrap();
242+
assert_eq!(hex_lower, evm::types::U256::from(42u32));
243+
244+
let hex_upper = parse_u256_string(" 0X2A ".to_string()).unwrap();
245+
assert_eq!(hex_upper, evm::types::U256::from(42u32));
246+
}
247+
248+
#[test]
249+
fn parse_u256_string_rejects_invalid_inputs() {
250+
assert!(parse_u256_string("".to_string()).is_err());
251+
assert!(parse_u256_string("0x".to_string()).is_err());
252+
assert!(parse_u256_string("-1".to_string()).is_err());
253+
assert!(parse_u256_string("0xZZ".to_string()).is_err());
254+
}
255+
256+
#[test]
257+
fn transaction_value_into_u256_handles_number_variant() {
258+
let value = TransactionValue::Number(10u128.pow(18));
259+
assert_eq!(
260+
value.into_u256().unwrap(),
261+
evm::types::U256::from(10u128.pow(18))
262+
);
263+
}
264+
265+
#[test]
266+
fn transaction_value_into_u256_handles_string_variant() {
267+
let value = TransactionValue::String("1000".to_string());
268+
assert_eq!(value.into_u256().unwrap(), evm::types::U256::from(1000u32));
269+
}
270+
}

rofl-client/ts/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const callResultBytes = await client.signAndSubmit({
3030
kind: 'eth',
3131
gas_limit: 200_000,
3232
to: '', // empty => contract creation
33-
value: 0,
33+
value: '0', // decimal or 0x hex string
3434
data: '0x', // hex calldata (0x optional)
3535
});
3636
// `callResultBytes` is the raw CBOR-encoded CallResult (Uint8Array)
@@ -126,8 +126,11 @@ type EthTx = {
126126
gas_limit: number;
127127
/** Hex address (0x optional). Empty string => contract creation. */
128128
to: string;
129-
/** JSON number; must fit JS number range (backend expects u128). */
130-
value: number;
129+
/**
130+
* Transaction value in wei. Accepts decimal strings, 0x-prefixed hex strings,
131+
* bigint, or safe JS integers. Values are normalized to decimal strings.
132+
*/
133+
value: string | number | bigint;
131134
/** Hex calldata (0x optional). */
132135
data: string;
133136
};

rofl-client/ts/examples/basic_usage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ async function main() {
3232
kind: 'eth',
3333
gas_limit: 200_000,
3434
to: '', // empty => contract creation
35-
value: 0,
35+
value: '0',
3636
data: '0x', // no-op calldata
3737
});
3838
console.log('CallResult (hex):', Buffer.from(result).toString('hex'));

rofl-client/ts/jest.config.cjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module.exports = {
2+
testMatch: ['**/test/**/*.ts'],
3+
testEnvironment: 'node',
4+
transform: {
5+
'^.+\\.ts$': ['ts-jest', {tsconfig: 'tsconfig.test.json'}],
6+
},
7+
modulePathIgnorePatterns: ['<rootDir>/dist'],
8+
moduleNameMapper: {
9+
'^(\\.{1,2}/.*)\\.js$': '$1',
10+
},
11+
};

rofl-client/ts/jest.config.js

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

rofl-client/ts/package.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,19 @@
99
"url": "git+https://github.com/oasisprotocol/oasis-sdk.git",
1010
"directory": "rofl-client/ts"
1111
},
12+
"type": "module",
1213
"files": [
1314
"dist"
1415
],
15-
"main": "dist/index.js",
16-
"types": "dist/index.d.ts",
16+
"main": "./dist/cjs/index.js",
17+
"module": "./dist/esm/index.js",
18+
"types": "./dist/index.d.ts",
1719
"exports": {
1820
".": {
1921
"types": "./dist/index.d.ts",
20-
"import": "./dist/index.js",
21-
"require": "./dist/index.js",
22-
"default": "./dist/index.js"
22+
"import": "./dist/esm/index.js",
23+
"require": "./dist/cjs/index.js",
24+
"default": "./dist/esm/index.js"
2325
}
2426
},
2527
"sideEffects": false,
@@ -32,7 +34,7 @@
3234
"lint": "prettier --check src test examples",
3335
"test": "jest",
3436
"typedoc": "npx typedoc",
35-
"build": "tsc -p tsconfig.json"
37+
"build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node scripts/emit-cjs-pkg.cjs"
3638
},
3739
"engines": {
3840
"node": ">=18"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// scripts/emit-cjs-pkg.cjs
2+
const fs = require('node:fs');
3+
const path = require('node:path');
4+
5+
const dir = path.join(__dirname, '..', 'dist', 'cjs');
6+
fs.mkdirSync(dir, {recursive: true});
7+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({type: 'commonjs'}, null, 2));

rofl-client/ts/src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
export {RoflClient, type RoflClientOptions, ROFL_SOCKET_PATH, KeyKind} from './roflClient';
1+
export {RoflClient, type RoflClientOptions, ROFL_SOCKET_PATH, KeyKind} from './roflClient.js';
22
export type {
33
Transport,
44
TransportRequest,
55
TransportResponse,
66
StdTx,
77
EthTx,
8+
EthValue,
89
QueryArgsInput,
9-
} from './roflClient';
10+
} from './roflClient.js';

0 commit comments

Comments
 (0)