diff --git a/Cargo.toml b/Cargo.toml
index d40d5cdfe..e9c34a07d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,8 +26,11 @@ test-strategy = "0.4.0"
thiserror = "2.0"
tokio = "1.40.0"
-iota-crypto = { version = "0.0.1-alpha.1", package = "iota-sdk-crypto", path = "crates/iota-sdk-crypto", default-features = false }
-iota-graphql-client = { version = "0.0.1-alpha.1", package = "iota-sdk-graphql-client", path = "crates/iota-sdk-graphql-client", default-features = false }
-iota-graphql-client-build = { version = "0.0.1-alpha.1", package = "iota-sdk-graphql-client-build", path = "crates/iota-sdk-graphql-client-build", default-features = false }
-iota-transaction-builder = { version = "0.0.1-alpha.1", package = "iota-sdk-transaction-builder", path = "crates/iota-sdk-transaction-builder", default-features = false }
-iota-types = { version = "0.0.1-alpha.1", package = "iota-sdk-types", path = "crates/iota-sdk-types", default-features = false }
+iota-ledger = { package = "iota-ledger", path = "crates/iota-ledger", default-features = false }
+iota-ledger-signer = { package = "iota-ledger-signer", path = "crates/iota-ledger-signer", default-features = false }
+iota-sdk = { package = "iota-sdk", path = "crates/iota-sdk", default-features = false }
+iota-crypto = { package = "iota-sdk-crypto", path = "crates/iota-sdk-crypto", default-features = false }
+iota-graphql-client = { package = "iota-sdk-graphql-client", path = "crates/iota-sdk-graphql-client", default-features = false }
+iota-graphql-client-build = { package = "iota-sdk-graphql-client-build", path = "crates/iota-sdk-graphql-client-build", default-features = false }
+iota-transaction-builder = { package = "iota-sdk-transaction-builder", path = "crates/iota-sdk-transaction-builder", default-features = false }
+iota-types = { package = "iota-sdk-types", path = "crates/iota-sdk-types", default-features = false }
diff --git a/bindings/go/examples/ledger/main.go b/bindings/go/examples/ledger/main.go
new file mode 100644
index 000000000..bd78ab864
--- /dev/null
+++ b/bindings/go/examples/ledger/main.go
@@ -0,0 +1,65 @@
+// Copyright (c) 2025 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/iotaledger/iota-rust-sdk/bindings/go/iota_sdk"
+)
+
+type AsyncSigner struct {
+ ledger *iota_sdk.LedgerSigner
+}
+
+func (signer *AsyncSigner) Sign(transaction *iota_sdk.Transaction) (iota_sdk.TransactionSignerFnOutput, error) {
+ fmt.Println("BEFORE")
+ signature, err := signer.ledger.SignTransaction(transaction)
+ fmt.Println("AFTER")
+ return iota_sdk.TransactionSignerFnOutput{Signature: signature}, err
+}
+
+func main() {
+ ledger, err := iota_sdk.LedgerSignerNewWithDefault("m/44'/4218'/0'/1'/0'")
+
+ if err != nil {
+ log.Fatalf("Failed to create ledger: %v", err)
+ }
+
+ address, err := ledger.GetAddress()
+
+ if err != nil {
+ log.Fatalf("Failed to get address: %v", err)
+ }
+
+ fmt.Println("Address:", address.ToHex())
+
+ // Request funds from faucet
+ faucet := iota_sdk.FaucetClientNewLocalnet()
+ _, err = faucet.RequestAndWait(address)
+ if err.(*iota_sdk.SdkFfiError) != nil {
+ log.Fatalf("Failed to request faucet: %v", err)
+ }
+
+ client := iota_sdk.GraphQlClientNewLocalnet()
+
+ recipientAddress, err := iota_sdk.AddressFromHex("0x0000a4984bd495d4346fa208ddff4f5d5e5ad48c21dec631ddebc99809f16900")
+ if err != nil {
+ log.Fatalf("Failed to parse recipient address: %v", err)
+ }
+
+ builder := iota_sdk.NewTransactionBuilder(address).WithClient(client)
+ builder.SendIota(recipientAddress, iota_sdk.PtbArgumentU64(1000))
+
+ signer := iota_sdk.NewTransactionSigner(&AsyncSigner{ledger: ledger})
+ waitFor := iota_sdk.WaitForTxFinalized
+ effects, err := builder.Execute(signer, &waitFor)
+ if err.(*iota_sdk.SdkFfiError) != nil {
+ log.Fatalf("Failed to execute: %v", err)
+ }
+ log.Printf("Digest: %s", iota_sdk.HexEncode((*effects).Digest().ToBytes()))
+ log.Printf("Transaction status: %v", (*effects).AsV1().Status)
+ log.Printf("Effects: %+v", (*effects).AsV1())
+}
diff --git a/bindings/go/iota_sdk/iota_sdk.go b/bindings/go/iota_sdk/iota_sdk.go
index 20d9d07f7..095cfc894 100644
--- a/bindings/go/iota_sdk/iota_sdk.go
+++ b/bindings/go/iota_sdk/iota_sdk.go
@@ -4169,6 +4169,33 @@ func uniffiCheckChecksums() {
}
}
{
+ checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t {
+ return C.uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_address()
+ })
+ if checksum != 5173 {
+ // If this happens try cleaning and rebuilding your project
+ panic("iota_sdk_ffi: uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_address: UniFFI API checksum mismatch")
+ }
+ }
+ {
+ checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t {
+ return C.uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_public_key()
+ })
+ if checksum != 26320 {
+ // If this happens try cleaning and rebuilding your project
+ panic("iota_sdk_ffi: uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_public_key: UniFFI API checksum mismatch")
+ }
+ }
+ {
+ checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t {
+ return C.uniffi_iota_sdk_ffi_checksum_method_ledgersigner_sign_transaction()
+ })
+ if checksum != 39180 {
+ // If this happens try cleaning and rebuilding your project
+ panic("iota_sdk_ffi: uniffi_iota_sdk_ffi_checksum_method_ledgersigner_sign_transaction: UniFFI API checksum mismatch")
+ }
+ }
+ {
checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t {
return C.uniffi_iota_sdk_ffi_checksum_method_makemovevector_elements()
})
@@ -8147,6 +8174,15 @@ func uniffiCheckChecksums() {
}
}
{
+ checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t {
+ return C.uniffi_iota_sdk_ffi_checksum_constructor_ledgersigner_new_with_default()
+ })
+ if checksum != 16447 {
+ // If this happens try cleaning and rebuilding your project
+ panic("iota_sdk_ffi: uniffi_iota_sdk_ffi_checksum_constructor_ledgersigner_new_with_default: UniFFI API checksum mismatch")
+ }
+ }
+ {
checksum := rustCall(func(_uniffiStatus *C.RustCallStatus) C.uint16_t {
return C.uniffi_iota_sdk_ffi_checksum_constructor_makemovevector_new()
})
@@ -18697,6 +18733,212 @@ func (_ FfiDestroyerInput) Destroy(value *Input) {
+type LedgerSignerInterface interface {
+ GetAddress() (*Address, error)
+ GetPublicKey() (*Ed25519PublicKey, error)
+ SignTransaction(transaction *Transaction) (*UserSignature, error)
+}
+type LedgerSigner struct {
+ ffiObject FfiObject
+}
+
+
+func LedgerSignerNewWithDefault(path string) (*LedgerSigner, error) {
+ _uniffiRV, _uniffiErr := rustCallWithError[LedgerSignerError](FfiConverterLedgerSignerError{},func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer {
+ return C.uniffi_iota_sdk_ffi_fn_constructor_ledgersigner_new_with_default(FfiConverterStringINSTANCE.Lower(path),_uniffiStatus)
+ })
+ if _uniffiErr != nil {
+ var _uniffiDefaultValue *LedgerSigner
+ return _uniffiDefaultValue, _uniffiErr
+ } else {
+ return FfiConverterLedgerSignerINSTANCE.Lift(_uniffiRV), nil
+ }
+}
+
+
+
+func (_self *LedgerSigner) GetAddress() (*Address, error) {
+ _pointer := _self.ffiObject.incrementPointer("*LedgerSigner")
+ defer _self.ffiObject.decrementPointer()
+ _uniffiRV, _uniffiErr := rustCallWithError[LedgerSignerError](FfiConverterLedgerSignerError{},func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer {
+ return C.uniffi_iota_sdk_ffi_fn_method_ledgersigner_get_address(
+ _pointer,_uniffiStatus)
+ })
+ if _uniffiErr != nil {
+ var _uniffiDefaultValue *Address
+ return _uniffiDefaultValue, _uniffiErr
+ } else {
+ return FfiConverterAddressINSTANCE.Lift(_uniffiRV), nil
+ }
+}
+
+func (_self *LedgerSigner) GetPublicKey() (*Ed25519PublicKey, error) {
+ _pointer := _self.ffiObject.incrementPointer("*LedgerSigner")
+ defer _self.ffiObject.decrementPointer()
+ _uniffiRV, _uniffiErr := rustCallWithError[LedgerSignerError](FfiConverterLedgerSignerError{},func(_uniffiStatus *C.RustCallStatus) unsafe.Pointer {
+ return C.uniffi_iota_sdk_ffi_fn_method_ledgersigner_get_public_key(
+ _pointer,_uniffiStatus)
+ })
+ if _uniffiErr != nil {
+ var _uniffiDefaultValue *Ed25519PublicKey
+ return _uniffiDefaultValue, _uniffiErr
+ } else {
+ return FfiConverterEd25519PublicKeyINSTANCE.Lift(_uniffiRV), nil
+ }
+}
+
+func (_self *LedgerSigner) SignTransaction(transaction *Transaction) (*UserSignature, error) {
+ _pointer := _self.ffiObject.incrementPointer("*LedgerSigner")
+ defer _self.ffiObject.decrementPointer()
+ res, err :=uniffiRustCallAsync[LedgerSignerError](
+ FfiConverterLedgerSignerErrorINSTANCE,
+ // completeFn
+ func(handle C.uint64_t, status *C.RustCallStatus) unsafe.Pointer {
+ res := C.ffi_iota_sdk_ffi_rust_future_complete_pointer(handle, status)
+ return res
+ },
+ // liftFn
+ func(ffi unsafe.Pointer) *UserSignature {
+ return FfiConverterUserSignatureINSTANCE.Lift(ffi)
+ },
+ C.uniffi_iota_sdk_ffi_fn_method_ledgersigner_sign_transaction(
+ _pointer,FfiConverterTransactionINSTANCE.Lower(transaction)),
+ // pollFn
+ func (handle C.uint64_t, continuation C.UniffiRustFutureContinuationCallback, data C.uint64_t) {
+ C.ffi_iota_sdk_ffi_rust_future_poll_pointer(handle, continuation, data)
+ },
+ // freeFn
+ func (handle C.uint64_t) {
+ C.ffi_iota_sdk_ffi_rust_future_free_pointer(handle)
+ },
+ )
+
+ return res, err
+}
+func (object *LedgerSigner) Destroy() {
+ runtime.SetFinalizer(object, nil)
+ object.ffiObject.destroy()
+}
+
+type FfiConverterLedgerSigner struct {}
+
+var FfiConverterLedgerSignerINSTANCE = FfiConverterLedgerSigner{}
+
+
+func (c FfiConverterLedgerSigner) Lift(pointer unsafe.Pointer) *LedgerSigner {
+ result := &LedgerSigner {
+ newFfiObject(
+ pointer,
+ func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer {
+ return C.uniffi_iota_sdk_ffi_fn_clone_ledgersigner(pointer, status)
+ },
+ func(pointer unsafe.Pointer, status *C.RustCallStatus) {
+ C.uniffi_iota_sdk_ffi_fn_free_ledgersigner(pointer, status)
+ },
+ ),
+ }
+ runtime.SetFinalizer(result, (*LedgerSigner).Destroy)
+ return result
+}
+
+func (c FfiConverterLedgerSigner) Read(reader io.Reader) *LedgerSigner {
+ return c.Lift(unsafe.Pointer(uintptr(readUint64(reader))))
+}
+
+func (c FfiConverterLedgerSigner) Lower(value *LedgerSigner) unsafe.Pointer {
+ // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here,
+ // because the pointer will be decremented immediately after this function returns,
+ // and someone will be left holding onto a non-locked pointer.
+ pointer := value.ffiObject.incrementPointer("*LedgerSigner")
+ defer value.ffiObject.decrementPointer()
+ return pointer
+
+}
+
+func (c FfiConverterLedgerSigner) Write(writer io.Writer, value *LedgerSigner) {
+ writeUint64(writer, uint64(uintptr(c.Lower(value))))
+}
+
+type FfiDestroyerLedgerSigner struct {}
+
+func (_ FfiDestroyerLedgerSigner) Destroy(value *LedgerSigner) {
+ value.Destroy()
+}
+
+
+
+type LedgerSignerErrorInterface interface {
+}
+type LedgerSignerError struct {
+ ffiObject FfiObject
+}
+
+
+
+func (object *LedgerSignerError) Destroy() {
+ runtime.SetFinalizer(object, nil)
+ object.ffiObject.destroy()
+}
+
+type FfiConverterLedgerSignerError struct {}
+
+var FfiConverterLedgerSignerErrorINSTANCE = FfiConverterLedgerSignerError{}
+
+
+
+func (_self LedgerSignerError) Error() string {
+ return "LedgerSignerError"
+}
+
+func (_self *LedgerSignerError) AsError() error {
+ if _self == nil {
+ return nil
+ } else {
+ return _self
+ }
+}
+func (c FfiConverterLedgerSignerError) Lift(pointer unsafe.Pointer) *LedgerSignerError {
+ result := &LedgerSignerError {
+ newFfiObject(
+ pointer,
+ func(pointer unsafe.Pointer, status *C.RustCallStatus) unsafe.Pointer {
+ return C.uniffi_iota_sdk_ffi_fn_clone_ledgersignererror(pointer, status)
+ },
+ func(pointer unsafe.Pointer, status *C.RustCallStatus) {
+ C.uniffi_iota_sdk_ffi_fn_free_ledgersignererror(pointer, status)
+ },
+ ),
+ }
+ runtime.SetFinalizer(result, (*LedgerSignerError).Destroy)
+ return result
+}
+
+func (c FfiConverterLedgerSignerError) Read(reader io.Reader) *LedgerSignerError {
+ return c.Lift(unsafe.Pointer(uintptr(readUint64(reader))))
+}
+
+func (c FfiConverterLedgerSignerError) Lower(value *LedgerSignerError) unsafe.Pointer {
+ // TODO: this is bad - all synchronization from ObjectRuntime.go is discarded here,
+ // because the pointer will be decremented immediately after this function returns,
+ // and someone will be left holding onto a non-locked pointer.
+ pointer := value.ffiObject.incrementPointer("*LedgerSignerError")
+ defer value.ffiObject.decrementPointer()
+ return pointer
+
+}
+
+func (c FfiConverterLedgerSignerError) Write(writer io.Writer, value *LedgerSignerError) {
+ writeUint64(writer, uint64(uintptr(c.Lower(value))))
+}
+
+type FfiDestroyerLedgerSignerError struct {}
+
+func (_ FfiDestroyerLedgerSignerError) Destroy(value *LedgerSignerError) {
+ value.Destroy()
+}
+
+
+
// Command to build a move vector out of a set of individual elements
//
// # BCS
diff --git a/bindings/go/iota_sdk/iota_sdk.h b/bindings/go/iota_sdk/iota_sdk.h
index 514a82eef..29d43fc2a 100644
--- a/bindings/go/iota_sdk/iota_sdk.h
+++ b/bindings/go/iota_sdk/iota_sdk.h
@@ -2287,6 +2287,46 @@ void* uniffi_iota_sdk_ffi_fn_constructor_input_new_receiving(RustBuffer object_r
void* uniffi_iota_sdk_ffi_fn_constructor_input_new_shared(void* object_id, uint64_t initial_shared_version, int8_t mutable, RustCallStatus *out_status
);
#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_CLONE_LEDGERSIGNER
+#define UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_CLONE_LEDGERSIGNER
+void* uniffi_iota_sdk_ffi_fn_clone_ledgersigner(void* ptr, RustCallStatus *out_status
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_FREE_LEDGERSIGNER
+#define UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_FREE_LEDGERSIGNER
+void uniffi_iota_sdk_ffi_fn_free_ledgersigner(void* ptr, RustCallStatus *out_status
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_CONSTRUCTOR_LEDGERSIGNER_NEW_WITH_DEFAULT
+#define UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_CONSTRUCTOR_LEDGERSIGNER_NEW_WITH_DEFAULT
+void* uniffi_iota_sdk_ffi_fn_constructor_ledgersigner_new_with_default(RustBuffer path, RustCallStatus *out_status
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_METHOD_LEDGERSIGNER_GET_ADDRESS
+#define UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_METHOD_LEDGERSIGNER_GET_ADDRESS
+void* uniffi_iota_sdk_ffi_fn_method_ledgersigner_get_address(void* ptr, RustCallStatus *out_status
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_METHOD_LEDGERSIGNER_GET_PUBLIC_KEY
+#define UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_METHOD_LEDGERSIGNER_GET_PUBLIC_KEY
+void* uniffi_iota_sdk_ffi_fn_method_ledgersigner_get_public_key(void* ptr, RustCallStatus *out_status
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_METHOD_LEDGERSIGNER_SIGN_TRANSACTION
+#define UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_METHOD_LEDGERSIGNER_SIGN_TRANSACTION
+uint64_t uniffi_iota_sdk_ffi_fn_method_ledgersigner_sign_transaction(void* ptr, void* transaction
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_CLONE_LEDGERSIGNERERROR
+#define UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_CLONE_LEDGERSIGNERERROR
+void* uniffi_iota_sdk_ffi_fn_clone_ledgersignererror(void* ptr, RustCallStatus *out_status
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_FREE_LEDGERSIGNERERROR
+#define UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_FREE_LEDGERSIGNERERROR
+void uniffi_iota_sdk_ffi_fn_free_ledgersignererror(void* ptr, RustCallStatus *out_status
+);
+#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_CLONE_MAKEMOVEVECTOR
#define UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_FN_CLONE_MAKEMOVEVECTOR
void* uniffi_iota_sdk_ffi_fn_clone_makemovevector(void* ptr, RustCallStatus *out_status
@@ -9936,6 +9976,24 @@ uint16_t uniffi_iota_sdk_ffi_checksum_method_graphqlclient_wait_for_tx(void
#define UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_CHECKSUM_METHOD_IDENTIFIER_AS_STR
uint16_t uniffi_iota_sdk_ffi_checksum_method_identifier_as_str(void
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_CHECKSUM_METHOD_LEDGERSIGNER_GET_ADDRESS
+#define UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_CHECKSUM_METHOD_LEDGERSIGNER_GET_ADDRESS
+uint16_t uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_address(void
+
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_CHECKSUM_METHOD_LEDGERSIGNER_GET_PUBLIC_KEY
+#define UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_CHECKSUM_METHOD_LEDGERSIGNER_GET_PUBLIC_KEY
+uint16_t uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_public_key(void
+
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_CHECKSUM_METHOD_LEDGERSIGNER_SIGN_TRANSACTION
+#define UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_CHECKSUM_METHOD_LEDGERSIGNER_SIGN_TRANSACTION
+uint16_t uniffi_iota_sdk_ffi_checksum_method_ledgersigner_sign_transaction(void
+
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_CHECKSUM_METHOD_MAKEMOVEVECTOR_ELEMENTS
@@ -12588,6 +12646,12 @@ uint16_t uniffi_iota_sdk_ffi_checksum_constructor_input_new_receiving(void
#define UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_CHECKSUM_CONSTRUCTOR_INPUT_NEW_SHARED
uint16_t uniffi_iota_sdk_ffi_checksum_constructor_input_new_shared(void
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_CHECKSUM_CONSTRUCTOR_LEDGERSIGNER_NEW_WITH_DEFAULT
+#define UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_CHECKSUM_CONSTRUCTOR_LEDGERSIGNER_NEW_WITH_DEFAULT
+uint16_t uniffi_iota_sdk_ffi_checksum_constructor_ledgersigner_new_with_default(void
+
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IOTA_SDK_FFI_CHECKSUM_CONSTRUCTOR_MAKEMOVEVECTOR_NEW
diff --git a/bindings/kotlin/lib/iota_sdk/iota_sdk_ffi.kt b/bindings/kotlin/lib/iota_sdk/iota_sdk_ffi.kt
index 47bbf43a3..8855afd4e 100644
--- a/bindings/kotlin/lib/iota_sdk/iota_sdk_ffi.kt
+++ b/bindings/kotlin/lib/iota_sdk/iota_sdk_ffi.kt
@@ -3150,6 +3150,18 @@ internal open class UniffiVTableCallbackInterfaceTransactionSignerFn(
+
+
+
+
+
+
+
+
+
+
+
+
@@ -4024,6 +4036,12 @@ fun uniffi_iota_sdk_ffi_checksum_method_graphqlclient_wait_for_tx(
): Short
fun uniffi_iota_sdk_ffi_checksum_method_identifier_as_str(
): Short
+fun uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_address(
+): Short
+fun uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_public_key(
+): Short
+fun uniffi_iota_sdk_ffi_checksum_method_ledgersigner_sign_transaction(
+): Short
fun uniffi_iota_sdk_ffi_checksum_method_makemovevector_elements(
): Short
fun uniffi_iota_sdk_ffi_checksum_method_makemovevector_type_tag(
@@ -4908,6 +4926,8 @@ fun uniffi_iota_sdk_ffi_checksum_constructor_input_new_receiving(
): Short
fun uniffi_iota_sdk_ffi_checksum_constructor_input_new_shared(
): Short
+fun uniffi_iota_sdk_ffi_checksum_constructor_ledgersigner_new_with_default(
+): Short
fun uniffi_iota_sdk_ffi_checksum_constructor_makemovevector_new(
): Short
fun uniffi_iota_sdk_ffi_checksum_constructor_mergecoins_new(
@@ -6166,6 +6186,22 @@ fun uniffi_iota_sdk_ffi_fn_constructor_input_new_receiving(`objectRef`: RustBuff
): Pointer
fun uniffi_iota_sdk_ffi_fn_constructor_input_new_shared(`objectId`: Pointer,`initialSharedVersion`: Long,`mutable`: Byte,uniffi_out_err: UniffiRustCallStatus,
): Pointer
+fun uniffi_iota_sdk_ffi_fn_clone_ledgersigner(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus,
+): Pointer
+fun uniffi_iota_sdk_ffi_fn_free_ledgersigner(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus,
+): Unit
+fun uniffi_iota_sdk_ffi_fn_constructor_ledgersigner_new_with_default(`path`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus,
+): Pointer
+fun uniffi_iota_sdk_ffi_fn_method_ledgersigner_get_address(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus,
+): Pointer
+fun uniffi_iota_sdk_ffi_fn_method_ledgersigner_get_public_key(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus,
+): Pointer
+fun uniffi_iota_sdk_ffi_fn_method_ledgersigner_sign_transaction(`ptr`: Pointer,`transaction`: Pointer,
+): Long
+fun uniffi_iota_sdk_ffi_fn_clone_ledgersignererror(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus,
+): Pointer
+fun uniffi_iota_sdk_ffi_fn_free_ledgersignererror(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus,
+): Unit
fun uniffi_iota_sdk_ffi_fn_clone_makemovevector(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus,
): Pointer
fun uniffi_iota_sdk_ffi_fn_free_makemovevector(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus,
@@ -9473,6 +9509,15 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) {
if (lib.uniffi_iota_sdk_ffi_checksum_method_identifier_as_str() != 63815.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
+ if (lib.uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_address() != 5173.toShort()) {
+ throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
+ }
+ if (lib.uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_public_key() != 26320.toShort()) {
+ throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
+ }
+ if (lib.uniffi_iota_sdk_ffi_checksum_method_ledgersigner_sign_transaction() != 39180.toShort()) {
+ throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
+ }
if (lib.uniffi_iota_sdk_ffi_checksum_method_makemovevector_elements() != 20773.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
@@ -10799,6 +10844,9 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) {
if (lib.uniffi_iota_sdk_ffi_checksum_constructor_input_new_shared() != 61970.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
+ if (lib.uniffi_iota_sdk_ffi_checksum_constructor_ledgersigner_new_with_default() != 16447.toShort()) {
+ throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
+ }
if (lib.uniffi_iota_sdk_ffi_checksum_constructor_makemovevector_new() != 20934.toShort()) {
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
}
@@ -27434,6 +27482,525 @@ public object FfiConverterTypeInput: FfiConverter {
//
+public interface LedgerSignerInterface {
+
+ fun `getAddress`(): Address
+
+ fun `getPublicKey`(): Ed25519PublicKey
+
+ suspend fun `signTransaction`(`transaction`: Transaction): UserSignature
+
+ companion object
+}
+
+open class LedgerSigner: Disposable, AutoCloseable, LedgerSignerInterface
+{
+
+ constructor(pointer: Pointer) {
+ this.pointer = pointer
+ this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer))
+ }
+
+ /**
+ * This constructor can be used to instantiate a fake object. Only used for tests. Any
+ * attempt to actually use an object constructed this way will fail as there is no
+ * connected Rust object.
+ */
+ @Suppress("UNUSED_PARAMETER")
+ constructor(noPointer: NoPointer) {
+ this.pointer = null
+ this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer))
+ }
+
+ protected val pointer: Pointer?
+ protected val cleanable: UniffiCleaner.Cleanable
+
+ private val wasDestroyed = AtomicBoolean(false)
+ private val callCounter = AtomicLong(1)
+
+ override fun destroy() {
+ // Only allow a single call to this method.
+ // TODO: maybe we should log a warning if called more than once?
+ if (this.wasDestroyed.compareAndSet(false, true)) {
+ // This decrement always matches the initial count of 1 given at creation time.
+ if (this.callCounter.decrementAndGet() == 0L) {
+ cleanable.clean()
+ }
+ }
+ }
+
+ @Synchronized
+ override fun close() {
+ this.destroy()
+ }
+
+ internal inline fun callWithPointer(block: (ptr: Pointer) -> R): R {
+ // Check and increment the call counter, to keep the object alive.
+ // This needs a compare-and-set retry loop in case of concurrent updates.
+ do {
+ val c = this.callCounter.get()
+ if (c == 0L) {
+ throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed")
+ }
+ if (c == Long.MAX_VALUE) {
+ throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow")
+ }
+ } while (! this.callCounter.compareAndSet(c, c + 1L))
+ // Now we can safely do the method call without the pointer being freed concurrently.
+ try {
+ return block(this.uniffiClonePointer())
+ } finally {
+ // This decrement always matches the increment we performed above.
+ if (this.callCounter.decrementAndGet() == 0L) {
+ cleanable.clean()
+ }
+ }
+ }
+
+ // Use a static inner class instead of a closure so as not to accidentally
+ // capture `this` as part of the cleanable's action.
+ private class UniffiCleanAction(private val pointer: Pointer?) : Runnable {
+ override fun run() {
+ pointer?.let { ptr ->
+ uniffiRustCall { status ->
+ UniffiLib.INSTANCE.uniffi_iota_sdk_ffi_fn_free_ledgersigner(ptr, status)
+ }
+ }
+ }
+ }
+
+ fun uniffiClonePointer(): Pointer {
+ return uniffiRustCall() { status ->
+ UniffiLib.INSTANCE.uniffi_iota_sdk_ffi_fn_clone_ledgersigner(pointer!!, status)
+ }
+ }
+
+
+ @Throws(LedgerSignerException::class)override fun `getAddress`(): Address {
+ return FfiConverterTypeAddress.lift(
+ callWithPointer {
+ uniffiRustCallWithError(LedgerSignerException) { _status ->
+ UniffiLib.INSTANCE.uniffi_iota_sdk_ffi_fn_method_ledgersigner_get_address(
+ it, _status)
+}
+ }
+ )
+ }
+
+
+
+ @Throws(LedgerSignerException::class)override fun `getPublicKey`(): Ed25519PublicKey {
+ return FfiConverterTypeEd25519PublicKey.lift(
+ callWithPointer {
+ uniffiRustCallWithError(LedgerSignerException) { _status ->
+ UniffiLib.INSTANCE.uniffi_iota_sdk_ffi_fn_method_ledgersigner_get_public_key(
+ it, _status)
+}
+ }
+ )
+ }
+
+
+
+ @Throws(LedgerSignerException::class)
+ @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
+ override suspend fun `signTransaction`(`transaction`: Transaction) : UserSignature {
+ return uniffiRustCallAsync(
+ callWithPointer { thisPtr ->
+ UniffiLib.INSTANCE.uniffi_iota_sdk_ffi_fn_method_ledgersigner_sign_transaction(
+ thisPtr,
+ FfiConverterTypeTransaction.lower(`transaction`),
+ )
+ },
+ { future, callback, continuation -> UniffiLib.INSTANCE.ffi_iota_sdk_ffi_rust_future_poll_pointer(future, callback, continuation) },
+ { future, continuation -> UniffiLib.INSTANCE.ffi_iota_sdk_ffi_rust_future_complete_pointer(future, continuation) },
+ { future -> UniffiLib.INSTANCE.ffi_iota_sdk_ffi_rust_future_free_pointer(future) },
+ // lift function
+ { FfiConverterTypeUserSignature.lift(it) },
+ // Error FFI converter
+ LedgerSignerException.ErrorHandler,
+ )
+ }
+
+
+
+
+ companion object {
+
+ @Throws(LedgerSignerException::class) fun `newWithDefault`(`path`: kotlin.String): LedgerSigner {
+ return FfiConverterTypeLedgerSigner.lift(
+ uniffiRustCallWithError(LedgerSignerException) { _status ->
+ UniffiLib.INSTANCE.uniffi_iota_sdk_ffi_fn_constructor_ledgersigner_new_with_default(
+ FfiConverterString.lower(`path`),_status)
+}
+ )
+ }
+
+
+
+ }
+
+}
+
+/**
+ * @suppress
+ */
+public object FfiConverterTypeLedgerSigner: FfiConverter {
+
+ override fun lower(value: LedgerSigner): Pointer {
+ return value.uniffiClonePointer()
+ }
+
+ override fun lift(value: Pointer): LedgerSigner {
+ return LedgerSigner(value)
+ }
+
+ override fun read(buf: ByteBuffer): LedgerSigner {
+ // The Rust code always writes pointers as 8 bytes, and will
+ // fail to compile if they don't fit.
+ return lift(Pointer(buf.getLong()))
+ }
+
+ override fun allocationSize(value: LedgerSigner) = 8UL
+
+ override fun write(value: LedgerSigner, buf: ByteBuffer) {
+ // The Rust code always expects pointers written as 8 bytes,
+ // and will fail to compile if they don't fit.
+ buf.putLong(Pointer.nativeValue(lower(value)))
+ }
+}
+
+
+// This template implements a class for working with a Rust struct via a Pointer/Arc
+// to the live Rust struct on the other side of the FFI.
+//
+// Each instance implements core operations for working with the Rust `Arc` and the
+// Kotlin Pointer to work with the live Rust struct on the other side of the FFI.
+//
+// There's some subtlety here, because we have to be careful not to operate on a Rust
+// struct after it has been dropped, and because we must expose a public API for freeing
+// theq Kotlin wrapper object in lieu of reliable finalizers. The core requirements are:
+//
+// * Each instance holds an opaque pointer to the underlying Rust struct.
+// Method calls need to read this pointer from the object's state and pass it in to
+// the Rust FFI.
+//
+// * When an instance is no longer needed, its pointer should be passed to a
+// special destructor function provided by the Rust FFI, which will drop the
+// underlying Rust struct.
+//
+// * Given an instance, calling code is expected to call the special
+// `destroy` method in order to free it after use, either by calling it explicitly
+// or by using a higher-level helper like the `use` method. Failing to do so risks
+// leaking the underlying Rust struct.
+//
+// * We can't assume that calling code will do the right thing, and must be prepared
+// to handle Kotlin method calls executing concurrently with or even after a call to
+// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`.
+//
+// * We must never allow Rust code to operate on the underlying Rust struct after
+// the destructor has been called, and must never call the destructor more than once.
+// Doing so may trigger memory unsafety.
+//
+// * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner`
+// is implemented to call the destructor when the Kotlin object becomes unreachable.
+// This is done in a background thread. This is not a panacea, and client code should be aware that
+// 1. the thread may starve if some there are objects that have poorly performing
+// `drop` methods or do significant work in their `drop` methods.
+// 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`,
+// or `android = true` in the [`kotlin` section of the `uniffi.toml` file](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html).
+//
+// If we try to implement this with mutual exclusion on access to the pointer, there is the
+// possibility of a race between a method call and a concurrent call to `destroy`:
+//
+// * Thread A starts a method call, reads the value of the pointer, but is interrupted
+// before it can pass the pointer over the FFI to Rust.
+// * Thread B calls `destroy` and frees the underlying Rust struct.
+// * Thread A resumes, passing the already-read pointer value to Rust and triggering
+// a use-after-free.
+//
+// One possible solution would be to use a `ReadWriteLock`, with each method call taking
+// a read lock (and thus allowed to run concurrently) and the special `destroy` method
+// taking a write lock (and thus blocking on live method calls). However, we aim not to
+// generate methods with any hidden blocking semantics, and a `destroy` method that might
+// block if called incorrectly seems to meet that bar.
+//
+// So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track
+// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy`
+// has been called. These are updated according to the following rules:
+//
+// * The initial value of the counter is 1, indicating a live object with no in-flight calls.
+// The initial value for the flag is false.
+//
+// * At the start of each method call, we atomically check the counter.
+// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted.
+// If it is nonzero them we atomically increment it by 1 and proceed with the method call.
+//
+// * At the end of each method call, we atomically decrement and check the counter.
+// If it has reached zero then we destroy the underlying Rust struct.
+//
+// * When `destroy` is called, we atomically flip the flag from false to true.
+// If the flag was already true we silently fail.
+// Otherwise we atomically decrement and check the counter.
+// If it has reached zero then we destroy the underlying Rust struct.
+//
+// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works,
+// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`.
+//
+// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been
+// called *and* all in-flight method calls have completed, avoiding violating any of the expectations
+// of the underlying Rust code.
+//
+// This makes a cleaner a better alternative to _not_ calling `destroy()` as
+// and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop`
+// method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner
+// thread may be starved, and the app will leak memory.
+//
+// In this case, `destroy`ing manually may be a better solution.
+//
+// The cleaner can live side by side with the manual calling of `destroy`. In the order of responsiveness, uniffi objects
+// with Rust peers are reclaimed:
+//
+// 1. By calling the `destroy` method of the object, which calls `rustObject.free()`. If that doesn't happen:
+// 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then:
+// 3. The memory is reclaimed when the process terminates.
+//
+// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219
+//
+
+
+public interface LedgerSignerExceptionInterface {
+
+ companion object
+}
+
+
+open class LedgerSignerException : kotlin.Exception, Disposable, AutoCloseable, LedgerSignerExceptionInterface {
+
+
+ constructor(pointer: Pointer) {
+ this.pointer = pointer
+ this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer))
+ }
+
+ /**
+ * This constructor can be used to instantiate a fake object. Only used for tests. Any
+ * attempt to actually use an object constructed this way will fail as there is no
+ * connected Rust object.
+ */
+ @Suppress("UNUSED_PARAMETER")
+ constructor(noPointer: NoPointer) {
+ this.pointer = null
+ this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer))
+ }
+
+ protected val pointer: Pointer?
+ protected val cleanable: UniffiCleaner.Cleanable
+
+ private val wasDestroyed = AtomicBoolean(false)
+ private val callCounter = AtomicLong(1)
+
+ override fun destroy() {
+ // Only allow a single call to this method.
+ // TODO: maybe we should log a warning if called more than once?
+ if (this.wasDestroyed.compareAndSet(false, true)) {
+ // This decrement always matches the initial count of 1 given at creation time.
+ if (this.callCounter.decrementAndGet() == 0L) {
+ cleanable.clean()
+ }
+ }
+ }
+
+ @Synchronized
+ override fun close() {
+ this.destroy()
+ }
+
+ internal inline fun callWithPointer(block: (ptr: Pointer) -> R): R {
+ // Check and increment the call counter, to keep the object alive.
+ // This needs a compare-and-set retry loop in case of concurrent updates.
+ do {
+ val c = this.callCounter.get()
+ if (c == 0L) {
+ throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed")
+ }
+ if (c == Long.MAX_VALUE) {
+ throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow")
+ }
+ } while (! this.callCounter.compareAndSet(c, c + 1L))
+ // Now we can safely do the method call without the pointer being freed concurrently.
+ try {
+ return block(this.uniffiClonePointer())
+ } finally {
+ // This decrement always matches the increment we performed above.
+ if (this.callCounter.decrementAndGet() == 0L) {
+ cleanable.clean()
+ }
+ }
+ }
+
+ // Use a static inner class instead of a closure so as not to accidentally
+ // capture `this` as part of the cleanable's action.
+ private class UniffiCleanAction(private val pointer: Pointer?) : Runnable {
+ override fun run() {
+ pointer?.let { ptr ->
+ uniffiRustCall { status ->
+ UniffiLib.INSTANCE.uniffi_iota_sdk_ffi_fn_free_ledgersignererror(ptr, status)
+ }
+ }
+ }
+ }
+
+ fun uniffiClonePointer(): Pointer {
+ return uniffiRustCall() { status ->
+ UniffiLib.INSTANCE.uniffi_iota_sdk_ffi_fn_clone_ledgersignererror(pointer!!, status)
+ }
+ }
+
+
+
+
+
+ companion object ErrorHandler : UniffiRustCallStatusErrorHandler {
+ override fun lift(error_buf: RustBuffer.ByValue): LedgerSignerException {
+ // Due to some mismatches in the ffi converter mechanisms, errors are a RustBuffer.
+ val bb = error_buf.asByteBuffer()
+ if (bb == null) {
+ throw InternalException("?")
+ }
+ return FfiConverterTypeLedgerSignerError.read(bb)
+ }
+ }
+
+}
+
+/**
+ * @suppress
+ */
+public object FfiConverterTypeLedgerSignerError: FfiConverter {
+
+ override fun lower(value: LedgerSignerException): Pointer {
+ return value.uniffiClonePointer()
+ }
+
+ override fun lift(value: Pointer): LedgerSignerException {
+ return LedgerSignerException(value)
+ }
+
+ override fun read(buf: ByteBuffer): LedgerSignerException {
+ // The Rust code always writes pointers as 8 bytes, and will
+ // fail to compile if they don't fit.
+ return lift(Pointer(buf.getLong()))
+ }
+
+ override fun allocationSize(value: LedgerSignerException) = 8UL
+
+ override fun write(value: LedgerSignerException, buf: ByteBuffer) {
+ // The Rust code always expects pointers written as 8 bytes,
+ // and will fail to compile if they don't fit.
+ buf.putLong(Pointer.nativeValue(lower(value)))
+ }
+}
+
+
+// This template implements a class for working with a Rust struct via a Pointer/Arc
+// to the live Rust struct on the other side of the FFI.
+//
+// Each instance implements core operations for working with the Rust `Arc` and the
+// Kotlin Pointer to work with the live Rust struct on the other side of the FFI.
+//
+// There's some subtlety here, because we have to be careful not to operate on a Rust
+// struct after it has been dropped, and because we must expose a public API for freeing
+// theq Kotlin wrapper object in lieu of reliable finalizers. The core requirements are:
+//
+// * Each instance holds an opaque pointer to the underlying Rust struct.
+// Method calls need to read this pointer from the object's state and pass it in to
+// the Rust FFI.
+//
+// * When an instance is no longer needed, its pointer should be passed to a
+// special destructor function provided by the Rust FFI, which will drop the
+// underlying Rust struct.
+//
+// * Given an instance, calling code is expected to call the special
+// `destroy` method in order to free it after use, either by calling it explicitly
+// or by using a higher-level helper like the `use` method. Failing to do so risks
+// leaking the underlying Rust struct.
+//
+// * We can't assume that calling code will do the right thing, and must be prepared
+// to handle Kotlin method calls executing concurrently with or even after a call to
+// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`.
+//
+// * We must never allow Rust code to operate on the underlying Rust struct after
+// the destructor has been called, and must never call the destructor more than once.
+// Doing so may trigger memory unsafety.
+//
+// * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner`
+// is implemented to call the destructor when the Kotlin object becomes unreachable.
+// This is done in a background thread. This is not a panacea, and client code should be aware that
+// 1. the thread may starve if some there are objects that have poorly performing
+// `drop` methods or do significant work in their `drop` methods.
+// 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`,
+// or `android = true` in the [`kotlin` section of the `uniffi.toml` file](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html).
+//
+// If we try to implement this with mutual exclusion on access to the pointer, there is the
+// possibility of a race between a method call and a concurrent call to `destroy`:
+//
+// * Thread A starts a method call, reads the value of the pointer, but is interrupted
+// before it can pass the pointer over the FFI to Rust.
+// * Thread B calls `destroy` and frees the underlying Rust struct.
+// * Thread A resumes, passing the already-read pointer value to Rust and triggering
+// a use-after-free.
+//
+// One possible solution would be to use a `ReadWriteLock`, with each method call taking
+// a read lock (and thus allowed to run concurrently) and the special `destroy` method
+// taking a write lock (and thus blocking on live method calls). However, we aim not to
+// generate methods with any hidden blocking semantics, and a `destroy` method that might
+// block if called incorrectly seems to meet that bar.
+//
+// So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track
+// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy`
+// has been called. These are updated according to the following rules:
+//
+// * The initial value of the counter is 1, indicating a live object with no in-flight calls.
+// The initial value for the flag is false.
+//
+// * At the start of each method call, we atomically check the counter.
+// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted.
+// If it is nonzero them we atomically increment it by 1 and proceed with the method call.
+//
+// * At the end of each method call, we atomically decrement and check the counter.
+// If it has reached zero then we destroy the underlying Rust struct.
+//
+// * When `destroy` is called, we atomically flip the flag from false to true.
+// If the flag was already true we silently fail.
+// Otherwise we atomically decrement and check the counter.
+// If it has reached zero then we destroy the underlying Rust struct.
+//
+// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works,
+// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`.
+//
+// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been
+// called *and* all in-flight method calls have completed, avoiding violating any of the expectations
+// of the underlying Rust code.
+//
+// This makes a cleaner a better alternative to _not_ calling `destroy()` as
+// and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop`
+// method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner
+// thread may be starved, and the app will leak memory.
+//
+// In this case, `destroy`ing manually may be a better solution.
+//
+// The cleaner can live side by side with the manual calling of `destroy`. In the order of responsiveness, uniffi objects
+// with Rust peers are reclaimed:
+//
+// 1. By calling the `destroy` method of the object, which calls `rustObject.free()`. If that doesn't happen:
+// 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then:
+// 3. The memory is reclaimed when the process terminates.
+//
+// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219
+//
+
+
/**
* Command to build a move vector out of a set of individual elements
*
diff --git a/bindings/python/lib/iota_sdk_ffi.py b/bindings/python/lib/iota_sdk_ffi.py
index 61d77a831..b2af5126f 100644
--- a/bindings/python/lib/iota_sdk_ffi.py
+++ b/bindings/python/lib/iota_sdk_ffi.py
@@ -1307,6 +1307,12 @@ def _uniffi_check_api_checksums(lib):
raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
if lib.uniffi_iota_sdk_ffi_checksum_method_identifier_as_str() != 63815:
raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
+ if lib.uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_address() != 5173:
+ raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
+ if lib.uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_public_key() != 26320:
+ raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
+ if lib.uniffi_iota_sdk_ffi_checksum_method_ledgersigner_sign_transaction() != 39180:
+ raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
if lib.uniffi_iota_sdk_ffi_checksum_method_makemovevector_elements() != 20773:
raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
if lib.uniffi_iota_sdk_ffi_checksum_method_makemovevector_type_tag() != 31154:
@@ -2191,6 +2197,8 @@ def _uniffi_check_api_checksums(lib):
raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
if lib.uniffi_iota_sdk_ffi_checksum_constructor_input_new_shared() != 61970:
raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
+ if lib.uniffi_iota_sdk_ffi_checksum_constructor_ledgersigner_new_with_default() != 16447:
+ raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
if lib.uniffi_iota_sdk_ffi_checksum_constructor_makemovevector_new() != 20934:
raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
if lib.uniffi_iota_sdk_ffi_checksum_constructor_mergecoins_new() != 1506:
@@ -4769,6 +4777,46 @@ class _UniffiVTableCallbackInterfaceTransactionSignerFn(ctypes.Structure):
ctypes.POINTER(_UniffiRustCallStatus),
)
_UniffiLib.uniffi_iota_sdk_ffi_fn_constructor_input_new_shared.restype = ctypes.c_void_p
+_UniffiLib.uniffi_iota_sdk_ffi_fn_clone_ledgersigner.argtypes = (
+ ctypes.c_void_p,
+ ctypes.POINTER(_UniffiRustCallStatus),
+)
+_UniffiLib.uniffi_iota_sdk_ffi_fn_clone_ledgersigner.restype = ctypes.c_void_p
+_UniffiLib.uniffi_iota_sdk_ffi_fn_free_ledgersigner.argtypes = (
+ ctypes.c_void_p,
+ ctypes.POINTER(_UniffiRustCallStatus),
+)
+_UniffiLib.uniffi_iota_sdk_ffi_fn_free_ledgersigner.restype = None
+_UniffiLib.uniffi_iota_sdk_ffi_fn_constructor_ledgersigner_new_with_default.argtypes = (
+ _UniffiRustBuffer,
+ ctypes.POINTER(_UniffiRustCallStatus),
+)
+_UniffiLib.uniffi_iota_sdk_ffi_fn_constructor_ledgersigner_new_with_default.restype = ctypes.c_void_p
+_UniffiLib.uniffi_iota_sdk_ffi_fn_method_ledgersigner_get_address.argtypes = (
+ ctypes.c_void_p,
+ ctypes.POINTER(_UniffiRustCallStatus),
+)
+_UniffiLib.uniffi_iota_sdk_ffi_fn_method_ledgersigner_get_address.restype = ctypes.c_void_p
+_UniffiLib.uniffi_iota_sdk_ffi_fn_method_ledgersigner_get_public_key.argtypes = (
+ ctypes.c_void_p,
+ ctypes.POINTER(_UniffiRustCallStatus),
+)
+_UniffiLib.uniffi_iota_sdk_ffi_fn_method_ledgersigner_get_public_key.restype = ctypes.c_void_p
+_UniffiLib.uniffi_iota_sdk_ffi_fn_method_ledgersigner_sign_transaction.argtypes = (
+ ctypes.c_void_p,
+ ctypes.c_void_p,
+)
+_UniffiLib.uniffi_iota_sdk_ffi_fn_method_ledgersigner_sign_transaction.restype = ctypes.c_uint64
+_UniffiLib.uniffi_iota_sdk_ffi_fn_clone_ledgersignererror.argtypes = (
+ ctypes.c_void_p,
+ ctypes.POINTER(_UniffiRustCallStatus),
+)
+_UniffiLib.uniffi_iota_sdk_ffi_fn_clone_ledgersignererror.restype = ctypes.c_void_p
+_UniffiLib.uniffi_iota_sdk_ffi_fn_free_ledgersignererror.argtypes = (
+ ctypes.c_void_p,
+ ctypes.POINTER(_UniffiRustCallStatus),
+)
+_UniffiLib.uniffi_iota_sdk_ffi_fn_free_ledgersignererror.restype = None
_UniffiLib.uniffi_iota_sdk_ffi_fn_clone_makemovevector.argtypes = (
ctypes.c_void_p,
ctypes.POINTER(_UniffiRustCallStatus),
@@ -11207,6 +11255,15 @@ class _UniffiVTableCallbackInterfaceTransactionSignerFn(ctypes.Structure):
_UniffiLib.uniffi_iota_sdk_ffi_checksum_method_identifier_as_str.argtypes = (
)
_UniffiLib.uniffi_iota_sdk_ffi_checksum_method_identifier_as_str.restype = ctypes.c_uint16
+_UniffiLib.uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_address.argtypes = (
+)
+_UniffiLib.uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_address.restype = ctypes.c_uint16
+_UniffiLib.uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_public_key.argtypes = (
+)
+_UniffiLib.uniffi_iota_sdk_ffi_checksum_method_ledgersigner_get_public_key.restype = ctypes.c_uint16
+_UniffiLib.uniffi_iota_sdk_ffi_checksum_method_ledgersigner_sign_transaction.argtypes = (
+)
+_UniffiLib.uniffi_iota_sdk_ffi_checksum_method_ledgersigner_sign_transaction.restype = ctypes.c_uint16
_UniffiLib.uniffi_iota_sdk_ffi_checksum_method_makemovevector_elements.argtypes = (
)
_UniffiLib.uniffi_iota_sdk_ffi_checksum_method_makemovevector_elements.restype = ctypes.c_uint16
@@ -12533,6 +12590,9 @@ class _UniffiVTableCallbackInterfaceTransactionSignerFn(ctypes.Structure):
_UniffiLib.uniffi_iota_sdk_ffi_checksum_constructor_input_new_shared.argtypes = (
)
_UniffiLib.uniffi_iota_sdk_ffi_checksum_constructor_input_new_shared.restype = ctypes.c_uint16
+_UniffiLib.uniffi_iota_sdk_ffi_checksum_constructor_ledgersigner_new_with_default.argtypes = (
+)
+_UniffiLib.uniffi_iota_sdk_ffi_checksum_constructor_ledgersigner_new_with_default.restype = ctypes.c_uint16
_UniffiLib.uniffi_iota_sdk_ffi_checksum_constructor_makemovevector_new.argtypes = (
)
_UniffiLib.uniffi_iota_sdk_ffi_checksum_constructor_makemovevector_new.restype = ctypes.c_uint16
@@ -13687,6 +13747,10 @@ def write(value, buf):
+
+
+
+
@@ -35867,6 +35931,190 @@ def read(cls, buf: _UniffiRustBuffer):
@classmethod
def write(cls, value: InputProtocol, buf: _UniffiRustBuffer):
buf.write_u64(cls.lower(value))
+class LedgerSignerProtocol(typing.Protocol):
+ def get_address(self, ):
+ raise NotImplementedError
+ def get_public_key(self, ):
+ raise NotImplementedError
+ def sign_transaction(self, transaction: "Transaction"):
+ raise NotImplementedError
+# LedgerSigner is a Rust-only trait - it's a wrapper around a Rust implementation.
+class LedgerSigner():
+ _pointer: ctypes.c_void_p
+
+ def __init__(self, *args, **kwargs):
+ raise ValueError("This class has no default constructor")
+
+ def __del__(self):
+ # In case of partial initialization of instances.
+ pointer = getattr(self, "_pointer", None)
+ if pointer is not None:
+ _uniffi_rust_call(_UniffiLib.uniffi_iota_sdk_ffi_fn_free_ledgersigner, pointer)
+
+ def _uniffi_clone_pointer(self):
+ return _uniffi_rust_call(_UniffiLib.uniffi_iota_sdk_ffi_fn_clone_ledgersigner, self._pointer)
+
+ # Used by alternative constructors or any methods which return this type.
+ @classmethod
+ def _make_instance_(cls, pointer):
+ # Lightly yucky way to bypass the usual __init__ logic
+ # and just create a new instance with the required pointer.
+ inst = cls.__new__(cls)
+ inst._pointer = pointer
+ return inst
+ @classmethod
+ def new_with_default(cls, path: "str"):
+ _UniffiConverterString.check_lower(path)
+
+ # Call the (fallible) function before creating any half-baked object instances.
+ pointer = _uniffi_rust_call_with_error(_UniffiConverterTypeLedgerSignerError__as_error,_UniffiLib.uniffi_iota_sdk_ffi_fn_constructor_ledgersigner_new_with_default,
+ _UniffiConverterString.lower(path))
+ return cls._make_instance_(pointer)
+
+
+
+ def get_address(self, ) -> "Address":
+ return _UniffiConverterTypeAddress.lift(
+ _uniffi_rust_call_with_error(_UniffiConverterTypeLedgerSignerError__as_error,_UniffiLib.uniffi_iota_sdk_ffi_fn_method_ledgersigner_get_address,self._uniffi_clone_pointer(),)
+ )
+
+
+
+
+
+ def get_public_key(self, ) -> "Ed25519PublicKey":
+ return _UniffiConverterTypeEd25519PublicKey.lift(
+ _uniffi_rust_call_with_error(_UniffiConverterTypeLedgerSignerError__as_error,_UniffiLib.uniffi_iota_sdk_ffi_fn_method_ledgersigner_get_public_key,self._uniffi_clone_pointer(),)
+ )
+
+
+
+
+ async def sign_transaction(self, transaction: "Transaction") -> "UserSignature":
+ _UniffiConverterTypeTransaction.check_lower(transaction)
+
+ return await _uniffi_rust_call_async(
+ _UniffiLib.uniffi_iota_sdk_ffi_fn_method_ledgersigner_sign_transaction(
+ self._uniffi_clone_pointer(),
+ _UniffiConverterTypeTransaction.lower(transaction)
+ ),
+ _UniffiLib.ffi_iota_sdk_ffi_rust_future_poll_pointer,
+ _UniffiLib.ffi_iota_sdk_ffi_rust_future_complete_pointer,
+ _UniffiLib.ffi_iota_sdk_ffi_rust_future_free_pointer,
+ # lift function
+ _UniffiConverterTypeUserSignature.lift,
+
+ # Error FFI converter
+_UniffiConverterTypeLedgerSignerError__as_error,
+
+ )
+
+
+
+
+
+class _UniffiConverterTypeLedgerSigner:
+
+ @staticmethod
+ def lift(value: int):
+ return LedgerSigner._make_instance_(value)
+
+ @staticmethod
+ def check_lower(value: LedgerSigner):
+ if not isinstance(value, LedgerSigner):
+ raise TypeError("Expected LedgerSigner instance, {} found".format(type(value).__name__))
+
+ @staticmethod
+ def lower(value: LedgerSignerProtocol):
+ if not isinstance(value, LedgerSigner):
+ raise TypeError("Expected LedgerSigner instance, {} found".format(type(value).__name__))
+ return value._uniffi_clone_pointer()
+
+ @classmethod
+ def read(cls, buf: _UniffiRustBuffer):
+ ptr = buf.read_u64()
+ if ptr == 0:
+ raise InternalError("Raw pointer value was null")
+ return cls.lift(ptr)
+
+ @classmethod
+ def write(cls, value: LedgerSignerProtocol, buf: _UniffiRustBuffer):
+ buf.write_u64(cls.lower(value))
+class LedgerSignerErrorProtocol(typing.Protocol):
+ pass
+# LedgerSignerError is a Rust-only trait - it's a wrapper around a Rust implementation.
+class LedgerSignerError(Exception):
+ _pointer: ctypes.c_void_p
+
+ def __init__(self, *args, **kwargs):
+ raise ValueError("This class has no default constructor")
+
+ def __del__(self):
+ # In case of partial initialization of instances.
+ pointer = getattr(self, "_pointer", None)
+ if pointer is not None:
+ _uniffi_rust_call(_UniffiLib.uniffi_iota_sdk_ffi_fn_free_ledgersignererror, pointer)
+
+ def _uniffi_clone_pointer(self):
+ return _uniffi_rust_call(_UniffiLib.uniffi_iota_sdk_ffi_fn_clone_ledgersignererror, self._pointer)
+
+ # Used by alternative constructors or any methods which return this type.
+ @classmethod
+ def _make_instance_(cls, pointer):
+ # Lightly yucky way to bypass the usual __init__ logic
+ # and just create a new instance with the required pointer.
+ inst = cls.__new__(cls)
+ inst._pointer = pointer
+ return inst
+
+
+
+class _UniffiConverterTypeLedgerSignerError__as_error(_UniffiConverterRustBuffer):
+ @classmethod
+ def read(cls, buf):
+ raise NotImplementedError()
+
+ @classmethod
+ def write(cls, value, buf):
+ raise NotImplementedError()
+
+ @staticmethod
+ def lift(value):
+ # Errors are always a rust buffer holding a pointer - which is a "read"
+ with value.consume_with_stream() as stream:
+ return _UniffiConverterTypeLedgerSignerError.read(stream)
+
+ @staticmethod
+ def lower(value):
+ raise NotImplementedError()
+
+class _UniffiConverterTypeLedgerSignerError:
+
+ @staticmethod
+ def lift(value: int):
+ return LedgerSignerError._make_instance_(value)
+
+ @staticmethod
+ def check_lower(value: LedgerSignerError):
+ if not isinstance(value, LedgerSignerError):
+ raise TypeError("Expected LedgerSignerError instance, {} found".format(type(value).__name__))
+
+ @staticmethod
+ def lower(value: LedgerSignerErrorProtocol):
+ if not isinstance(value, LedgerSignerError):
+ raise TypeError("Expected LedgerSignerError instance, {} found".format(type(value).__name__))
+ return value._uniffi_clone_pointer()
+
+ @classmethod
+ def read(cls, buf: _UniffiRustBuffer):
+ ptr = buf.read_u64()
+ if ptr == 0:
+ raise InternalError("Raw pointer value was null")
+ return cls.lift(ptr)
+
+ @classmethod
+ def write(cls, value: LedgerSignerErrorProtocol, buf: _UniffiRustBuffer):
+ buf.write_u64(cls.lower(value))
class MakeMoveVectorProtocol(typing.Protocol):
"""
Command to build a move vector out of a set of individual elements
@@ -51825,6 +52073,8 @@ def zk_login_public_identifier_to_bcs(data: "ZkLoginPublicIdentifier") -> "bytes
"GraphQlClient",
"Identifier",
"Input",
+ "LedgerSigner",
+ "LedgerSignerError",
"MakeMoveVector",
"MergeCoins",
"MoveArg",
diff --git a/crates/iota-ledger-signer/Cargo.toml b/crates/iota-ledger-signer/Cargo.toml
new file mode 100644
index 000000000..4920e7ba2
--- /dev/null
+++ b/crates/iota-ledger-signer/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "iota-ledger-signer"
+version = "0.0.0"
+authors = ["IOTA Foundation "]
+edition = "2021"
+license = "Apache-2.0"
+publish = false
+
+[dependencies]
+# external dependencies
+bip32 = "0.5.3"
+thiserror.workspace = true
+tracing = "0.1"
+
+# internal dependencies
+iota-ledger.workspace = true
+iota-sdk.workspace = true
+iota-types.workspace = true
+iota-graphql-client.workspace = true
+
+[dev-dependencies]
+anyhow = "1.0"
+base64 = "0.22"
+bcs.workspace = true
+clap = "4.5"
+tokio.workspace = true
diff --git a/crates/iota-ledger-signer/README.md b/crates/iota-ledger-signer/README.md
new file mode 100644
index 000000000..a3bf481cf
--- /dev/null
+++ b/crates/iota-ledger-signer/README.md
@@ -0,0 +1,16 @@
+# iota-ledger-signer
+
+High-level IOTA Ledger signer implementation for transaction signing and key management.
+
+## Overview
+
+This crate provides a convenient, high-level interface for using Ledger hardware wallets with the IOTA network. It wraps the lower-level `iota-ledger` crate and integrates with the IOTA SDK to provide seamless transaction signing and key management capabilities.
+
+## Examples
+
+This crate provides a sample transaction signing implementation in `examples/ledger_signer.rs`.
+To run the example, use:
+
+```bash
+cargo run --example ledger_signer -- --path "m/44'/4218'/0'/0'/0'" --network testnet --tx ""
+```
diff --git a/crates/iota-ledger-signer/examples/ledger_signer.rs b/crates/iota-ledger-signer/examples/ledger_signer.rs
new file mode 100644
index 000000000..07729c12c
--- /dev/null
+++ b/crates/iota-ledger-signer/examples/ledger_signer.rs
@@ -0,0 +1,93 @@
+// // Copyright (c) 2025 IOTA Stiftung
+// // SPDX-License-Identifier: Apache-2.0
+
+// use std::str::FromStr;
+
+use anyhow::Result;
+// use clap::{Arg, Command};
+// use iota_sdk::{
+// IotaClientBuilder,
+// types::{crypto::EncodeDecodeBase64, transaction::TransactionData},
+// };
+
+// fn transaction_from_base64(b64: &str) -> Result { let bytes =
+// base64::Engine::decode(&base64::engine::general_purpose::STANDARD, b64)
+// .map_err(|e| anyhow::format_err!("Invalid base64 in transaction:
+// {e}"))?; bcs::from_bytes(&bytes).map_err(|e| anyhow::format_err!("Invalid
+// transaction format: {e}")) }
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ // let matches = Command::new("ledger_signer")
+ // .version("1.0")
+ // .arg(
+ // Arg::new("bip32-path")
+ // .short('p')
+ // .long("path")
+ // .help("bip32 path to use (default
+ // \"m/44'/4218'/0'/0'/0'\")") .value_name("PATH")
+ // .required(false),
+ // )
+ // .arg(
+ // Arg::new("network")
+ // .short('n')
+ // .long("network")
+ // .help("select the network to connect to for fetching
+ // inputs (local, devnet, testnet, mainnet or custom URL)")
+ // .required(false), )
+ // .arg(
+ // Arg::new("transaction")
+ // .long("tx")
+ // .help("transaction bytes in base64 format")
+ // .required(true),
+ // )
+ // .get_matches();
+
+ // let derivation_path = bip32::DerivationPath::from_str(
+ // matches
+ // .get_one::("bip32-path")
+ // .map(|s| s.as_str())
+ // .unwrap_or("m/44'/4218'/0'/0'/0'"),
+ // )?;
+
+ // let network = matches.get_one::("network").map(|s|
+ // s.as_str()); let client = match network {
+ // Some("local") =>
+ // Some(IotaClientBuilder::default().build_localnet().await?),
+ // Some(" devnet") =>
+ // Some(IotaClientBuilder::default().build_devnet().await?),
+ // Some("testnet") =>
+ // Some(IotaClientBuilder::default().build_testnet().await?), Some("
+ // mainnet") => Some(IotaClientBuilder::default().build_mainnet().await?),
+ // Some(url) =>
+ // Some(IotaClientBuilder::default().build(url).await?), None =>
+ // None, };
+ // if let Some(c) = &client {
+ // println!(
+ // "Connected to IOTA network: {} using version {}",
+ // network.unwrap(),
+ // c.api_version()
+ // );
+ // } else {
+ // println!("No IOTA network specified, only blind-signing
+ // supported."); }
+
+ // let transaction =
+ // transaction_from_base64(matches.get_one::("transaction").
+ // unwrap())?;
+
+ // let signer =
+ // iota_ledger_signer::LedgerSigner::new_with_default(derivation_path,
+ // client)?;
+
+ // // Get the signer's address
+ // let address = signer.get_address()?;
+ // println!("Signer address: {}", &address);
+
+ // let signed_tx = signer.sign_transaction(&transaction,
+ // &address).await?; println!("Signature: {}",
+ // signed_tx.signature.encode_base64());
+
+ Ok(())
+}
diff --git a/crates/iota-ledger-signer/src/errors.rs b/crates/iota-ledger-signer/src/errors.rs
new file mode 100644
index 000000000..f39ecc383
--- /dev/null
+++ b/crates/iota-ledger-signer/src/errors.rs
@@ -0,0 +1,14 @@
+// Copyright (c) 2025 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use iota_graphql_client::error::Error as ClientError;
+use iota_ledger::LedgerError;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum LedgerSignerError {
+ #[error("Ledger error: {0}")]
+ Ledger(#[from] LedgerError),
+ #[error("Client error: {0}")]
+ Client(#[from] ClientError),
+}
diff --git a/crates/iota-ledger-signer/src/lib.rs b/crates/iota-ledger-signer/src/lib.rs
new file mode 100644
index 000000000..8461850ca
--- /dev/null
+++ b/crates/iota-ledger-signer/src/lib.rs
@@ -0,0 +1,119 @@
+// Copyright (c) 2025 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use iota_graphql_client::Client as IotaClient;
+use iota_ledger::{Ledger, SignedTransaction};
+use iota_types::{
+ Address, Transaction,
+ crypto::{Ed25519PublicKey, Intent, SignatureScheme},
+};
+use tracing::warn;
+
+mod errors;
+pub use errors::LedgerSignerError;
+mod utils;
+
+pub struct LedgerSigner {
+ path: bip32::DerivationPath,
+ ledger: Ledger,
+ client: Option,
+}
+
+impl LedgerSigner {
+ pub fn new_with_default(
+ path: bip32::DerivationPath,
+ client: Option,
+ ) -> Result {
+ let ledger = Ledger::new_with_default()?;
+ Ok(Self::new(ledger, path, client))
+ }
+
+ pub fn new(ledger: Ledger, path: bip32::DerivationPath, client: Option) -> Self {
+ LedgerSigner {
+ ledger,
+ path,
+ client,
+ }
+ }
+
+ pub fn get_signature_scheme(&self) -> SignatureScheme {
+ self.ledger.get_signature_scheme()
+ }
+
+ pub fn get_address(&self) -> Result {
+ let public_key = self.ledger.get_public_key(&self.path)?;
+ Ok(public_key.address)
+ }
+
+ pub fn get_public_key(&self) -> Result {
+ let public_key = self.ledger.get_public_key(&self.path)?;
+ Ok(public_key.public_key)
+ }
+
+ // TODO
+ pub async fn sign_transaction_unchecked(
+ &self,
+ transaction: &Transaction,
+ ) -> Result {
+ let objects = if let Some(client) = &self.client {
+ match utils::load_objects_with_client(client, transaction).await {
+ Ok(objects) => objects,
+ Err(e) => {
+ warn!("Failed to load objects: {e}. Falling back to blind-signing.");
+ vec![]
+ }
+ }
+ } else {
+ vec![]
+ };
+
+ self.ledger
+ .sign_intent_unchecked(&self.path, Intent::iota_transaction(), transaction, objects)
+ .map_err(LedgerSignerError::from)
+ }
+
+ // TODO why address?
+ pub async fn sign_transaction(
+ &self,
+ transaction: &Transaction,
+ address: &Address,
+ ) -> Result {
+ let objects = if let Some(client) = &self.client {
+ match utils::load_objects_with_client(client, transaction).await {
+ Ok(objects) => objects,
+ Err(e) => {
+ warn!("Failed to load objects: {e}. Falling back to blind-signing.");
+ vec![]
+ }
+ }
+ } else {
+ vec![]
+ };
+
+ self.ledger
+ .sign_intent(
+ &self.path,
+ address,
+ Intent::iota_transaction(),
+ transaction,
+ objects,
+ )
+ .map_err(LedgerSignerError::from)
+ }
+
+ pub fn sign_message(
+ &self,
+ message: Vec,
+ address: &Address,
+ ) -> Result {
+ self.ledger
+ .sign_intent(
+ &self.path,
+ address,
+ Intent::personal_message(),
+ &message,
+ vec![],
+ )
+ .map_err(LedgerSignerError::from)
+ }
+}
diff --git a/crates/iota-ledger-signer/src/utils.rs b/crates/iota-ledger-signer/src/utils.rs
new file mode 100644
index 000000000..9b289d7fa
--- /dev/null
+++ b/crates/iota-ledger-signer/src/utils.rs
@@ -0,0 +1,74 @@
+// Copyright (c) 2025 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use std::collections::HashSet;
+
+// use iota_sdk::{
+// IotaClient,
+// rpc_types::{IotaObjectData, IotaObjectDataOptions, IotaObjectResponse},
+// types::{
+// base_types::{ObjectID, ObjectType},
+// object::{MoveObject, Object},
+// transaction::{InputObjectKind, TransactionData, TransactionDataAPI},
+// },
+// };
+use iota_graphql_client::{
+ Client as IotaClient, pagination::PaginationFilter, query_types::ObjectFilter,
+};
+use iota_types::{Input, Object, ObjectId, Transaction, TransactionKind};
+
+use crate::LedgerSignerError;
+
+pub(crate) async fn load_objects_with_client(
+ client: &IotaClient,
+ transaction: &Transaction,
+) -> Result, LedgerSignerError> {
+ let object_ids = object_ids_from_transaction(transaction)?;
+
+ if object_ids.is_empty() {
+ return Ok(vec![]);
+ }
+
+ let responses = client
+ .objects(
+ ObjectFilter {
+ object_ids: Some(object_ids),
+ ..Default::default()
+ },
+ PaginationFilter::default(),
+ )
+ .await?;
+
+ // TODO properly iterate pages?
+
+ Ok(responses.data)
+}
+
+fn object_ids_from_transaction(
+ transaction: &Transaction,
+) -> Result, LedgerSignerError> {
+ // TODO v1 ? Need a Tx API ?
+ let object_ids = transaction
+ .as_v1()
+ .gas_payment
+ .objects
+ .iter()
+ .map(|payment| payment.object_id);
+
+ let ptb = if let TransactionKind::ProgrammableTransaction(ptb) = &transaction.as_v1().kind {
+ ptb
+ } else {
+ panic!("Expected ProgrammableTransaction")
+ };
+
+ let input_objects = ptb.inputs.iter().filter_map(|input| match input {
+ Input::ImmutableOrOwned(object_ref) => Some(object_ref.object_id),
+ _ => None,
+ });
+
+ let mut unique_ids = HashSet::new();
+ unique_ids.extend(object_ids);
+ unique_ids.extend(input_objects);
+
+ Ok(unique_ids.into_iter().collect())
+}
diff --git a/crates/iota-ledger/Cargo.toml b/crates/iota-ledger/Cargo.toml
new file mode 100644
index 000000000..e97bb6dad
--- /dev/null
+++ b/crates/iota-ledger/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "iota-ledger"
+version = "0.0.0"
+authors = ["IOTA Foundation "]
+edition = "2021"
+license = "Apache-2.0"
+publish = false
+
+[dependencies]
+# external dependencies
+bcs.workspace = true
+bip32 = "0.5.3"
+fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "69d496c71fb37e3d22fe85e5bbfd4256d61422b9" }
+hex.workspace = true
+hidapi = { version = "2.6.3", features = [
+ "linux-static-hidraw",
+], default-features = false }
+ledger-transport = "0.11.0"
+ledger-transport-hid = "0.11.0"
+serde.workspace = true
+thiserror.workspace = true
+tracing = "0.1"
+
+# internal dependencies
+iota-types.workspace = true
+
+[dev-dependencies]
+anyhow = "1.0"
+base64 = "0.22"
+clap = "4.5"
diff --git a/crates/iota-ledger/README.md b/crates/iota-ledger/README.md
new file mode 100644
index 000000000..e7b7eff96
--- /dev/null
+++ b/crates/iota-ledger/README.md
@@ -0,0 +1,66 @@
+# iota-ledger
+
+Low-level IOTA Ledger hardware wallet integration library.
+
+## Overview
+
+This crate provides direct communication with IOTA Ledger applications running on Ledger hardware wallets or the Speculos simulator. It implements the APDU protocol for key operations like getting public keys, signing transactions, and managing the app lifecycle.
+
+## Features
+
+- **Multiple Transport Support**: Connects via USB HID or TCP (for Speculos simulator)
+- **Key Management**: Retrieve public keys and IOTA addresses using BIP32 derivation paths
+- **Transaction Signing**: Sign IOTA transactions with hardware wallet security
+- **App Management**: Open/close IOTA app and check app status
+- **Address Verification**: Display and verify addresses on device screen
+- **Version Information**: Query the IOTA app version on the device
+
+## Usage
+
+```rust
+use iota_ledger::Ledger;
+use bip32::DerivationPath;
+use std::str::FromStr;
+
+// Create a ledger instance (automatically detects HID or simulator)
+let ledger = Ledger::new_with_default()?;
+
+// Or explicitly use the simulator
+let ledger = Ledger::new_with_simulator()?;
+
+// Define a BIP32 derivation path
+let path = DerivationPath::from_str("m/44'/4218'/0'/0'/0'")?;
+
+// Get public key and address
+let public_key_result = ledger.get_public_key(&path)?;
+println!("Address: {}", public_key_result.address);
+
+// Verify address on device (shows on screen for user confirmation)
+let verified = ledger.verify_address(&path)?;
+
+// Sign a transaction using intent-based signing
+let signed_tx = ledger.sign_intent(&path, &address, intent, &transaction_data, objects)?;
+```
+
+## Transport Types
+
+The crate supports two transport mechanisms:
+
+- **Native HID**: Direct USB communication with physical Ledger devices
+- **TCP**: Communication with Speculos simulator (default port 9999)
+
+Set the `LEDGER_SIMULATOR` environment variable to automatically use TCP transport.
+
+## Examples
+
+The crate includes several examples in the `examples/` directory:
+
+- `ledger_get_public_key.rs`: Retrieve public keys and addresses
+- `ledger_sign_tx.rs`: Sign transactions with the Ledger device
+- `ledger_open_app.rs`: Ensure the IOTA app is open and read the version
+
+Run examples with:
+
+```bash
+cargo run --example ledger_get_public_key -- --path "m/44'/4218'/0'/0'/0'"
+```
diff --git a/crates/iota-ledger/examples/ledger_get_public_key.rs b/crates/iota-ledger/examples/ledger_get_public_key.rs
new file mode 100644
index 000000000..137a967bc
--- /dev/null
+++ b/crates/iota-ledger/examples/ledger_get_public_key.rs
@@ -0,0 +1,65 @@
+// // Copyright (c) 2025 IOTA Stiftung
+// // SPDX-License-Identifier: Apache-2.0
+
+// use std::str::FromStr;
+
+use anyhow::Result;
+// use clap::{Arg, Command};
+
+pub fn main() -> Result<()> {
+ // let matches = Command::new("get_public_key")
+ // .version("1.0")
+ // .arg(
+ // Arg::new("bip32-path")
+ // .short('p')
+ // .long("path")
+ // .help("bip32 path to use (default \"m/44'/4218'/0'/0'/0'\")")
+ // .value_name("PATH")
+ // .required(false),
+ // )
+ // .arg(
+ // Arg::new("verify")
+ // .long("verify")
+ // .help("verify address (default false)")
+ // .action(clap::ArgAction::SetTrue)
+ // .required(false),
+ // )
+ // .arg(
+ // Arg::new("is-simulator")
+ // .short('s')
+ // .long("simulator")
+ // .help("select the simulator as transport")
+ // .action(clap::ArgAction::SetTrue)
+ // .required(false),
+ // )
+ // .get_matches();
+
+ // let is_simulator = matches.get_flag("is-simulator");
+
+ // let derivation_path = bip32::DerivationPath::from_str(
+ // matches
+ // .get_one::("bip32-path")
+ // .map(|s| s.as_str())
+ // .unwrap_or("m/44'/4218'/0'/0'/0'"),
+ // )?;
+
+ // let verify = matches.get_flag("verify");
+
+ // let ledger = if is_simulator {
+ // iota_ledger::Ledger::new_with_simulator()?
+ // } else {
+ // iota_ledger::Ledger::new_with_native_hid()?
+ // };
+
+ // // generate address without prompt
+ // let pk_result = if verify {
+ // ledger.verify_address(&derivation_path)?
+ // } else {
+ // ledger.get_public_key(&derivation_path)?
+ // };
+
+ // println!("Public Key: 0x{}", hex::encode(&pk_result.public_key));
+ // println!("Address: 0x{}", hex::encode(pk_result.address));
+
+ Ok(())
+}
diff --git a/crates/iota-ledger/examples/ledger_open_app.rs b/crates/iota-ledger/examples/ledger_open_app.rs
new file mode 100644
index 000000000..1dfadb405
--- /dev/null
+++ b/crates/iota-ledger/examples/ledger_open_app.rs
@@ -0,0 +1,12 @@
+// // Copyright (c) 2025 IOTA Stiftung
+// // SPDX-License-Identifier: Apache-2.0
+
+use anyhow::Result;
+
+pub fn main() -> Result<()> {
+ // let mut ledger = iota_ledger::Ledger::new_with_native_hid()?;
+ // ledger.ensure_app_is_open()?;
+ // let version = ledger.get_version()?;
+ // println!("Current IOTA app version: {version}");
+ Ok(())
+}
diff --git a/crates/iota-ledger/examples/ledger_sign_tx.rs b/crates/iota-ledger/examples/ledger_sign_tx.rs
new file mode 100644
index 000000000..737f71674
--- /dev/null
+++ b/crates/iota-ledger/examples/ledger_sign_tx.rs
@@ -0,0 +1,103 @@
+// // Copyright (c) 2025 IOTA Stiftung
+// // SPDX-License-Identifier: Apache-2.0
+
+// use std::str::FromStr;
+
+use anyhow::Result;
+// use clap::{Arg, Command};
+// use iota_types::{
+// crypto::{EncodeDecodeBase64, Intent},
+// object::Object,
+// transaction::TransactionData,
+// };
+
+// fn transaction_from_base64(b64: &str) -> TransactionData {
+// let bytes =
+// base64::Engine::decode(&base64::engine::general_purpose::STANDARD, b64)
+// .expect("Invalid base64 in transaction");
+// bcs::from_bytes(&bytes).expect("Invalid bcs in transaction")
+// }
+
+// fn object_from_base64(b64: &str) -> Object {
+// let bytes =
+// base64::Engine::decode(&base64::engine::general_purpose::STANDARD, b64)
+// .expect("Invalid base64 in object");
+// bcs::from_bytes(&bytes).expect("Invalid bcs in object")
+// }
+
+pub fn main() -> Result<()> {
+ // let matches = Command::new("sign_tx")
+ // .version("1.0")
+ // .arg(
+ // Arg::new("bip32-path")
+ // .short('p')
+ // .long("path")
+ // .help("bip32 path to use (default \"m/44'/4218'/0'/0'/0'\")")
+ // .value_name("PATH")
+ // .required(false),
+ // )
+ // .arg(
+ // Arg::new("transaction")
+ // .long("tx")
+ // .help("transaction bytes in base64 format")
+ // .required(true),
+ // )
+ // .arg(
+ // Arg::new("is-simulator")
+ // .short('s')
+ // .long("simulator")
+ // .help("select the simulator as transport")
+ // .action(clap::ArgAction::SetTrue)
+ // .required(false),
+ // )
+ // .arg(
+ // Arg::new("objects")
+ // .long("objects")
+ // .help("A list of input objects in base64 format")
+ // .value_name("OBJECTS")
+ // .num_args(1..)
+ // .action(clap::ArgAction::Append)
+ // .required(false),
+ // )
+ // .get_matches();
+
+ // let is_simulator = matches.get_flag("is-simulator");
+
+ // let derivation_path = bip32::DerivationPath::from_str(
+ // matches
+ // .get_one::("bip32-path")
+ // .map(|s| s.as_str())
+ // .unwrap_or("m/44'/4218'/0'/0'/0'"),
+ // )?;
+
+ // let objects: Vec