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 = matches + // .get_many::("objects") + // .map(|objs| objs.map(|o| object_from_base64(o)).collect()) + // .unwrap_or_default(); + + // let ledger = if is_simulator { + // iota_ledger::Ledger::new_with_simulator()? + // } else { + // iota_ledger::Ledger::new_with_native_hid()? + // }; + + // let key_response = ledger.get_public_key(&derivation_path)?; + // println!("Address: {}", key_response.address); + + // let transaction = transaction_from_base64( + // matches + // .get_one::("transaction") + // .expect("Transaction bytes are required"), + // ); + + // let signature = ledger.sign_intent( + // &derivation_path, + // &key_response.address, + // Intent::iota_transaction(), + // &transaction, + // objects, + // )?; + // println!("Signature: {}", &signature.signature.encode_base64()); + + Ok(()) +} diff --git a/crates/iota-ledger/src/api/bolos/app_exit.rs b/crates/iota-ledger/src/api/bolos/app_exit.rs new file mode 100644 index 000000000..95f4f48cf --- /dev/null +++ b/crates/iota-ledger/src/api/bolos/app_exit.rs @@ -0,0 +1,20 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use ledger_transport::APDUCommand; + +use crate::{ + Transport, + api::{bolos, errors, helpers}, +}; + +pub fn exec(transport: &T) -> Result<(), errors::LedgerError> { + let cmd = APDUCommand { + cla: bolos::APDU_CLA_B0, + ins: bolos::APDUInstructions::AppExitB0 as u8, + p1: bolos::APDU_P1, + p2: bolos::APDU_P2, + data: Vec::new(), + }; + helpers::exec::(transport, cmd) +} diff --git a/crates/iota-ledger/src/api/bolos/app_get_name.rs b/crates/iota-ledger/src/api/bolos/app_get_name.rs new file mode 100644 index 000000000..ff5ef7a24 --- /dev/null +++ b/crates/iota-ledger/src/api/bolos/app_get_name.rs @@ -0,0 +1,58 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use ledger_transport::APDUCommand; + +use crate::{ + Transport, + api::{ + bolos, errors, helpers, + packable::{Error as PackableError, Read, Unpackable}, + }, +}; +// dashboard: +// HID => b001000000 +// HID <= 0105|424f4c4f53|05|322e302e30|9000 +// B O L O S 2 . 0 . 0 +// +// "IOTA" +// HID => b001000000 +// HID <= 0104|494f5441|05|302e372e30|0102|9000 +// I O T A 0 . 7 . 0 + +#[expect(dead_code)] +pub struct Response { + pub app: String, + pub version: String, +} + +impl Unpackable for Response { + fn unpack(buf: &mut R) -> Result + where + Self: Sized, + { + // format always 0x01 but don't insist on it + let _format_id = u8::unpack(buf)?; + + let app = String::unpack(buf)?; + let version = String::unpack(buf)?; + + // consume all extra bytes (nano x <-> nano s compatibility!) + while u8::unpack(buf).is_ok() { + // NOP + } + + Ok(Self { app, version }) + } +} + +pub fn exec(transport: &T) -> Result { + let cmd = APDUCommand { + cla: bolos::APDU_CLA_B0, + ins: bolos::APDUInstructions::GetAppVersionB0 as u8, + p1: bolos::APDU_P1, + p2: bolos::APDU_P2, + data: Vec::new(), + }; + helpers::exec::(transport, cmd) +} diff --git a/crates/iota-ledger/src/api/bolos/app_open.rs b/crates/iota-ledger/src/api/bolos/app_open.rs new file mode 100644 index 000000000..eaa9b3272 --- /dev/null +++ b/crates/iota-ledger/src/api/bolos/app_open.rs @@ -0,0 +1,54 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use ledger_transport::APDUCommand; + +use crate::{ + Transport, + api::{ + bolos, errors, helpers, + packable::{Error as PackableError, Packable, Write}, + }, +}; + +#[derive(Debug)] +pub struct Request { + pub app: String, +} + +impl Packable for Request { + fn packed_len(&self) -> usize { + self.app.packed_len() + } + + fn pack(&self, buf: &mut W) -> Result<(), PackableError> { + self.app.pack(buf)?; + Ok(()) + } +} + +pub fn exec(transport: &T, app: String) -> Result<(), errors::LedgerError> { + let req = Request { app }; + + let mut buf = Vec::new(); + let _ = req.pack(&mut buf); + + // string serializer stores a length byte that is unwanted here because + // the p3 parameter will be the length of the string and the data itself + // must not contain the length + buf.remove(0); + + let cmd = APDUCommand { + cla: bolos::APDU_CLA_E0, + ins: bolos::APDUInstructions::OpenAppE0 as u8, + p1: bolos::APDU_P1, + p2: bolos::APDU_P2, + data: buf, + }; + helpers::exec::(transport, cmd).map_err(|e| match e { + errors::LedgerError::Syscall(errors::SyscallError::InvalidCounter) => { + errors::LedgerError::AppNotFound + } + _ => e, + }) +} diff --git a/crates/iota-ledger/src/api/bolos/mod.rs b/crates/iota-ledger/src/api/bolos/mod.rs new file mode 100644 index 000000000..c18eb471e --- /dev/null +++ b/crates/iota-ledger/src/api/bolos/mod.rs @@ -0,0 +1,17 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod app_exit; +pub(crate) mod app_get_name; +pub(crate) mod app_open; + +pub(crate) const APDU_CLA_B0: u8 = 0xb0; +pub(crate) const APDU_CLA_E0: u8 = 0xe0; +pub(crate) const APDU_P1: u8 = 0x00; +pub(crate) const APDU_P2: u8 = 0x00; + +pub(crate) enum APDUInstructions { + GetAppVersionB0 = 0x01, + AppExitB0 = 0xa7, + OpenAppE0 = 0xd8, +} diff --git a/crates/iota-ledger/src/api/constants.rs b/crates/iota-ledger/src/api/constants.rs new file mode 100644 index 000000000..f6294fdcb --- /dev/null +++ b/crates/iota-ledger/src/api/constants.rs @@ -0,0 +1,14 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub enum APDUInstructions { + GetVersion = 0x00, + VerifyAddress = 0x01, + GetPublicKey = 0x02, + SignTransaction = 0x03, + Exit = 0xff, +} + +pub(crate) const APDU_CLA: u8 = 0x00; +pub(crate) const APDU_P1: u8 = 0x00; +pub(crate) const APDU_P2: u8 = 0x00; diff --git a/crates/iota-ledger/src/api/errors.rs b/crates/iota-ledger/src/api/errors.rs new file mode 100644 index 000000000..a65f60023 --- /dev/null +++ b/crates/iota-ledger/src/api/errors.rs @@ -0,0 +1,183 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +pub use crate::transport::{HidError, LedgerHIDError, LedgerTCPError}; + +/// APDU error codes including standard codes from the ledger SDK +#[derive(Debug, Clone, PartialEq, Eq)] +#[repr(u16)] +pub enum APDUErrorCode { + /// No error + Ok = 0x9000, + /// Wrong length + WrongLength = 0x6700, + /// Nothing received + NothingReceived = 0x6982, + /// User cancelled + UserCancelled = 0x6985, + /// Wrong data + WrongData = 0x6a80, + /// Function not supported + FunctionNotSupported = 0x6a81, + /// File not found + FileNotFound = 0x6a82, + /// Record not found + RecordNotFound = 0x6a83, + /// Not enough memory space + NotEnoughMemory = 0x6a84, + /// Wrong P1 P2 + WrongP1P2 = 0x6a86, + /// Unknown error + Unknown = 0x6d00, + /// Bad class + BadCla = 0x6e00, + /// Bad instruction + BadIns = 0x6e01, + /// Bad P1 P2 parameters + BadP1P2 = 0x6e02, + /// Bad length + BadLen = 0x6e03, + /// Device panic + Panic = 0xe000, + /// Device locked + DeviceLocked = 0x5515, + /// User denied the request + UserDenied = 0x5501, +} + +impl TryFrom for APDUErrorCode { + type Error = (); + + fn try_from(value: u16) -> Result { + match value { + 0x9000 => Ok(APDUErrorCode::Ok), + 0x6700 => Ok(APDUErrorCode::WrongLength), + 0x6982 => Ok(APDUErrorCode::NothingReceived), + 0x6985 => Ok(APDUErrorCode::UserCancelled), + 0x6a80 => Ok(APDUErrorCode::WrongData), + 0x6a81 => Ok(APDUErrorCode::FunctionNotSupported), + 0x6a82 => Ok(APDUErrorCode::FileNotFound), + 0x6a83 => Ok(APDUErrorCode::RecordNotFound), + 0x6a84 => Ok(APDUErrorCode::NotEnoughMemory), + 0x6a86 => Ok(APDUErrorCode::WrongP1P2), + 0x6d00 => Ok(APDUErrorCode::Unknown), + 0x6e00 => Ok(APDUErrorCode::BadCla), + 0x6e01 => Ok(APDUErrorCode::BadIns), + 0x6e02 => Ok(APDUErrorCode::BadP1P2), + 0x6e03 => Ok(APDUErrorCode::BadLen), + 0xe000 => Ok(APDUErrorCode::Panic), + 0x5515 => Ok(APDUErrorCode::DeviceLocked), + 0x5501 => Ok(APDUErrorCode::UserDenied), + _ => Err(()), + } + } +} + +#[derive(PartialEq, Debug)] +#[repr(u8)] +pub enum SyscallError { + InvalidParameter = 2, + Overflow, + Security, + InvalidCrc, + InvalidChecksum, + InvalidCounter, + NotSupported, + InvalidState, + Timeout, + Unspecified, +} + +#[derive(Error, Debug)] +pub enum LedgerError { + #[error( + "Address mismatch - connect the correct Ledger device or select the correct bip32 path" + )] + AddressMismatch, + + #[error("Device not ready - ensure the IOTA app is open on the Ledger device")] + DeviceNotReady, + + #[error("Device not found - connect the Ledger device")] + DeviceNotFound, + + #[error("Device locked - unlock the Ledger device")] + DeviceLocked, + + #[error("User refused the operation")] + UserRefused, + + #[error("Device panic")] + DevicePanic, + + #[error("App not found - ensure the IOTA app is installed on the Ledger device")] + AppNotFound, + + #[error("Syscall error: {0:?}")] + Syscall(SyscallError), + + #[error("APDU error: {0:?}")] + APDUError(APDUErrorCode), + + #[error("Unknown APDU error {0:?}")] + UnknownAPDUError(u16), + + #[error("Blocks protocol failed")] + BlocksProtocolFailed, + + #[error("Hid API error: {0}")] + HidError(#[from] HidError), + + #[error("HID Transport error: {0}")] + LedgerHID(#[from] LedgerHIDError), + + #[error("TCP Transport error: {0}")] + LedgerTCP(#[from] LedgerTCPError), + + #[error("Serialization error")] + Serialization, +} + +impl LedgerError { + pub fn get_error(rc: u16) -> Option { + // First try to match APDU error codes (including ledger SDK standard codes) + if let Ok(apdu_error) = APDUErrorCode::try_from(rc) { + return match apdu_error { + APDUErrorCode::Ok => None, // No error, return None + APDUErrorCode::DeviceLocked => Some(LedgerError::DeviceLocked), + APDUErrorCode::Panic => Some(LedgerError::DevicePanic), + APDUErrorCode::UserCancelled | APDUErrorCode::UserDenied => { + Some(LedgerError::UserRefused) + } + APDUErrorCode::BadCla | APDUErrorCode::BadIns | APDUErrorCode::BadP1P2 => { + Some(LedgerError::DeviceNotReady) + } + _ => Some(LedgerError::APDUError(apdu_error)), + }; + } + + // Handle syscall errors range in the APDU error codes + let e = match rc { + rc if (0x6800..=0x680b).contains(&rc) => { + let value = (rc - 0x6800) as u8; + let syscall_error = match value { + 2 => SyscallError::InvalidParameter, + 3 => SyscallError::Overflow, + 4 => SyscallError::Security, + 5 => SyscallError::InvalidCrc, + 6 => SyscallError::InvalidChecksum, + 7 => SyscallError::InvalidCounter, + 8 => SyscallError::NotSupported, + 9 => SyscallError::InvalidState, + 10 => SyscallError::Timeout, + _ => SyscallError::Unspecified, + }; + LedgerError::Syscall(syscall_error) + } + _ => LedgerError::UnknownAPDUError(rc), + }; + Some(e) + } +} diff --git a/crates/iota-ledger/src/api/exit.rs b/crates/iota-ledger/src/api/exit.rs new file mode 100644 index 000000000..104e9c96e --- /dev/null +++ b/crates/iota-ledger/src/api/exit.rs @@ -0,0 +1,20 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use ledger_transport::APDUCommand; + +use crate::{ + Transport, + api::{constants, errors, helpers}, +}; + +pub fn exec(transport: &T) -> Result<(), errors::LedgerError> { + let cmd = APDUCommand { + cla: constants::APDU_CLA, + ins: constants::APDUInstructions::Exit as u8, + p1: constants::APDU_P1, + p2: constants::APDU_P2, + data: Vec::new(), + }; + helpers::exec::(transport, cmd) +} diff --git a/crates/iota-ledger/src/api/get_public_key.rs b/crates/iota-ledger/src/api/get_public_key.rs new file mode 100644 index 000000000..744976bfd --- /dev/null +++ b/crates/iota-ledger/src/api/get_public_key.rs @@ -0,0 +1,67 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// TODO public key maybe needs to be wrapped in enum? + +use fastcrypto::ed25519::ED25519_PUBLIC_KEY_LENGTH; +use iota_types::{ + Address, + crypto::{Ed25519PublicKey, PublicKeyExt}, +}; + +use crate::{ + Transport, + api::{ + constants::APDUInstructions, + errors, helpers, + packable::{Error as PackableError, Read, Unpackable}, + }, + packable_vec, +}; + +pub struct PublicKeyResult { + pub public_key: Ed25519PublicKey, + pub address: Address, +} + +impl Unpackable for PublicKeyResult { + fn unpack(buf: &mut R) -> Result + where + Self: Sized, + { + if u8::unpack(buf)? != ED25519_PUBLIC_KEY_LENGTH as u8 { + return Err(PackableError::InvalidAnnouncedLen); + } + let mut key = [0_u8; ED25519_PUBLIC_KEY_LENGTH]; + buf.read_exact(&mut key)?; + let public_key = + Ed25519PublicKey::from_bytes(&key).map_err(|_| PackableError::InvalidData)?; + + if u8::unpack(buf)? != 32 { + return Err(PackableError::InvalidAnnouncedLen); + } + let mut address_buffer = [0_u8; 32]; + buf.read_exact(&mut address_buffer)?; + let address = + Address::from_bytes(address_buffer).map_err(|_| PackableError::InvalidData)?; + + Ok(Self { + public_key, + address, + }) + } +} + +pub fn exec( + transport: &T, + bip32: &bip32::DerivationPath, + show: bool, +) -> Result { + let payload: helpers::PackedBIP32Path = bip32.into(); + let ins = if show { + APDUInstructions::VerifyAddress + } else { + APDUInstructions::GetPublicKey + }; + helpers::send_with_blocks(transport, ins, packable_vec![payload], None) +} diff --git a/crates/iota-ledger/src/api/get_version.rs b/crates/iota-ledger/src/api/get_version.rs new file mode 100644 index 000000000..892181a12 --- /dev/null +++ b/crates/iota-ledger/src/api/get_version.rs @@ -0,0 +1,67 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use core::fmt; + +use crate::{ + Transport, + api::{ + constants, errors, helpers, + packable::{Error as PackableError, Packable, Read, Unpackable, Write}, + }, +}; + +#[derive(Debug)] +pub struct Version { + pub major: u8, + pub minor: u8, + pub patch: u8, +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +impl Unpackable for Version { + fn unpack(buf: &mut R) -> Result + where + Self: Sized, + { + let major = u8::unpack(buf)?; + let minor = u8::unpack(buf)?; + let patch = u8::unpack(buf)?; + + // consume all extra bytes (app name) + while u8::unpack(buf).is_ok() { + // NOP + } + + Ok(Self { + major, + minor, + patch, + }) + } +} + +struct VersionRequest {} +impl Packable for VersionRequest { + fn packed_len(&self) -> usize { + 0 // No data to pack + } + + fn pack(&self, _buf: &mut W) -> Result<(), PackableError> { + Ok(()) // No data to pack + } +} + +pub fn exec(transport: &T) -> Result { + helpers::send_with_blocks::( + transport, + constants::APDUInstructions::GetVersion, + vec![Box::new(VersionRequest {})], + None, + ) +} diff --git a/crates/iota-ledger/src/api/helpers.rs b/crates/iota-ledger/src/api/helpers.rs new file mode 100644 index 000000000..6ebc287fb --- /dev/null +++ b/crates/iota-ledger/src/api/helpers.rs @@ -0,0 +1,231 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; + +use fastcrypto::hash::{Digest, HashFunction, Sha256}; +use ledger_transport::APDUCommand; + +use crate::{ + Transport, + api::{ + constants, + errors::{self}, + packable::{Error as PackableError, Packable, PackableObject, Read, Unpackable, Write}, + }, +}; + +/// Macro to create a vector of boxed packable objects +/// Usage: packable_vec![payload1, payload2, payload3] +#[macro_export] +macro_rules! packable_vec { + ($($payload:expr),* $(,)?) => { + vec![$(Box::new($payload) as Box),*] + }; +} + +#[derive(Default, Debug)] +pub(crate) struct PackedBIP32Path { + data: Vec, +} + +impl Packable for PackedBIP32Path { + fn packed_len(&self) -> usize { + self.data.len() + } + + fn pack(&self, buf: &mut W) -> Result<(), PackableError> { + buf.write_all(&self.data)?; + Ok(()) + } +} + +impl From<&bip32::DerivationPath> for PackedBIP32Path { + fn from(path: &bip32::DerivationPath) -> Self { + let mut data = Vec::with_capacity(path.len() * 4 + 1); + data.push(path.len() as u8); + for index in path.iter() { + data.extend_from_slice(&index.0.to_le_bytes()); + } + PackedBIP32Path { data } + } +} + +#[derive(Debug, Clone, Copy)] +enum LedgerToHost { + ResultAccumulating = 0, + ResultFinal = 1, + GetChunk = 2, + PutChunk = 3, +} + +impl TryFrom for LedgerToHost { + type Error = PackableError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(LedgerToHost::ResultAccumulating), + 1 => Ok(LedgerToHost::ResultFinal), + 2 => Ok(LedgerToHost::GetChunk), + 3 => Ok(LedgerToHost::PutChunk), + _ => Err(PackableError::InvalidVariant), + } + } +} + +#[derive(Debug, Clone, Copy)] +enum HostToLedger { + Start = 0, + GetChunkResponseSuccess = 1, + GetChunkResponseFailure = 2, + PutChunkResponse = 3, + ResultAccumulatingResponse = 4, +} + +impl HostToLedger { + fn as_vec(self) -> Vec { + vec![self as u8] + } +} + +#[derive(Debug)] +struct BlockResponse { + instruction: LedgerToHost, + payload: Vec, +} + +impl BlockResponse { + fn chunk_hash(&self) -> Result, errors::LedgerError> { + match self.instruction { + LedgerToHost::GetChunk => { + if self.payload.len() >= 32 { + let mut hash = [0u8; 32]; + hash.copy_from_slice(&self.payload[..32]); + return Ok(Digest::<32>::new(hash)); + } + Err(errors::LedgerError::BlocksProtocolFailed) + } + LedgerToHost::PutChunk => Ok(Sha256::digest(&self.payload)), + _ => Err(errors::LedgerError::BlocksProtocolFailed), + } + } +} + +impl Unpackable for BlockResponse { + fn unpack(buf: &mut R) -> Result { + let instruction = + LedgerToHost::try_from(u8::unpack(buf)?).map_err(|_| PackableError::InvalidVariant)?; + + let mut payload = Vec::new(); + buf.read_to_end(&mut payload)?; + Ok(Self { + instruction, + payload, + }) + } +} + +pub(crate) fn send_with_blocks( + transport: &T, + ins: constants::APDUInstructions, + payloads: Vec>, + extra_data: Option, Vec>>, +) -> Result { + const CHUNK_SIZE: usize = 180; + + let mut data = extra_data.unwrap_or_default(); + let mut parameter_list: Vec> = Vec::new(); + + for payload in payloads { + let packed = payload + .pack_as_vec() + .map_err(|_| errors::LedgerError::Serialization)?; + let chunks: Vec<&[u8]> = packed.chunks(CHUNK_SIZE).collect(); + + let mut last_hash: Digest<32> = Digest::<32>::new([0u8; 32]); + for chunk in chunks.iter().rev() { + let mut linked_chunk = Vec::with_capacity(32 + chunk.len()); + linked_chunk.extend(last_hash.to_vec()); + linked_chunk.extend_from_slice(chunk); + + last_hash = Sha256::digest(&linked_chunk); + data.insert(last_hash, linked_chunk); + } + + parameter_list.push(last_hash); + } + + let mut initial_payload = vec![HostToLedger::Start as u8]; + for param in parameter_list { + initial_payload.extend(¶m.to_vec()); + } + + handle_blocks_protocol(transport, ins, initial_payload, data) +} + +fn handle_blocks_protocol( + transport: &T, + ins: constants::APDUInstructions, + mut payload: Vec, + mut data: HashMap, Vec>, +) -> Result { + let mut result = Vec::new(); + let ins = ins as u8; + + loop { + let cmd = APDUCommand { + cla: constants::APDU_CLA, + ins, + p1: constants::APDU_P1, + p2: constants::APDU_P2, + data: payload, + }; + + let rv = exec::(transport, cmd)?; + + match rv.instruction { + LedgerToHost::ResultAccumulating => { + result.extend(rv.payload); + payload = HostToLedger::ResultAccumulatingResponse.as_vec(); + } + LedgerToHost::ResultFinal => { + result.extend(rv.payload); + break; + } + LedgerToHost::GetChunk => { + let key = rv.chunk_hash()?; + payload = if let Some(chunk) = data.get(&key) { + let mut resp = HostToLedger::GetChunkResponseSuccess.as_vec(); + resp.extend_from_slice(chunk); + resp + } else { + vec![HostToLedger::GetChunkResponseFailure as u8] + }; + } + LedgerToHost::PutChunk => { + data.insert(rv.chunk_hash()?, rv.payload); + payload = HostToLedger::PutChunkResponse.as_vec(); + } + } + } + + let res = U::unpack(&mut &result[..]).map_err(|_| errors::LedgerError::Serialization)?; + Ok(res) +} + +pub(crate) fn exec( + transport: &T, + cmd: APDUCommand>, +) -> Result { + transport.exchange(&cmd).and_then(|resp| { + let api_error = errors::LedgerError::get_error(resp.retcode()); + match api_error { + None => { + let res = U::unpack(&mut &resp.data()[..]) + .map_err(|_| errors::LedgerError::Serialization)?; + Ok(res) + } + Some(e) => Err(e), + } + }) +} diff --git a/crates/iota-ledger/src/api/mod.rs b/crates/iota-ledger/src/api/mod.rs new file mode 100644 index 000000000..c3b162b45 --- /dev/null +++ b/crates/iota-ledger/src/api/mod.rs @@ -0,0 +1,13 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub mod constants; +pub mod errors; + +pub(crate) mod bolos; +pub(crate) mod exit; +pub(crate) mod get_public_key; +pub(crate) mod get_version; +pub(crate) mod helpers; +pub(crate) mod packable; +pub(crate) mod sign_transaction; diff --git a/crates/iota-ledger/src/api/packable.rs b/crates/iota-ledger/src/api/packable.rs new file mode 100644 index 000000000..1fc2b4b61 --- /dev/null +++ b/crates/iota-ledger/src/api/packable.rs @@ -0,0 +1,131 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub use std::io::{Read, Write}; +use std::str; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("I/O error happened: {0}.")] + Io(#[from] std::io::Error), + #[error("Invalid variant read.")] + InvalidVariant, + #[error("Invalid data read.")] + InvalidData, + #[error("Invalid Utf8 string read.")] + InvalidUtf8String, + #[error("Invalid announced len.")] + InvalidAnnouncedLen, + #[error("String too long.")] + StringTooLong, +} + +pub trait Packable { + fn packed_len(&self) -> usize; + + fn pack(&self, buf: &mut W) -> Result<(), Error>; +} + +pub trait PackableObject { + fn pack_as_vec(&self) -> Result, Error>; +} + +// Blanket implementation for all types that implement Packable +impl PackableObject for T { + fn pack_as_vec(&self) -> Result, Error> { + let mut vec = Vec::with_capacity(self.packed_len()); + self.pack(&mut vec)?; + Ok(vec) + } +} + +pub trait Unpackable { + fn unpack(buf: &mut R) -> Result + where + Self: Sized; +} + +impl Packable for () { + fn packed_len(&self) -> usize { + 0 + } + + fn pack(&self, _buf: &mut W) -> Result<(), Error> { + Ok(()) + } +} + +impl Unpackable for () { + fn unpack(_buf: &mut R) -> Result + where + Self: Sized, + { + Ok(()) + } +} + +macro_rules! impl_packable_for_num { + ($ty:ident) => { + impl Packable for $ty { + fn packed_len(&self) -> usize { + std::mem::size_of::<$ty>() + } + + fn pack(&self, buf: &mut W) -> Result<(), Error> { + buf.write_all(self.to_le_bytes().as_ref())?; + Ok(()) + } + } + impl Unpackable for $ty { + fn unpack(buf: &mut R) -> Result { + let mut bytes = [0; std::mem::size_of::<$ty>()]; + buf.read_exact(&mut bytes)?; + Ok($ty::from_le_bytes(bytes)) + } + } + }; +} + +impl Packable for String { + fn packed_len(&self) -> usize { + 0u8.packed_len() + self.chars().count() + } + + fn pack(&self, buf: &mut W) -> Result<(), Error> { + if self.chars().count() > 255 { + return Err(Error::StringTooLong); + } + let bytes = self.as_bytes(); + (bytes.len() as u8).pack(buf)?; + buf.write_all(bytes)?; + Ok(()) + } +} + +impl Unpackable for String { + fn unpack(buf: &mut R) -> Result + where + Self: Sized, + { + let l = u8::unpack(buf)? as usize; + let mut v = vec![0u8; l]; + buf.read_exact(&mut v)?; + match str::from_utf8(&v) { + Ok(s) => Ok(s.to_owned()), + Err(_) => Err(Error::InvalidUtf8String), + } + } +} + +impl_packable_for_num!(i8); +impl_packable_for_num!(u8); +impl_packable_for_num!(i16); +impl_packable_for_num!(u16); +impl_packable_for_num!(i32); +impl_packable_for_num!(u32); +impl_packable_for_num!(i64); +impl_packable_for_num!(u64); +impl_packable_for_num!(i128); +impl_packable_for_num!(u128); diff --git a/crates/iota-ledger/src/api/sign_transaction.rs b/crates/iota-ledger/src/api/sign_transaction.rs new file mode 100644 index 000000000..690195e94 --- /dev/null +++ b/crates/iota-ledger/src/api/sign_transaction.rs @@ -0,0 +1,97 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + Transport, + api::{ + constants, errors, helpers, + helpers::PackedBIP32Path, + packable::{Error as PackableError, Packable, Read, Unpackable, Write}, + }, + packable_vec, +}; + +#[derive(Debug)] +pub struct SignatureBytes { + pub bytes: Vec, +} + +impl Unpackable for SignatureBytes { + fn unpack(buf: &mut R) -> Result + where + Self: Sized, + { + let mut bytes = Vec::new(); + buf.read_to_end(&mut bytes)?; + Ok(Self { bytes }) + } +} + +struct TransactionData { + transaction: Vec, +} + +impl Packable for TransactionData { + fn packed_len(&self) -> usize { + 0_u32.packed_len() + self.transaction.len() + } + + fn pack(&self, buf: &mut W) -> Result<(), PackableError> { + (self.transaction.len() as u32).pack(buf)?; + buf.write_all(&self.transaction)?; + Ok(()) + } +} + +struct TransactionObjects { + objects: Vec>, +} + +impl Packable for TransactionObjects { + fn packed_len(&self) -> usize { + 0_u32.packed_len() + + self + .objects + .iter() + .map(|o| 0u32.packed_len() + o.len()) + .sum::() + } + + fn pack(&self, buf: &mut W) -> Result<(), PackableError> { + // Pack the number of objects + (self.objects.len() as u32).pack(buf)?; + + // Pack each object + for object in &self.objects { + // Pack the length of the object + (object.len() as u32).pack(buf)?; + // Write the object data + buf.write_all(object)?; + } + Ok(()) + } +} + +pub fn exec( + transport: &T, + path: &bip32::DerivationPath, + transaction: Vec, + objects: Vec>, +) -> Result { + let payloads = if objects.is_empty() { + packable_vec![TransactionData { transaction }, PackedBIP32Path::from(path)] + } else { + packable_vec![ + TransactionData { transaction }, + PackedBIP32Path::from(path), + TransactionObjects { objects } + ] + }; + + helpers::send_with_blocks( + transport, + constants::APDUInstructions::SignTransaction, + payloads, + None, + ) +} diff --git a/crates/iota-ledger/src/lib.rs b/crates/iota-ledger/src/lib.rs new file mode 100644 index 000000000..5f93b47af --- /dev/null +++ b/crates/iota-ledger/src/lib.rs @@ -0,0 +1,232 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// TODO signature maybe needs to be wrapped in Signature enum? + +use std::{thread, time, vec}; + +use hex::ToHex; +use tracing::debug; +mod transport; +use iota_types::{ + Address, Object, + crypto::{Ed25519PublicKey, Ed25519Signature, Intent, IntentMessage, SignatureScheme}, +}; +use serde::Serialize; +use transport::{APDUAnswer, APDUCommand, LedgerTransport}; + +pub use crate::api::errors::LedgerError; +mod api; + +pub use crate::api::{get_public_key::PublicKeyResult, get_version::Version}; +use crate::{ + api::{bolos, exit, get_public_key, sign_transaction}, + transport::Transport, +}; + +pub struct Ledger { + transport: LedgerTransport, +} + +#[derive(Debug)] +pub struct SignedTransaction { + pub signature: Ed25519Signature, + pub public_key: Ed25519PublicKey, +} + +const IOTA_APP_NAME: &str = "IOTA"; +const DASHBOARD_APP_NAME: &str = "BOLOS"; + +impl Ledger { + pub fn new_with_default() -> Result { + let transport = if std::env::var("LEDGER_SIMULATOR").is_ok() { + LedgerTransport::new_simulator()? + } else { + LedgerTransport::new_native_hid()? + }; + Ok(crate::Ledger::new(transport)) + } + + pub fn new_with_native_hid() -> Result { + Ok(crate::Ledger::new(LedgerTransport::new_native_hid()?)) + } + + pub fn new_with_simulator() -> Result { + Ok(crate::Ledger::new(LedgerTransport::new_simulator()?)) + } + + fn new(transport: LedgerTransport) -> Self { + Ledger { transport } + } + + fn is_simulator(&self) -> bool { + matches!(&self.transport, LedgerTransport::Simulator(_)) + } + + fn recreate_transport(&mut self) -> Result<(), LedgerError> { + thread::sleep(time::Duration::from_secs(3)); + match &self.transport { + LedgerTransport::Simulator(_) => { + self.transport = LedgerTransport::new_simulator()?; + } + LedgerTransport::NativeHID(_) => { + self.transport = LedgerTransport::new_native_hid()?; + } + } + Ok(()) + } + + /// Check if the IOTA app is open on the Ledger device + pub fn is_app_open(&self) -> Result { + let app = bolos::app_get_name::exec(self)?; + Ok(app.app == IOTA_APP_NAME) + } + + /// Only works if dashboard is open + /// This will re-create the transport after opening the app + fn bolos_open_app(&mut self) -> Result<(), LedgerError> { + if self.is_app_open()? { + return Ok(()); + } + bolos::app_open::exec(self, IOTA_APP_NAME.to_string())?; + self.recreate_transport() + } + + /// Close current opened app + /// Only works if an app is open + /// This will re-create the transport after closing the app + fn bolos_exit_app(&mut self) -> Result<(), LedgerError> { + bolos::app_exit::exec(self)?; + self.recreate_transport() + } + + /// Ensure the IOTA app is open + /// If the app is not open, it will open it + /// If another app is open, it will close it first + /// This will re-create the transport after closing the app + pub fn ensure_app_is_open(&mut self) -> Result<(), LedgerError> { + if self.is_simulator() { + return Ok(()); + } + + match bolos::app_get_name::exec(self)?.app.as_str() { + IOTA_APP_NAME => { + // App is already open + return Ok(()); + } + DASHBOARD_APP_NAME => { + // Dashboard is open, we need to open the IOTA app + self.bolos_open_app()?; + } + _ => { + // Some other app is open, we need to close it first + self.bolos_exit_app()?; + self.bolos_open_app()?; + } + } + Ok(()) + } + + pub fn get_version(&self) -> Result { + let version = crate::api::get_version::exec(self)?; + Ok(version) + } + + pub fn verify_address( + &self, + bip32: &bip32::DerivationPath, + ) -> Result { + get_public_key::exec(self, bip32, true) + } + + pub fn get_public_key( + &self, + bip32: &bip32::DerivationPath, + ) -> Result { + get_public_key::exec(self, bip32, false) + } + + pub fn get_signature_scheme(&self) -> SignatureScheme { + SignatureScheme::Ed25519 + } + + // TODO + pub fn sign_intent_unchecked( + &self, + bip32: &bip32::DerivationPath, + intent: Intent, + msg: &T, + objects: Vec, + ) -> Result { + let version = self.get_version()?; + let key_response = self.get_public_key(bip32)?; + + let intent_msg = IntentMessage::new(intent, msg); + let intent_bytes = bcs::to_bytes(&intent_msg).map_err(|_| LedgerError::Serialization)?; + + let signature = (if version.major > 0 { + let bcs_objects: Vec> = objects + .iter() + .map(|o| bcs::to_bytes(&o).map_err(|_| LedgerError::Serialization)) + .collect::>()?; + // If the major version is greater than 0, we assume it supports clear signing + sign_transaction::exec(self, bip32, intent_bytes, bcs_objects) + } else { + sign_transaction::exec(self, bip32, intent_bytes, vec![]) + })?; + + // TODO do we need these? + let mut signature_bytes: Vec = Vec::new(); + signature_bytes.extend_from_slice(&[self.get_signature_scheme() as u8]); + signature_bytes.extend_from_slice(&signature.bytes); + signature_bytes.extend_from_slice(key_response.public_key.as_ref()); + + Ok(SignedTransaction { + signature: Ed25519Signature::from_bytes(&signature.bytes) + .map_err(|_| LedgerError::Serialization)? + .into(), + public_key: key_response.public_key, + }) + } + + // TODO why address? + pub fn sign_intent( + &self, + bip32: &bip32::DerivationPath, + address: &Address, + intent: Intent, + msg: &T, + objects: Vec, + ) -> Result { + let key_response = self.get_public_key(bip32)?; + + if key_response.address != *address { + return Err(LedgerError::AddressMismatch); + } + + self.sign_intent_unchecked(bip32, intent, msg, objects) + } + + /// Close the IOTA app from within + /// This will re-create the transport after closing the app + pub fn exit_app(&mut self) -> Result<(), LedgerError> { + exit::exec(self)?; + self.recreate_transport() + } +} + +impl Transport for Ledger { + fn exchange( + &self, + apdu_command: &APDUCommand>, + ) -> Result>, LedgerError> { + debug!( + "Exchanging APDU command: {}", + apdu_command.serialize().encode_hex::() + ); + match &self.transport { + LedgerTransport::Simulator(tcp) => Ok(tcp.exchange(apdu_command)?), + LedgerTransport::NativeHID(hid) => Ok(hid.exchange(apdu_command)?), + } + } +} diff --git a/crates/iota-ledger/src/transport/mod.rs b/crates/iota-ledger/src/transport/mod.rs new file mode 100644 index 000000000..ae432cd82 --- /dev/null +++ b/crates/iota-ledger/src/transport/mod.rs @@ -0,0 +1,44 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub use ledger_transport::{APDUAnswer, APDUCommand}; +pub use ledger_transport_hid::LedgerHIDError; +use ledger_transport_hid::TransportNativeHID; + +use crate::LedgerError; +mod tcp; +pub use hidapi::HidError; +pub use tcp::LedgerTCPError; +use tcp::TransportTCP; + +#[allow(clippy::upper_case_acronyms)] +pub(crate) enum LedgerTransport { + Simulator(TransportTCP), + NativeHID(TransportNativeHID), +} + +pub(crate) trait Transport { + fn exchange( + &self, + apdu_command: &APDUCommand>, + ) -> Result>, LedgerError>; +} + +impl LedgerTransport { + pub(crate) fn new_simulator() -> Result { + Ok(LedgerTransport::Simulator(TransportTCP::new( + "127.0.0.1", + 9999, + ))) + } + + pub(crate) fn new_native_hid() -> Result { + let api = hidapi::HidApi::new()?; + Ok(LedgerTransport::NativeHID( + TransportNativeHID::new(&api).map_err(|e| match e { + LedgerHIDError::DeviceNotFound => LedgerError::DeviceNotFound, + _ => e.into(), + })?, + )) + } +} diff --git a/crates/iota-ledger/src/transport/tcp.rs b/crates/iota-ledger/src/transport/tcp.rs new file mode 100644 index 000000000..f04b0d28e --- /dev/null +++ b/crates/iota-ledger/src/transport/tcp.rs @@ -0,0 +1,73 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + io::{Read, Write}, + net::TcpStream, +}; + +use ledger_transport::{APDUAnswer, APDUCommand}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum LedgerTCPError { + #[error("Ledger connect error")] + ConnectFailed, + #[error("Invalid TCP response error")] + InvalidResponse, + #[error("Ledger inner error")] + Inner, +} + +#[derive(Debug)] +pub struct TransportTCP { + url: String, +} + +impl TransportTCP { + pub fn new(host: &str, port: u16) -> Self { + Self { + url: format!("{host}:{port}"), + } + } + + fn request(raw_command: &[u8], stream: &mut TcpStream) -> Result, std::io::Error> { + // store length as 32bit big endian into array + let send_length_bytes = (raw_command.len() as u32).to_be_bytes(); + + // first send number of bytes + stream.write_all(&send_length_bytes[..])?; + + // then send bytes + stream.write_all(raw_command)?; + + let mut rcv_length_bytes = [0u8; 4]; + + // first read number of bytes + stream.read_exact(&mut rcv_length_bytes)?; + + // convert bytes from big endian (+2 for return code) + let rcv_length = u32::from_be_bytes(rcv_length_bytes) + 2; + + let mut buf = vec![0u8; rcv_length as usize]; + stream.read_exact(&mut buf)?; + Ok(buf) + } + + pub fn exchange( + &self, + command: &APDUCommand>, + ) -> Result>, LedgerTCPError> { + let raw_command = command.serialize(); + + let mut stream = + TcpStream::connect(&self.url).map_err(|_| LedgerTCPError::ConnectFailed)?; + + let raw_answer = + TransportTCP::request(&raw_command, &mut stream).map_err(|_| LedgerTCPError::Inner)?; + let answer = + APDUAnswer::from_answer(raw_answer).map_err(|_| LedgerTCPError::InvalidResponse)?; + + Ok(answer) + } +} diff --git a/crates/iota-sdk-ffi/Cargo.toml b/crates/iota-sdk-ffi/Cargo.toml index b5e0d139b..d5ea2b9b5 100644 --- a/crates/iota-sdk-ffi/Cargo.toml +++ b/crates/iota-sdk-ffi/Cargo.toml @@ -17,6 +17,7 @@ crate-type = ["lib", "cdylib"] [dependencies] async-trait = "0.1.89" base64ct = { workspace = true, features = ["alloc", "std"] } +bip32 = "0.5.3" bcs.workspace = true derive_more = { workspace = true, features = ["from", "deref", "display"] } hex.workspace = true @@ -30,3 +31,4 @@ tokio = { workspace = true, features = ["time"] } uniffi = { version = "0.29", features = ["cli", "tokio"] } iota-sdk = { version = "3.0.0-alpha.1", path = "../iota-sdk" } +iota-ledger-signer.workspace = true diff --git a/crates/iota-sdk-ffi/src/graphql.rs b/crates/iota-sdk-ffi/src/graphql.rs index 5893e193f..13f2a2c9f 100644 --- a/crates/iota-sdk-ffi/src/graphql.rs +++ b/crates/iota-sdk-ffi/src/graphql.rs @@ -52,7 +52,7 @@ pub enum WaitForTx { /// The GraphQL client for interacting with the IOTA blockchain. #[derive(uniffi::Object)] -pub struct GraphQLClient(RwLock); +pub struct GraphQLClient(pub RwLock); impl GraphQLClient { pub fn inner(&self) -> &RwLock { diff --git a/crates/iota-sdk-ffi/src/ledger.rs b/crates/iota-sdk-ffi/src/ledger.rs new file mode 100644 index 000000000..e51b34f64 --- /dev/null +++ b/crates/iota-sdk-ffi/src/ledger.rs @@ -0,0 +1,136 @@ +// Copyright (c) 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{str::FromStr, sync::Arc}; + +use iota_sdk::types::crypto::SignatureScheme; + +// use crate::graphql::GraphQLClient; +use crate::types::{ + address::Address, + crypto::Ed25519PublicKey, + signature::{SimpleSignature, UserSignature}, + transaction::Transaction, +}; + +#[derive(uniffi::Object)] +pub struct LedgerSigner(pub iota_ledger_signer::LedgerSigner); + +#[derive(Debug, derive_more::Display, uniffi::Object)] +pub struct LedgerSignerError(iota_ledger_signer::LedgerSignerError); + +#[uniffi::export] +impl LedgerSigner { + #[uniffi::constructor] + pub fn new_with_default( + path: String, + // TODO + // client: Arc, + ) -> Result { + // TODO unwrap + let collect = bip32::DerivationPath::from_str(&path).unwrap(); + // .into_iter() + // .map(|c| c.0) + // .collect::>(); + let path = collect; + + Ok(Self( + iota_ledger_signer::LedgerSigner::new_with_default( + path, // TODO unwrap + None, + ) + .map_err(LedgerSignerError)?, + )) + } + + // pub fn new(ledger: Ledger, path: bip32::DerivationPath, client: + // Option) -> Self { LedgerSigner { + // ledger, + // path, + // client, + // } + // } + + // pub fn get_signature_scheme(&self) -> SignatureScheme { + // self.0.get_signature_scheme() + // } + + pub fn get_address(&self) -> Result { + Ok(self + .0 + .get_address() + .map(Address::from) + .map_err(LedgerSignerError)?) + } + + pub fn get_public_key(&self) -> Result { + Ok(self + .0 + .get_public_key() + .map(Ed25519PublicKey::from) + .map_err(LedgerSignerError)?) + } + + pub async fn sign_transaction( + &self, + transaction: &Transaction, + ) -> Result { + println!("HELLO"); + let signature = self + .0 + .sign_transaction_unchecked(&transaction.0) + .await + .map_err(LedgerSignerError)?; + + println!("HELLO {signature:?}"); + + Ok(UserSignature::new_simple(&SimpleSignature::new_ed25519( + &(signature.signature.into()), + &(signature.public_key.into()), + ))) + } + + // 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-sdk-ffi/src/lib.rs b/crates/iota-sdk-ffi/src/lib.rs index 06752abc3..13b3c8242 100644 --- a/crates/iota-sdk-ffi/src/lib.rs +++ b/crates/iota-sdk-ffi/src/lib.rs @@ -17,6 +17,7 @@ pub mod crypto; pub mod error; pub mod faucet; pub mod graphql; +pub mod ledger; pub mod transaction_builder; pub mod types; pub mod uniffi_helpers; diff --git a/crates/iota-sdk-ffi/src/transaction_builder/signer.rs b/crates/iota-sdk-ffi/src/transaction_builder/signer.rs index 14058a41c..5eee1f0cf 100644 --- a/crates/iota-sdk-ffi/src/transaction_builder/signer.rs +++ b/crates/iota-sdk-ffi/src/transaction_builder/signer.rs @@ -11,6 +11,7 @@ use crate::{ simple::SimpleKeypair, }, error::Result, + ledger::LedgerSigner, types::{signature::UserSignature, transaction::Transaction}, }; @@ -75,6 +76,21 @@ impl TransactionSignerFn for SimpleKeypair { } } +// #[async_trait::async_trait] +// impl TransactionSignerFn for LedgerSigner { +// async fn sign(&self, transaction: Arc) -> +// Result { let signature = self +// .0 +// .sign_transaction_unchecked(&transaction.0) +// .await? +// .signature; + +// Ok(TransactionSignerFnOutput { +// signature: Arc::new(signature.into()), +// }) +// } +// } + /// An async signer implementation which wraps a `TransactionSignerFn` /// definition, which can be used to sign a transaction with a callback. #[derive(uniffi::Object)]