Skip to content

Commit ca7987b

Browse files
authored
Merge pull request #2393 from oasisprotocol/uniyalabhishek/rofl-client-ts/queries-endpoint
feat: update rofl-client ts to include queries endpoint
2 parents 777bcc4 + 8c6561c commit ca7987b

File tree

16 files changed

+744
-64
lines changed

16 files changed

+744
-64
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: 44 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)
@@ -57,6 +57,11 @@ const callResultBytes = await client.signAndSubmit({
5757
- `getMetadata(): Promise<Record<string, string>>`
5858
- `setMetadata(metadata: Record<string, string>): Promise<void>`
5959
- `getAppId(): Promise<string>` (helper)
60+
- `query<TArgs = void, TResult = unknown>(method: string,
61+
args?: QueryArgsInput<TArgs>): Promise<TResult)`
62+
([`QueryArgsInput`](#queryargsinput))
63+
- Encodes `args` as CBOR (or uses provided binary CBOR), POSTs to
64+
`/rofl/v1/query`, and decodes the CBOR response body into `TResult`.
6065
- `signAndSubmit(tx: StdTx | EthTx, opts?: { encrypt?: boolean }):
6166
Promise<Uint8Array>`
6267
- Signs the transaction with an app-authenticated key, submits it,
@@ -65,6 +70,39 @@ const callResultBytes = await client.signAndSubmit({
6570

6671
[`CallResult`]: https://api.docs.oasis.io/rust/oasis_runtime_sdk/types/transaction/enum.CallResult.html
6772

73+
### Runtime Queries
74+
75+
`query` lets you execute read-only runtime methods exposed by ROFL-compatible
76+
paratimes. Use the generated types from `@oasisprotocol/client-rt` for complete
77+
type safety:
78+
79+
```ts
80+
import {rofl, types} from '@oasisprotocol/client-rt';
81+
import {RoflClient} from '@oasisprotocol/rofl-client';
82+
83+
const client = new RoflClient();
84+
const appConfig = await client.query<types.RoflAppQuery, types.RoflAppConfig>(
85+
rofl.METHOD_APP,
86+
{ id: myAppId }
87+
);
88+
```
89+
90+
If you already have CBOR-encoded arguments (e.g., from `oasis.misc.toCBOR`),
91+
pass them directly as `Uint8Array`, `Buffer`, `ArrayBuffer`, or any
92+
`ArrayBufferView` via `QueryArgsInput`.
93+
94+
### QueryArgsInput
95+
96+
`QueryArgsInput<TArgs>` mirrors the runtime schema expected by your query method:
97+
98+
- When `TArgs` is anything other than `void`, you must pass either structured arguments (`TArgs`)
99+
or pre-encoded CBOR bytes (`Uint8Array`, `Buffer`, `ArrayBuffer`, or any `ArrayBufferView`).
100+
- When `TArgs` is omitted or `void`, the `args` parameter becomes optional; omit it to send `null`
101+
or pass CBOR bytes directly.
102+
103+
This keeps the `query` call site type-safe—TypeScript now enforces that calls providing structured
104+
types also supply the corresponding payload.
105+
68106
### KeyKind
69107

70108
Supported key generation types (serialized as stable strings):
@@ -88,8 +126,11 @@ type EthTx = {
88126
gas_limit: number;
89127
/** Hex address (0x optional). Empty string => contract creation. */
90128
to: string;
91-
/** JSON number; must fit JS number range (backend expects u128). */
92-
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;
93134
/** Hex calldata (0x optional). */
94135
data: string;
95136
};

rofl-client/ts/examples/basic_usage.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,16 @@ async function main() {
2323
const appId = await client.getAppId();
2424
console.log('App ID:', appId);
2525

26+
// Read-only runtime query (no args -> CBOR null)
27+
const runtimeInfo = await client.query('core.RuntimeInfo');
28+
console.log('Runtime info:', runtimeInfo);
29+
2630
// Sign & submit (ETH style)
2731
const result = await client.signAndSubmit({
2832
kind: 'eth',
2933
gas_limit: 200_000,
3034
to: '', // empty => contract creation
31-
value: 0,
35+
value: '0',
3236
data: '0x', // no-op calldata
3337
});
3438
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.

0 commit comments

Comments
 (0)