Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions crates/perry-codegen/src/lower_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use anyhow::{bail, Result};
use perry_hir::Expr;
use perry_types::Type as HirType;

use crate::expr::{lower_expr, nanbox_pointer_inline, nanbox_string_inline, unbox_to_i64, variant_name, FnCtx};
use crate::expr::{lower_expr, nanbox_bigint_inline, nanbox_pointer_inline, nanbox_string_inline, unbox_to_i64, variant_name, FnCtx};
use crate::lower_array_method::lower_array_method;

// Tier 1.3 (v0.5.332): the perry/ui, perry/ui-instance, perry/system,
Expand Down Expand Up @@ -3652,6 +3652,9 @@ enum NativeRetKind {
/// caller (and `JSON.stringify`, string-comparison, etc.) needs the
/// STRING_TAG to recognize it as a string rather than a heap object.
Str,
/// Returns `*mut BigIntHeader` → NaN-box as BIGINT (0x7FFA tag). Use
/// for functions like `parseEther`/`parseUnits` that return bigint values.
BigInt,
/// Returns f64 → pass through (NaN-boxed JSValue).
F64,
/// Returns i32 → ignored, return TAG_UNDEFINED.
Expand Down Expand Up @@ -3682,6 +3685,7 @@ const NA_PTR: NativeArgKind = NativeArgKind::PtrI64;
const NA_JSV: NativeArgKind = NativeArgKind::JsvalI64;
const NR_PTR: NativeRetKind = NativeRetKind::Ptr;
const NR_STR: NativeRetKind = NativeRetKind::Str;
const NR_BIGINT: NativeRetKind = NativeRetKind::BigInt;
const NR_F64: NativeRetKind = NativeRetKind::F64;
const NR_I32: NativeRetKind = NativeRetKind::I32Void;
const NR_VOID: NativeRetKind = NativeRetKind::Void;
Expand Down Expand Up @@ -4629,6 +4633,24 @@ const NATIVE_MODULE_TABLE: &[NativeModSig] = &[
class_filter: None, runtime: "js_worker_threads_parent_port", args: &[], ret: NR_F64 },
NativeModSig { module: "worker_threads", has_receiver: true, method: "postMessage",
class_filter: None, runtime: "js_worker_threads_post_message", args: &[NA_F64], ret: NR_F64 },

// ========== ethers ==========
// Utility functions (receiver-less, no class filter).
NativeModSig { module: "ethers", has_receiver: false, method: "getAddress",
class_filter: None, runtime: "js_ethers_get_address", args: &[NA_STR], ret: NR_STR },
NativeModSig { module: "ethers", has_receiver: false, method: "formatEther",
class_filter: None, runtime: "js_ethers_format_ether", args: &[NA_PTR], ret: NR_STR },
NativeModSig { module: "ethers", has_receiver: false, method: "formatUnits",
class_filter: None, runtime: "js_ethers_format_units", args: &[NA_PTR, NA_F64], ret: NR_STR },
NativeModSig { module: "ethers", has_receiver: false, method: "parseEther",
class_filter: None, runtime: "js_ethers_parse_ether", args: &[NA_STR], ret: NR_BIGINT },
NativeModSig { module: "ethers", has_receiver: false, method: "parseUnits",
class_filter: None, runtime: "js_ethers_parse_units", args: &[NA_STR, NA_F64], ret: NR_BIGINT },
// Wallet.createRandom() — static method on the Wallet class.
// class_filter matches `Wallet` so `ethers.Wallet.createRandom()` in HIR
// (which lowers to class_name="Wallet", method="createRandom") resolves here.
NativeModSig { module: "ethers", has_receiver: false, method: "createRandom",
class_filter: Some("Wallet"), runtime: "js_ethers_wallet_create_random", args: &[], ret: NR_PTR },
];

/// Walk a statement to collect LocalIds declared inside a closure body —
Expand Down Expand Up @@ -4879,7 +4901,7 @@ pub(super) fn lower_native_module_dispatch(

// Determine return type for the declare
let ret_type = match sig.ret {
NativeRetKind::Ptr | NativeRetKind::Str => I64,
NativeRetKind::Ptr | NativeRetKind::Str | NativeRetKind::BigInt => I64,
NativeRetKind::F64 => DOUBLE,
NativeRetKind::I32Void => I32,
NativeRetKind::Void => crate::types::VOID,
Expand Down Expand Up @@ -4909,6 +4931,12 @@ pub(super) fn lower_native_module_dispatch(
let null_val = double_literal(f64::from_bits(crate::nanbox::TAG_NULL));
Ok(blk.select(crate::types::I1, &is_null, DOUBLE, &null_val, &boxed))
}
NativeRetKind::BigInt => {
// Returned raw *mut BigIntHeader — NaN-box with BIGINT_TAG (0x7FFA).
let blk = ctx.block();
let raw = blk.call(I64, sig.runtime, &arg_slices);
Ok(nanbox_bigint_inline(blk, &raw))
}
NativeRetKind::F64 => {
Ok(ctx.block().call(DOUBLE, sig.runtime, &arg_slices))
}
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-dispatch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,10 @@ pub static PERRY_SYSTEM_TABLE: &[MethodRow] = &[
args: &[ArgKind::F64], ret: ReturnKind::F64 },
MethodRow { method: "getDeviceModel", runtime: "perry_system_get_device_model",
args: &[], ret: ReturnKind::F64 },
MethodRow { method: "getLocale", runtime: "perry_system_get_locale",
args: &[], ret: ReturnKind::Str },
MethodRow { method: "getAppIcon", runtime: "perry_system_get_app_icon",
args: &[ArgKind::Str], ret: ReturnKind::Widget },
];
pub static PERRY_I18N_TABLE: &[MethodRow] = &[
MethodRow { method: "Currency", runtime: "perry_i18n_format_currency_default",
Expand Down
24 changes: 24 additions & 0 deletions crates/perry-hir/src/lower/expr_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,30 @@ pub(super) fn lower_call(ctx: &mut LoweringContext, call: &ast::CallExpr) -> Res
}
}

// Check for module.Class.staticMethod() pattern (e.g., ethers.Wallet.createRandom())
if let ast::Expr::Member(outer_member) = expr.as_ref() {
if let ast::Expr::Member(inner_member) = outer_member.obj.as_ref() {
if let ast::Expr::Ident(mod_ident) = inner_member.obj.as_ref() {
let mod_name = mod_ident.sym.to_string();
if let Some((module_name, _)) = ctx.lookup_native_module(&mod_name) {
if let ast::MemberProp::Ident(class_ident) = &inner_member.prop {
let class_name = class_ident.sym.to_string();
if let ast::MemberProp::Ident(method_ident) = &outer_member.prop {
let method_name = method_ident.sym.to_string();
return Ok(Expr::NativeMethodCall {
module: module_name.to_string(),
class_name: Some(class_name),
object: None,
method: method_name,
args,
});
}
}
}
}
}
}

// Check for native module method calls (e.g., mysql.createConnection())
if let ast::Expr::Member(member) = expr.as_ref() {
if let ast::Expr::Ident(obj_ident) = member.obj.as_ref() {
Expand Down
57 changes: 56 additions & 1 deletion crates/perry-stdlib/src/ethers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
//!
//! Provides formatUnits, parseUnits, parseEther, formatEther, getAddress, and other ethers utilities.

use perry_runtime::{js_string_from_bytes, js_bigint_from_string, BigIntHeader, StringHeader};
use perry_runtime::{
js_string_from_bytes, js_bigint_from_string, js_object_alloc, js_object_set_field_by_name,
BigIntHeader, StringHeader, ObjectHeader,
};

/// getAddress(address: string) -> string
/// Returns the checksummed address (EIP-55 format).
Expand All @@ -29,6 +32,58 @@ pub extern "C" fn js_ethers_get_address(str_ptr: *const StringHeader) -> *mut St
}
}

/// Wallet.createRandom() -> { address: string, privateKey: string }
/// Generates a random Ethereum wallet with a cryptographically random private key.
/// The address is derived as the last 20 bytes of keccak256(private_key_bytes),
/// formatted with EIP-55 checksum encoding.
#[no_mangle]
pub extern "C" fn js_ethers_wallet_create_random() -> *mut ObjectHeader {
use rand::Rng;
let pk_bytes: [u8; 32] = rand::thread_rng().gen();

// Derive address: keccak256(private_key_bytes)[12..32] = 20-byte address
let hash = keccak256(&pk_bytes);
let addr_bytes = &hash[12..32];

let hex_chars = b"0123456789abcdef";

// Format private key as "0x" + 64 hex chars
let mut pk_hex = Vec::<u8>::with_capacity(66);
pk_hex.extend_from_slice(b"0x");
for &b in &pk_bytes {
pk_hex.push(hex_chars[(b >> 4) as usize]);
pk_hex.push(hex_chars[(b & 0x0f) as usize]);
}

// Build lowercase address hex then apply EIP-55 checksum
let mut addr_lower = String::with_capacity(40);
for &b in addr_bytes {
addr_lower.push(hex_chars[(b >> 4) as usize] as char);
addr_lower.push(hex_chars[(b & 0x0f) as usize] as char);
}
let addr_checksummed = to_checksum_address(&addr_lower);

// NaN-box a StringHeader pointer with STRING_TAG
const STRING_TAG: u64 = 0x7FFF_0000_0000_0000;
let nanbox_str = |ptr: *mut StringHeader| -> f64 {
f64::from_bits(STRING_TAG | (ptr as u64 & 0x0000_FFFF_FFFF_FFFF))
};

unsafe {
let obj = js_object_alloc(0, 2);

let key_addr_str = js_string_from_bytes(b"address".as_ptr(), 7);
let val_addr_str = js_string_from_bytes(addr_checksummed.as_ptr(), addr_checksummed.len() as u32);
js_object_set_field_by_name(obj, key_addr_str, nanbox_str(val_addr_str));

let key_pk_str = js_string_from_bytes(b"privateKey".as_ptr(), 10);
let val_pk_str = js_string_from_bytes(pk_hex.as_ptr(), pk_hex.len() as u32);
js_object_set_field_by_name(obj, key_pk_str, nanbox_str(val_pk_str));

obj
}
}

/// parseEther(value: string) -> bigint
/// Parses a string representing ether to a BigInt in wei (18 decimals).
/// Example: parseEther("1.5") -> 1500000000000000000n
Expand Down
20 changes: 19 additions & 1 deletion docs/examples/stdlib/crypto/snippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,24 @@ function cryptoExample(): void {
}
// ANCHOR_END: crypto-node

// ANCHOR: ethers
import { ethers } from "ethers"

function ethersExample(): void {
// Utility functions
const addr = ethers.getAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
const wei = ethers.parseEther("1.5")
const ether = ethers.formatEther(wei)
console.log(`checksum: ${addr}`)
console.log(`1.5 ether in wei → formatted back: ${ether}`)

// Create a random wallet
const wallet = ethers.Wallet.createRandom()
console.log(`address: ${wallet.address}`)
console.log(`privateKey length: ${wallet.privateKey.length}`)
}
// ANCHOR_END: ethers

// Reference everything so unused-import elimination doesn't strip the imports.
const _keep = [bcryptExample, argon2Example, jwtExample, cryptoExample]
const _keep = [bcryptExample, argon2Example, jwtExample, cryptoExample, ethersExample]
console.log(`crypto-snippets: ${_keep.length}`)
13 changes: 13 additions & 0 deletions docs/examples/system/snippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { App, VStack, Text } from "perry/ui"
import {
isDarkMode,
getDeviceModel, getDeviceIdiom,
getLocale, getAppIcon,
openURL,
keychainSave, keychainGet, keychainDelete,
preferencesGet, preferencesSet,
Expand Down Expand Up @@ -47,6 +48,18 @@ console.log(`device idiom: ${getDeviceIdiom()}`)
console.log(`device model: ${getDeviceModel()}`)
// ANCHOR_END: device

// ANCHOR: locale
const locale = getLocale()
console.log(`locale: ${locale}`)
// ANCHOR_END: locale

// ANCHOR: app-icon
// Returns a Widget handle for the app icon image.
// Pass "" to use the default bundle icon; pass a specific asset path on disk otherwise.
const iconWidget = getAppIcon("")
console.log(`icon widget handle: ${iconWidget}`)
// ANCHOR_END: app-icon

// ANCHOR: open-url
openURL("https://example.com")
// ANCHOR_END: open-url
Expand Down
16 changes: 2 additions & 14 deletions docs/src/stdlib/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,8 @@ Perry natively implements password hashing, JWT tokens, and Ethereum cryptograph

## Ethers

The `ethers` runtime exposes utility functions (`formatEther`, `formatUnits`,
`parseEther`, `parseUnits`, `getAddress`) but the higher-level
`Wallet.createRandom()` constructor flow shown below is not yet wired into
the LLVM backend. Track the follow-up at issue #199.

```text
import { ethers } from "ethers";

// Create a wallet
const wallet = ethers.Wallet.createRandom();
console.log(wallet.address);

// Sign a message
const signature = await wallet.signMessage("Hello, Ethereum!");
```typescript
{{#include ../../examples/stdlib/crypto/snippets.ts:ethers}}
```

## Next Steps
Expand Down
12 changes: 12 additions & 0 deletions types/perry/system/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ export function getDeviceIdiom(): string;
/** Returns the device model identifier (e.g. "iPhone13,4"). */
export function getDeviceModel(): string;

/** Returns the BCP 47 locale tag for the device's primary language (e.g. "en-US", "fr-FR"). */
export function getLocale(): string;

/**
* Returns a Widget handle rendering the application's icon at the given path,
* or 0 on platforms where app-icon retrieval is not supported.
*
* On macOS/iOS the path is the `.icns` / `.png` asset path inside the app bundle.
* Pass an empty string `""` to use the app bundle's default icon.
*/
export function getAppIcon(path: string): import("perry/ui").Widget;

// ---------------------------------------------------------------------------
// URL
// ---------------------------------------------------------------------------
Expand Down