diff --git a/Package.swift b/Package.swift index 0555e2c..66a316b 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription import Foundation let tag = "v0.6.0" -let checksum = "7cb8c8c49221d991f7cbe71c73dcad8e7ceb4d8ad410d68d7a781eb70e04fbdf" +let checksum = "29e0b2bae05b5b4f8538cddaa21adac44e2aaf81822939b87e43be224e71bc75" let url = "https://github.com/synonymdev/bitkit-core/releases/download/\(tag)/BitkitCore.xcframework.zip" let localBinary = ProcessInfo.processInfo.environment["BITKIT_CORE_LOCAL"] == "1" diff --git a/bindings/ios/bitkitcore.swift b/bindings/ios/bitkitcore.swift index 65c4e4f..c31a327 100644 --- a/bindings/ios/bitkitcore.swift +++ b/bindings/ios/bitkitcore.swift @@ -2563,6 +2563,223 @@ public func FfiConverterTypeUrDecoder_lower(_ value: UrDecoder) -> UnsafeMutable +public protocol UsdtDepositClientProtocol: AnyObject, Sendable { + + func detail(depositId: String, offset: UInt32, mnemonic: String, passphrase: String?) async throws -> UsdtDepositDetail + + func history(offset: UInt32, mnemonic: String, passphrase: String?) async throws -> UsdtDepositPage + + func networks() async throws -> [UsdtDepositNetwork] + + func receive(network: UsdtDepositNetwork, amount: UInt64, mnemonic: String, passphrase: String?) async throws -> UsdtDepositAddress + + func requestRefund(depositId: String, offset: UInt32, refundAddress: String, network: UsdtDepositNetwork, mnemonic: String, passphrase: String?) async throws + +} +open class UsdtDepositClient: UsdtDepositClientProtocol, @unchecked Sendable { + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public struct NoPointer { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + // This constructor can be used to instantiate a fake object. + // - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. + // + // - Warning: + // Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash. +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public init(noPointer: NoPointer) { + self.pointer = nil + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + return try! rustCall { uniffi_bitkitcore_fn_clone_usdtdepositclient(self.pointer, $0) } + } +public convenience init(address: String, serviceUrl: String)throws { + let pointer = + try rustCallWithError(FfiConverterTypeUsdtError_lift) { + uniffi_bitkitcore_fn_constructor_usdtdepositclient_new( + FfiConverterString.lower(address), + FfiConverterString.lower(serviceUrl),$0 + ) +} + self.init(unsafeFromRawPointer: pointer) +} + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_bitkitcore_fn_free_usdtdepositclient(pointer, $0) } + } + + + + +open func detail(depositId: String, offset: UInt32, mnemonic: String, passphrase: String?)async throws -> UsdtDepositDetail { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_bitkitcore_fn_method_usdtdepositclient_detail( + self.uniffiClonePointer(), + FfiConverterString.lower(depositId),FfiConverterUInt32.lower(offset),FfiConverterString.lower(mnemonic),FfiConverterOptionString.lower(passphrase) + ) + }, + pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, + completeFunc: ffi_bitkitcore_rust_future_complete_rust_buffer, + freeFunc: ffi_bitkitcore_rust_future_free_rust_buffer, + liftFunc: FfiConverterTypeUsdtDepositDetail_lift, + errorHandler: FfiConverterTypeUsdtError_lift + ) +} + +open func history(offset: UInt32, mnemonic: String, passphrase: String?)async throws -> UsdtDepositPage { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_bitkitcore_fn_method_usdtdepositclient_history( + self.uniffiClonePointer(), + FfiConverterUInt32.lower(offset),FfiConverterString.lower(mnemonic),FfiConverterOptionString.lower(passphrase) + ) + }, + pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, + completeFunc: ffi_bitkitcore_rust_future_complete_rust_buffer, + freeFunc: ffi_bitkitcore_rust_future_free_rust_buffer, + liftFunc: FfiConverterTypeUsdtDepositPage_lift, + errorHandler: FfiConverterTypeUsdtError_lift + ) +} + +open func networks()async throws -> [UsdtDepositNetwork] { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_bitkitcore_fn_method_usdtdepositclient_networks( + self.uniffiClonePointer() + + ) + }, + pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, + completeFunc: ffi_bitkitcore_rust_future_complete_rust_buffer, + freeFunc: ffi_bitkitcore_rust_future_free_rust_buffer, + liftFunc: FfiConverterSequenceTypeUsdtDepositNetwork.lift, + errorHandler: FfiConverterTypeUsdtError_lift + ) +} + +open func receive(network: UsdtDepositNetwork, amount: UInt64, mnemonic: String, passphrase: String?)async throws -> UsdtDepositAddress { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_bitkitcore_fn_method_usdtdepositclient_receive( + self.uniffiClonePointer(), + FfiConverterTypeUsdtDepositNetwork_lower(network),FfiConverterUInt64.lower(amount),FfiConverterString.lower(mnemonic),FfiConverterOptionString.lower(passphrase) + ) + }, + pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, + completeFunc: ffi_bitkitcore_rust_future_complete_rust_buffer, + freeFunc: ffi_bitkitcore_rust_future_free_rust_buffer, + liftFunc: FfiConverterTypeUsdtDepositAddress_lift, + errorHandler: FfiConverterTypeUsdtError_lift + ) +} + +open func requestRefund(depositId: String, offset: UInt32, refundAddress: String, network: UsdtDepositNetwork, mnemonic: String, passphrase: String?)async throws { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_bitkitcore_fn_method_usdtdepositclient_request_refund( + self.uniffiClonePointer(), + FfiConverterString.lower(depositId),FfiConverterUInt32.lower(offset),FfiConverterString.lower(refundAddress),FfiConverterTypeUsdtDepositNetwork_lower(network),FfiConverterString.lower(mnemonic),FfiConverterOptionString.lower(passphrase) + ) + }, + pollFunc: ffi_bitkitcore_rust_future_poll_void, + completeFunc: ffi_bitkitcore_rust_future_complete_void, + freeFunc: ffi_bitkitcore_rust_future_free_void, + liftFunc: { $0 }, + errorHandler: FfiConverterTypeUsdtError_lift + ) +} + + +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeUsdtDepositClient: FfiConverter { + + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = UsdtDepositClient + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> UsdtDepositClient { + return UsdtDepositClient(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: UsdtDepositClient) -> UnsafeMutableRawPointer { + return value.uniffiClonePointer() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UsdtDepositClient { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: UsdtDepositClient, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeUsdtDepositClient_lift(_ pointer: UnsafeMutableRawPointer) throws -> UsdtDepositClient { + return try FfiConverterTypeUsdtDepositClient.lift(pointer) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeUsdtDepositClient_lower(_ value: UsdtDepositClient) -> UnsafeMutableRawPointer { + return FfiConverterTypeUsdtDepositClient.lower(value) +} + + + + + + public protocol UsdtWalletProtocol: AnyObject, Sendable { func balance() async throws -> UInt64 @@ -15788,55 +16005,580 @@ extension TxOutput: Equatable, Hashable { if lhs.scriptpubkey != rhs.scriptpubkey { return false } - if lhs.scriptpubkeyType != rhs.scriptpubkeyType { + if lhs.scriptpubkeyType != rhs.scriptpubkeyType { + return false + } + if lhs.scriptpubkeyAddress != rhs.scriptpubkeyAddress { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.n != rhs.n { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(scriptpubkey) + hasher.combine(scriptpubkeyType) + hasher.combine(scriptpubkeyAddress) + hasher.combine(value) + hasher.combine(n) + } +} + +extension TxOutput: Codable {} + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeTxOutput: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> TxOutput { + return + try TxOutput( + scriptpubkey: FfiConverterString.read(from: &buf), + scriptpubkeyType: FfiConverterOptionString.read(from: &buf), + scriptpubkeyAddress: FfiConverterOptionString.read(from: &buf), + value: FfiConverterInt64.read(from: &buf), + n: FfiConverterUInt32.read(from: &buf) + ) + } + + public static func write(_ value: TxOutput, into buf: inout [UInt8]) { + FfiConverterString.write(value.scriptpubkey, into: &buf) + FfiConverterOptionString.write(value.scriptpubkeyType, into: &buf) + FfiConverterOptionString.write(value.scriptpubkeyAddress, into: &buf) + FfiConverterInt64.write(value.value, into: &buf) + FfiConverterUInt32.write(value.n, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTxOutput_lift(_ buf: RustBuffer) throws -> TxOutput { + return try FfiConverterTypeTxOutput.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTxOutput_lower(_ value: TxOutput) -> RustBuffer { + return FfiConverterTypeTxOutput.lower(value) +} + + +/** + * Current state after accepting a scanned UR frame. + */ +public struct UrDecoderStatus { + /** + * Estimated completion from 0.0 through 1.0. + */ + public var progress: Double + /** + * Fountain source-fragment count, or 1 for a single-part UR. + */ + public var fragmentCount: UInt32 + /** + * Present once the complete message has been decoded. + */ + public var payload: UrPayload? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init( + /** + * Estimated completion from 0.0 through 1.0. + */progress: Double, + /** + * Fountain source-fragment count, or 1 for a single-part UR. + */fragmentCount: UInt32, + /** + * Present once the complete message has been decoded. + */payload: UrPayload?) { + self.progress = progress + self.fragmentCount = fragmentCount + self.payload = payload + } +} + +#if compiler(>=6) +extension UrDecoderStatus: Sendable {} +#endif + + +extension UrDecoderStatus: Equatable, Hashable { + public static func ==(lhs: UrDecoderStatus, rhs: UrDecoderStatus) -> Bool { + if lhs.progress != rhs.progress { + return false + } + if lhs.fragmentCount != rhs.fragmentCount { + return false + } + if lhs.payload != rhs.payload { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(progress) + hasher.combine(fragmentCount) + hasher.combine(payload) + } +} + +extension UrDecoderStatus: Codable {} + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeUrDecoderStatus: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UrDecoderStatus { + return + try UrDecoderStatus( + progress: FfiConverterDouble.read(from: &buf), + fragmentCount: FfiConverterUInt32.read(from: &buf), + payload: FfiConverterOptionTypeUrPayload.read(from: &buf) + ) + } + + public static func write(_ value: UrDecoderStatus, into buf: inout [UInt8]) { + FfiConverterDouble.write(value.progress, into: &buf) + FfiConverterUInt32.write(value.fragmentCount, into: &buf) + FfiConverterOptionTypeUrPayload.write(value.payload, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeUrDecoderStatus_lift(_ buf: RustBuffer) throws -> UrDecoderStatus { + return try FfiConverterTypeUrDecoderStatus.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeUrDecoderStatus_lower(_ value: UrDecoderStatus) -> RustBuffer { + return FfiConverterTypeUrDecoderStatus.lower(value) +} + + +public struct UsdtDeposit { + public var id: String + public var network: String + public var asset: String + public var amount: UInt64? + public var sourceTx: String + public var status: String + public var code: String? + public var refundTx: String? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(id: String, network: String, asset: String, amount: UInt64?, sourceTx: String, status: String, code: String?, refundTx: String?) { + self.id = id + self.network = network + self.asset = asset + self.amount = amount + self.sourceTx = sourceTx + self.status = status + self.code = code + self.refundTx = refundTx + } +} + +#if compiler(>=6) +extension UsdtDeposit: Sendable {} +#endif + + +extension UsdtDeposit: Equatable, Hashable { + public static func ==(lhs: UsdtDeposit, rhs: UsdtDeposit) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.network != rhs.network { + return false + } + if lhs.asset != rhs.asset { + return false + } + if lhs.amount != rhs.amount { + return false + } + if lhs.sourceTx != rhs.sourceTx { + return false + } + if lhs.status != rhs.status { + return false + } + if lhs.code != rhs.code { + return false + } + if lhs.refundTx != rhs.refundTx { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(network) + hasher.combine(asset) + hasher.combine(amount) + hasher.combine(sourceTx) + hasher.combine(status) + hasher.combine(code) + hasher.combine(refundTx) + } +} + +extension UsdtDeposit: Codable {} + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeUsdtDeposit: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UsdtDeposit { + return + try UsdtDeposit( + id: FfiConverterString.read(from: &buf), + network: FfiConverterString.read(from: &buf), + asset: FfiConverterString.read(from: &buf), + amount: FfiConverterOptionUInt64.read(from: &buf), + sourceTx: FfiConverterString.read(from: &buf), + status: FfiConverterString.read(from: &buf), + code: FfiConverterOptionString.read(from: &buf), + refundTx: FfiConverterOptionString.read(from: &buf) + ) + } + + public static func write(_ value: UsdtDeposit, into buf: inout [UInt8]) { + FfiConverterString.write(value.id, into: &buf) + FfiConverterString.write(value.network, into: &buf) + FfiConverterString.write(value.asset, into: &buf) + FfiConverterOptionUInt64.write(value.amount, into: &buf) + FfiConverterString.write(value.sourceTx, into: &buf) + FfiConverterString.write(value.status, into: &buf) + FfiConverterOptionString.write(value.code, into: &buf) + FfiConverterOptionString.write(value.refundTx, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeUsdtDeposit_lift(_ buf: RustBuffer) throws -> UsdtDeposit { + return try FfiConverterTypeUsdtDeposit.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeUsdtDeposit_lower(_ value: UsdtDeposit) -> RustBuffer { + return FfiConverterTypeUsdtDeposit.lower(value) +} + + +public struct UsdtDepositAddress { + public var network: UsdtDepositNetwork + public var address: String + public var recipient: String + public var amount: UInt64 + public var estimatedReceived: UInt64 + public var minUsdCents: String? + public var maxUsdCents: String? + public var slippageBps: UInt32 + public var uri: String + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(network: UsdtDepositNetwork, address: String, recipient: String, amount: UInt64, estimatedReceived: UInt64, minUsdCents: String?, maxUsdCents: String?, slippageBps: UInt32, uri: String) { + self.network = network + self.address = address + self.recipient = recipient + self.amount = amount + self.estimatedReceived = estimatedReceived + self.minUsdCents = minUsdCents + self.maxUsdCents = maxUsdCents + self.slippageBps = slippageBps + self.uri = uri + } +} + +#if compiler(>=6) +extension UsdtDepositAddress: Sendable {} +#endif + + +extension UsdtDepositAddress: Equatable, Hashable { + public static func ==(lhs: UsdtDepositAddress, rhs: UsdtDepositAddress) -> Bool { + if lhs.network != rhs.network { + return false + } + if lhs.address != rhs.address { + return false + } + if lhs.recipient != rhs.recipient { + return false + } + if lhs.amount != rhs.amount { + return false + } + if lhs.estimatedReceived != rhs.estimatedReceived { + return false + } + if lhs.minUsdCents != rhs.minUsdCents { + return false + } + if lhs.maxUsdCents != rhs.maxUsdCents { + return false + } + if lhs.slippageBps != rhs.slippageBps { + return false + } + if lhs.uri != rhs.uri { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(network) + hasher.combine(address) + hasher.combine(recipient) + hasher.combine(amount) + hasher.combine(estimatedReceived) + hasher.combine(minUsdCents) + hasher.combine(maxUsdCents) + hasher.combine(slippageBps) + hasher.combine(uri) + } +} + +extension UsdtDepositAddress: Codable {} + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeUsdtDepositAddress: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UsdtDepositAddress { + return + try UsdtDepositAddress( + network: FfiConverterTypeUsdtDepositNetwork.read(from: &buf), + address: FfiConverterString.read(from: &buf), + recipient: FfiConverterString.read(from: &buf), + amount: FfiConverterUInt64.read(from: &buf), + estimatedReceived: FfiConverterUInt64.read(from: &buf), + minUsdCents: FfiConverterOptionString.read(from: &buf), + maxUsdCents: FfiConverterOptionString.read(from: &buf), + slippageBps: FfiConverterUInt32.read(from: &buf), + uri: FfiConverterString.read(from: &buf) + ) + } + + public static func write(_ value: UsdtDepositAddress, into buf: inout [UInt8]) { + FfiConverterTypeUsdtDepositNetwork.write(value.network, into: &buf) + FfiConverterString.write(value.address, into: &buf) + FfiConverterString.write(value.recipient, into: &buf) + FfiConverterUInt64.write(value.amount, into: &buf) + FfiConverterUInt64.write(value.estimatedReceived, into: &buf) + FfiConverterOptionString.write(value.minUsdCents, into: &buf) + FfiConverterOptionString.write(value.maxUsdCents, into: &buf) + FfiConverterUInt32.write(value.slippageBps, into: &buf) + FfiConverterString.write(value.uri, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeUsdtDepositAddress_lift(_ buf: RustBuffer) throws -> UsdtDepositAddress { + return try FfiConverterTypeUsdtDepositAddress.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeUsdtDepositAddress_lower(_ value: UsdtDepositAddress) -> RustBuffer { + return FfiConverterTypeUsdtDepositAddress.lower(value) +} + + +public struct UsdtDepositDetail { + public var deposit: UsdtDeposit + public var order: UsdtDepositOrder? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(deposit: UsdtDeposit, order: UsdtDepositOrder?) { + self.deposit = deposit + self.order = order + } +} + +#if compiler(>=6) +extension UsdtDepositDetail: Sendable {} +#endif + + +extension UsdtDepositDetail: Equatable, Hashable { + public static func ==(lhs: UsdtDepositDetail, rhs: UsdtDepositDetail) -> Bool { + if lhs.deposit != rhs.deposit { + return false + } + if lhs.order != rhs.order { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(deposit) + hasher.combine(order) + } +} + +extension UsdtDepositDetail: Codable {} + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeUsdtDepositDetail: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UsdtDepositDetail { + return + try UsdtDepositDetail( + deposit: FfiConverterTypeUsdtDeposit.read(from: &buf), + order: FfiConverterOptionTypeUsdtDepositOrder.read(from: &buf) + ) + } + + public static func write(_ value: UsdtDepositDetail, into buf: inout [UInt8]) { + FfiConverterTypeUsdtDeposit.write(value.deposit, into: &buf) + FfiConverterOptionTypeUsdtDepositOrder.write(value.order, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeUsdtDepositDetail_lift(_ buf: RustBuffer) throws -> UsdtDepositDetail { + return try FfiConverterTypeUsdtDepositDetail.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeUsdtDepositDetail_lower(_ value: UsdtDepositDetail) -> RustBuffer { + return FfiConverterTypeUsdtDepositDetail.lower(value) +} + + +public struct UsdtDepositOrder { + public var status: String + public var amountIn: UInt64? + public var amountOut: UInt64? + public var destinationTx: String? + public var refundTx: String? + public var code: String? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(status: String, amountIn: UInt64?, amountOut: UInt64?, destinationTx: String?, refundTx: String?, code: String?) { + self.status = status + self.amountIn = amountIn + self.amountOut = amountOut + self.destinationTx = destinationTx + self.refundTx = refundTx + self.code = code + } +} + +#if compiler(>=6) +extension UsdtDepositOrder: Sendable {} +#endif + + +extension UsdtDepositOrder: Equatable, Hashable { + public static func ==(lhs: UsdtDepositOrder, rhs: UsdtDepositOrder) -> Bool { + if lhs.status != rhs.status { + return false + } + if lhs.amountIn != rhs.amountIn { return false } - if lhs.scriptpubkeyAddress != rhs.scriptpubkeyAddress { + if lhs.amountOut != rhs.amountOut { return false } - if lhs.value != rhs.value { + if lhs.destinationTx != rhs.destinationTx { return false } - if lhs.n != rhs.n { + if lhs.refundTx != rhs.refundTx { + return false + } + if lhs.code != rhs.code { return false } return true } public func hash(into hasher: inout Hasher) { - hasher.combine(scriptpubkey) - hasher.combine(scriptpubkeyType) - hasher.combine(scriptpubkeyAddress) - hasher.combine(value) - hasher.combine(n) + hasher.combine(status) + hasher.combine(amountIn) + hasher.combine(amountOut) + hasher.combine(destinationTx) + hasher.combine(refundTx) + hasher.combine(code) } } -extension TxOutput: Codable {} +extension UsdtDepositOrder: Codable {} #if swift(>=5.8) @_documentation(visibility: private) #endif -public struct FfiConverterTypeTxOutput: FfiConverterRustBuffer { - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> TxOutput { +public struct FfiConverterTypeUsdtDepositOrder: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UsdtDepositOrder { return - try TxOutput( - scriptpubkey: FfiConverterString.read(from: &buf), - scriptpubkeyType: FfiConverterOptionString.read(from: &buf), - scriptpubkeyAddress: FfiConverterOptionString.read(from: &buf), - value: FfiConverterInt64.read(from: &buf), - n: FfiConverterUInt32.read(from: &buf) + try UsdtDepositOrder( + status: FfiConverterString.read(from: &buf), + amountIn: FfiConverterOptionUInt64.read(from: &buf), + amountOut: FfiConverterOptionUInt64.read(from: &buf), + destinationTx: FfiConverterOptionString.read(from: &buf), + refundTx: FfiConverterOptionString.read(from: &buf), + code: FfiConverterOptionString.read(from: &buf) ) } - public static func write(_ value: TxOutput, into buf: inout [UInt8]) { - FfiConverterString.write(value.scriptpubkey, into: &buf) - FfiConverterOptionString.write(value.scriptpubkeyType, into: &buf) - FfiConverterOptionString.write(value.scriptpubkeyAddress, into: &buf) - FfiConverterInt64.write(value.value, into: &buf) - FfiConverterUInt32.write(value.n, into: &buf) + public static func write(_ value: UsdtDepositOrder, into buf: inout [UInt8]) { + FfiConverterString.write(value.status, into: &buf) + FfiConverterOptionUInt64.write(value.amountIn, into: &buf) + FfiConverterOptionUInt64.write(value.amountOut, into: &buf) + FfiConverterOptionString.write(value.destinationTx, into: &buf) + FfiConverterOptionString.write(value.refundTx, into: &buf) + FfiConverterOptionString.write(value.code, into: &buf) } } @@ -15844,100 +16586,71 @@ public struct FfiConverterTypeTxOutput: FfiConverterRustBuffer { #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeTxOutput_lift(_ buf: RustBuffer) throws -> TxOutput { - return try FfiConverterTypeTxOutput.lift(buf) +public func FfiConverterTypeUsdtDepositOrder_lift(_ buf: RustBuffer) throws -> UsdtDepositOrder { + return try FfiConverterTypeUsdtDepositOrder.lift(buf) } #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeTxOutput_lower(_ value: TxOutput) -> RustBuffer { - return FfiConverterTypeTxOutput.lower(value) +public func FfiConverterTypeUsdtDepositOrder_lower(_ value: UsdtDepositOrder) -> RustBuffer { + return FfiConverterTypeUsdtDepositOrder.lower(value) } -/** - * Current state after accepting a scanned UR frame. - */ -public struct UrDecoderStatus { - /** - * Estimated completion from 0.0 through 1.0. - */ - public var progress: Double - /** - * Fountain source-fragment count, or 1 for a single-part UR. - */ - public var fragmentCount: UInt32 - /** - * Present once the complete message has been decoded. - */ - public var payload: UrPayload? +public struct UsdtDepositPage { + public var deposits: [UsdtDeposit] + public var nextOffset: UInt32? // Default memberwise initializers are never public by default, so we // declare one manually. - public init( - /** - * Estimated completion from 0.0 through 1.0. - */progress: Double, - /** - * Fountain source-fragment count, or 1 for a single-part UR. - */fragmentCount: UInt32, - /** - * Present once the complete message has been decoded. - */payload: UrPayload?) { - self.progress = progress - self.fragmentCount = fragmentCount - self.payload = payload + public init(deposits: [UsdtDeposit], nextOffset: UInt32?) { + self.deposits = deposits + self.nextOffset = nextOffset } } #if compiler(>=6) -extension UrDecoderStatus: Sendable {} +extension UsdtDepositPage: Sendable {} #endif -extension UrDecoderStatus: Equatable, Hashable { - public static func ==(lhs: UrDecoderStatus, rhs: UrDecoderStatus) -> Bool { - if lhs.progress != rhs.progress { - return false - } - if lhs.fragmentCount != rhs.fragmentCount { +extension UsdtDepositPage: Equatable, Hashable { + public static func ==(lhs: UsdtDepositPage, rhs: UsdtDepositPage) -> Bool { + if lhs.deposits != rhs.deposits { return false } - if lhs.payload != rhs.payload { + if lhs.nextOffset != rhs.nextOffset { return false } return true } public func hash(into hasher: inout Hasher) { - hasher.combine(progress) - hasher.combine(fragmentCount) - hasher.combine(payload) + hasher.combine(deposits) + hasher.combine(nextOffset) } } -extension UrDecoderStatus: Codable {} +extension UsdtDepositPage: Codable {} #if swift(>=5.8) @_documentation(visibility: private) #endif -public struct FfiConverterTypeUrDecoderStatus: FfiConverterRustBuffer { - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UrDecoderStatus { +public struct FfiConverterTypeUsdtDepositPage: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UsdtDepositPage { return - try UrDecoderStatus( - progress: FfiConverterDouble.read(from: &buf), - fragmentCount: FfiConverterUInt32.read(from: &buf), - payload: FfiConverterOptionTypeUrPayload.read(from: &buf) + try UsdtDepositPage( + deposits: FfiConverterSequenceTypeUsdtDeposit.read(from: &buf), + nextOffset: FfiConverterOptionUInt32.read(from: &buf) ) } - public static func write(_ value: UrDecoderStatus, into buf: inout [UInt8]) { - FfiConverterDouble.write(value.progress, into: &buf) - FfiConverterUInt32.write(value.fragmentCount, into: &buf) - FfiConverterOptionTypeUrPayload.write(value.payload, into: &buf) + public static func write(_ value: UsdtDepositPage, into buf: inout [UInt8]) { + FfiConverterSequenceTypeUsdtDeposit.write(value.deposits, into: &buf) + FfiConverterOptionUInt32.write(value.nextOffset, into: &buf) } } @@ -15945,15 +16658,15 @@ public struct FfiConverterTypeUrDecoderStatus: FfiConverterRustBuffer { #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeUrDecoderStatus_lift(_ buf: RustBuffer) throws -> UrDecoderStatus { - return try FfiConverterTypeUrDecoderStatus.lift(buf) +public func FfiConverterTypeUsdtDepositPage_lift(_ buf: RustBuffer) throws -> UsdtDepositPage { + return try FfiConverterTypeUsdtDepositPage.lift(buf) } #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeUrDecoderStatus_lower(_ value: UrDecoderStatus) -> RustBuffer { - return FfiConverterTypeUrDecoderStatus.lower(value) +public func FfiConverterTypeUsdtDepositPage_lower(_ value: UsdtDepositPage) -> RustBuffer { + return FfiConverterTypeUsdtDepositPage.lower(value) } @@ -23437,6 +24150,78 @@ extension UrPayload: Codable {} +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum UsdtDepositNetwork { + + case ethereum + case tron +} + + +#if compiler(>=6) +extension UsdtDepositNetwork: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeUsdtDepositNetwork: FfiConverterRustBuffer { + typealias SwiftType = UsdtDepositNetwork + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UsdtDepositNetwork { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .ethereum + + case 2: return .tron + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: UsdtDepositNetwork, into buf: inout [UInt8]) { + switch value { + + + case .ethereum: + writeInt(&buf, Int32(1)) + + + case .tron: + writeInt(&buf, Int32(2)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeUsdtDepositNetwork_lift(_ buf: RustBuffer) throws -> UsdtDepositNetwork { + return try FfiConverterTypeUsdtDepositNetwork.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeUsdtDepositNetwork_lower(_ value: UsdtDepositNetwork) -> RustBuffer { + return FfiConverterTypeUsdtDepositNetwork.lower(value) +} + + +extension UsdtDepositNetwork: Equatable, Hashable {} + +extension UsdtDepositNetwork: Codable {} + + + + + + // Note that we don't yet support `indirect` for enums. // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. @@ -23539,11 +24324,17 @@ public enum UsdtError: Swift.Error { case InvalidAddress case WrongNetwork case InvalidCredentials + case ClockSkew case UnsupportedDelegation case InsufficientBalance case QuoteExpired case PendingTransfer case UnsupportedRoute + case DepositNeedsAttention + case DepositNotFound + case DepositAuthorizationRejected + case DepositAmountOutOfRange(minUsdCents: String?, maxUsdCents: String? + ) case NotConfigured case NetworkUnavailable case RateLimited @@ -23573,22 +24364,30 @@ public struct FfiConverterTypeUsdtError: FfiConverterRustBuffer { case 2: return .InvalidAddress case 3: return .WrongNetwork case 4: return .InvalidCredentials - case 5: return .UnsupportedDelegation - case 6: return .InsufficientBalance - case 7: return .QuoteExpired - case 8: return .PendingTransfer - case 9: return .UnsupportedRoute - case 10: return .NotConfigured - case 11: return .NetworkUnavailable - case 12: return .RateLimited - case 13: return .LogRangeTooLarge - case 14: return .TransactionRejected( + case 5: return .ClockSkew + case 6: return .UnsupportedDelegation + case 7: return .InsufficientBalance + case 8: return .QuoteExpired + case 9: return .PendingTransfer + case 10: return .UnsupportedRoute + case 11: return .DepositNeedsAttention + case 12: return .DepositNotFound + case 13: return .DepositAuthorizationRejected + case 14: return .DepositAmountOutOfRange( + minUsdCents: try FfiConverterOptionString.read(from: &buf), + maxUsdCents: try FfiConverterOptionString.read(from: &buf) + ) + case 15: return .NotConfigured + case 16: return .NetworkUnavailable + case 17: return .RateLimited + case 18: return .LogRangeTooLarge + case 19: return .TransactionRejected( reason: try FfiConverterString.read(from: &buf) ) - case 15: return .Storage( + case 20: return .Storage( reason: try FfiConverterString.read(from: &buf) ) - case 16: return .InvalidResponse + case 21: return .InvalidResponse default: throw UniffiInternalError.unexpectedEnumCase } @@ -23617,54 +24416,76 @@ public struct FfiConverterTypeUsdtError: FfiConverterRustBuffer { writeInt(&buf, Int32(4)) - case .UnsupportedDelegation: + case .ClockSkew: writeInt(&buf, Int32(5)) - case .InsufficientBalance: + case .UnsupportedDelegation: writeInt(&buf, Int32(6)) - case .QuoteExpired: + case .InsufficientBalance: writeInt(&buf, Int32(7)) - case .PendingTransfer: + case .QuoteExpired: writeInt(&buf, Int32(8)) - case .UnsupportedRoute: + case .PendingTransfer: writeInt(&buf, Int32(9)) - case .NotConfigured: + case .UnsupportedRoute: writeInt(&buf, Int32(10)) - case .NetworkUnavailable: + case .DepositNeedsAttention: writeInt(&buf, Int32(11)) - case .RateLimited: + case .DepositNotFound: writeInt(&buf, Int32(12)) - case .LogRangeTooLarge: + case .DepositAuthorizationRejected: writeInt(&buf, Int32(13)) - case let .TransactionRejected(reason): + case let .DepositAmountOutOfRange(minUsdCents,maxUsdCents): writeInt(&buf, Int32(14)) + FfiConverterOptionString.write(minUsdCents, into: &buf) + FfiConverterOptionString.write(maxUsdCents, into: &buf) + + + case .NotConfigured: + writeInt(&buf, Int32(15)) + + + case .NetworkUnavailable: + writeInt(&buf, Int32(16)) + + + case .RateLimited: + writeInt(&buf, Int32(17)) + + + case .LogRangeTooLarge: + writeInt(&buf, Int32(18)) + + + case let .TransactionRejected(reason): + writeInt(&buf, Int32(19)) FfiConverterString.write(reason, into: &buf) case let .Storage(reason): - writeInt(&buf, Int32(15)) + writeInt(&buf, Int32(20)) FfiConverterString.write(reason, into: &buf) case .InvalidResponse: - writeInt(&buf, Int32(16)) + writeInt(&buf, Int32(21)) } } @@ -24954,6 +25775,30 @@ fileprivate struct FfiConverterOptionTypeTrezorFeatures: FfiConverterRustBuffer } } +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionTypeUsdtDepositOrder: FfiConverterRustBuffer { + typealias SwiftType = UsdtDepositOrder? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeUsdtDepositOrder.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeUsdtDepositOrder.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -26333,6 +27178,31 @@ fileprivate struct FfiConverterSequenceTypeTxOutput: FfiConverterRustBuffer { } } +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypeUsdtDeposit: FfiConverterRustBuffer { + typealias SwiftType = [UsdtDeposit] + + public static func write(_ value: [UsdtDeposit], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeUsdtDeposit.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [UsdtDeposit] { + let len: Int32 = try readInt(&buf) + var seq = [UsdtDeposit]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeUsdtDeposit.read(from: &buf)) + } + return seq + } +} + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -26483,6 +27353,31 @@ fileprivate struct FfiConverterSequenceTypeHardwareWalletTransport: FfiConverter } } +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypeUsdtDepositNetwork: FfiConverterRustBuffer { + typealias SwiftType = [UsdtDepositNetwork] + + public static func write(_ value: [UsdtDepositNetwork], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeUsdtDepositNetwork.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [UsdtDepositNetwork] { + let len: Int32 = try readInt(&buf) + var seq = [UsdtDepositNetwork]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeUsdtDepositNetwork.read(from: &buf)) + } + return seq + } +} + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -29914,6 +30809,21 @@ private let initializationResult: InitializationResult = { if (uniffi_bitkitcore_checksum_method_urdecoder_reset() != 6027) { return InitializationResult.apiChecksumMismatch } + if (uniffi_bitkitcore_checksum_method_usdtdepositclient_detail() != 63177) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_bitkitcore_checksum_method_usdtdepositclient_history() != 10976) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_bitkitcore_checksum_method_usdtdepositclient_networks() != 27890) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_bitkitcore_checksum_method_usdtdepositclient_receive() != 27375) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_bitkitcore_checksum_method_usdtdepositclient_request_refund() != 10045) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_bitkitcore_checksum_method_usdtwallet_balance() != 12328) { return InitializationResult.apiChecksumMismatch } @@ -29944,6 +30854,9 @@ private let initializationResult: InitializationResult = { if (uniffi_bitkitcore_checksum_constructor_urdecoder_new() != 23014) { return InitializationResult.apiChecksumMismatch } + if (uniffi_bitkitcore_checksum_constructor_usdtdepositclient_new() != 44626) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_bitkitcore_checksum_constructor_usdtwallet_new() != 62148) { return InitializationResult.apiChecksumMismatch } diff --git a/bindings/ios/bitkitcoreFFI.h b/bindings/ios/bitkitcoreFFI.h index 7ebda6a..9df5bb9 100644 --- a/bindings/ios/bitkitcoreFFI.h +++ b/bindings/ios/bitkitcoreFFI.h @@ -660,6 +660,46 @@ RustBuffer uniffi_bitkitcore_fn_method_urdecoder_receive(void*_Nonnull ptr, Rust void uniffi_bitkitcore_fn_method_urdecoder_reset(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_CLONE_USDTDEPOSITCLIENT +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_CLONE_USDTDEPOSITCLIENT +void*_Nonnull uniffi_bitkitcore_fn_clone_usdtdepositclient(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FREE_USDTDEPOSITCLIENT +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FREE_USDTDEPOSITCLIENT +void uniffi_bitkitcore_fn_free_usdtdepositclient(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_CONSTRUCTOR_USDTDEPOSITCLIENT_NEW +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_CONSTRUCTOR_USDTDEPOSITCLIENT_NEW +void*_Nonnull uniffi_bitkitcore_fn_constructor_usdtdepositclient_new(RustBuffer address, RustBuffer service_url, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_METHOD_USDTDEPOSITCLIENT_DETAIL +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_METHOD_USDTDEPOSITCLIENT_DETAIL +uint64_t uniffi_bitkitcore_fn_method_usdtdepositclient_detail(void*_Nonnull ptr, RustBuffer deposit_id, uint32_t offset, RustBuffer mnemonic, RustBuffer passphrase +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_METHOD_USDTDEPOSITCLIENT_HISTORY +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_METHOD_USDTDEPOSITCLIENT_HISTORY +uint64_t uniffi_bitkitcore_fn_method_usdtdepositclient_history(void*_Nonnull ptr, uint32_t offset, RustBuffer mnemonic, RustBuffer passphrase +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_METHOD_USDTDEPOSITCLIENT_NETWORKS +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_METHOD_USDTDEPOSITCLIENT_NETWORKS +uint64_t uniffi_bitkitcore_fn_method_usdtdepositclient_networks(void*_Nonnull ptr +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_METHOD_USDTDEPOSITCLIENT_RECEIVE +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_METHOD_USDTDEPOSITCLIENT_RECEIVE +uint64_t uniffi_bitkitcore_fn_method_usdtdepositclient_receive(void*_Nonnull ptr, RustBuffer network, uint64_t amount, RustBuffer mnemonic, RustBuffer passphrase +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_METHOD_USDTDEPOSITCLIENT_REQUEST_REFUND +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_METHOD_USDTDEPOSITCLIENT_REQUEST_REFUND +uint64_t uniffi_bitkitcore_fn_method_usdtdepositclient_request_refund(void*_Nonnull ptr, RustBuffer deposit_id, uint32_t offset, RustBuffer refund_address, RustBuffer network, RustBuffer mnemonic, RustBuffer passphrase +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_CLONE_USDTWALLET #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_CLONE_USDTWALLET void*_Nonnull uniffi_bitkitcore_fn_clone_usdtwallet(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status @@ -3466,6 +3506,36 @@ uint16_t uniffi_bitkitcore_checksum_method_urdecoder_receive(void #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_METHOD_URDECODER_RESET uint16_t uniffi_bitkitcore_checksum_method_urdecoder_reset(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_METHOD_USDTDEPOSITCLIENT_DETAIL +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_METHOD_USDTDEPOSITCLIENT_DETAIL +uint16_t uniffi_bitkitcore_checksum_method_usdtdepositclient_detail(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_METHOD_USDTDEPOSITCLIENT_HISTORY +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_METHOD_USDTDEPOSITCLIENT_HISTORY +uint16_t uniffi_bitkitcore_checksum_method_usdtdepositclient_history(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_METHOD_USDTDEPOSITCLIENT_NETWORKS +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_METHOD_USDTDEPOSITCLIENT_NETWORKS +uint16_t uniffi_bitkitcore_checksum_method_usdtdepositclient_networks(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_METHOD_USDTDEPOSITCLIENT_RECEIVE +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_METHOD_USDTDEPOSITCLIENT_RECEIVE +uint16_t uniffi_bitkitcore_checksum_method_usdtdepositclient_receive(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_METHOD_USDTDEPOSITCLIENT_REQUEST_REFUND +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_METHOD_USDTDEPOSITCLIENT_REQUEST_REFUND +uint16_t uniffi_bitkitcore_checksum_method_usdtdepositclient_request_refund(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_METHOD_USDTWALLET_BALANCE @@ -3526,6 +3596,12 @@ uint16_t uniffi_bitkitcore_checksum_method_usdtwallet_sync_history(void #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_CONSTRUCTOR_URDECODER_NEW uint16_t uniffi_bitkitcore_checksum_constructor_urdecoder_new(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_CONSTRUCTOR_USDTDEPOSITCLIENT_NEW +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_CONSTRUCTOR_USDTDEPOSITCLIENT_NEW +uint16_t uniffi_bitkitcore_checksum_constructor_usdtdepositclient_new(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_CONSTRUCTOR_USDTWALLET_NEW diff --git a/src/lib.rs b/src/lib.rs index 985a20e..9266c44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,9 +91,10 @@ pub use modules::onchain; pub use modules::scanner::{DecodingError, LnurlPayData, Scanner}; pub use modules::seedqr::{decode_compact_seed_qr, decode_standard_seed_qr, SeedQrError}; pub use modules::usdt::{ - usdt_address, usdt_format_amount, usdt_parse_amount, usdt_parse_payment_request, - UsdtDestination, UsdtError, UsdtPaymentRequest, UsdtQuote, UsdtTransfer, UsdtTransferStatus, - UsdtWallet, + usdt_address, usdt_format_amount, usdt_parse_amount, usdt_parse_payment_request, UsdtDeposit, + UsdtDepositAddress, UsdtDepositClient, UsdtDepositDetail, UsdtDepositNetwork, UsdtDepositOrder, + UsdtDepositPage, UsdtDestination, UsdtError, UsdtPaymentRequest, UsdtQuote, UsdtTransfer, + UsdtTransferStatus, UsdtWallet, }; use bip39::Mnemonic; diff --git a/src/modules/usdt/README.md b/src/modules/usdt/README.md index 24496d3..b7ed7ce 100644 --- a/src/modules/usdt/README.md +++ b/src/modules/usdt/README.md @@ -42,6 +42,8 @@ Storage is wallet-specific and must have one owning `UsdtWallet` object. Drop it Both chain and bundler endpoints must be controlled, credential-free HTTPS URLs; HTTP is accepted only on loopback for fixtures. Provider keys belong on the server. Chain/bundler calls share an 80/minute budget with a burst of 20. Responses are bounded to 2 MiB, except protocol-projected receipts up to 16 MiB. The companion service documents provider requirements, receipt projection and deployment limits. +`UsdtDepositClient` signs Orchestra deposit registration, history, detail and explicit refund requests for the derived account. It uses a separate optional service endpoint; estimates do not imply delivery. A clock-skew error requires correcting the device clock. Amount-limit errors carry the provider’s known USD limits so callers can explain rejected amounts. Source-network fees are paid by the sender. Partner provisioning, delivered deposits and refund acceptance are separate release checks. + The outbound bridge API supports Ethereum (30101), Polygon (30109), Plasma (30383) and Stable (30396), alongside direct Arbitrum transfers. Native release flows expose Arbitrum only; bridge routes require explicit service enablement and destination acceptance. Plain deposits on another chain are not automatically forwarded. Recipient validation rejects the destination token and the pinned EntryPoint, paymaster and Simple7702 delegate addresses on every destination. Direct Arbitrum sends also reject the source OFT and helper. Bridge quotes include 10% native messaging-fee headroom and 20% token-conversion headroom, both within the displayed maximum USDT fee. Before signing or rebroadcasting, the stored native fee, helper liquidity and token approval are checked against current requirements without raising approved limits. The service reports OFT/helper execution reverts as sanitized RPC code `3`, which core maps to `UnsupportedRoute` during initial quoting. A reverted fee recheck for an already reviewed bridge quote requires a fresh quote (`QuoteExpired`); insufficient helper liquidity remains `UnsupportedRoute`. Provider outages remain retryable network errors. Delivery checks process up to three transfers concurrently outside the send lock, with a ten-second request budget, even when source recovery fails; failed lookups retain the last known status, while an explicit `INFLIGHT` or `CONFIRMING` update clears a previous needs-attention state. @@ -56,6 +58,12 @@ RPC providers see queried addresses. Delivery checks use `bitkit_getBridgeMessag Unavailable, unmatched or unknown delivery responses preserve the last status. Those lookups and retryable problems (`FAILED`, `BLOCKED`, `PAYLOAD_STORED`) wait at least one minute before another automatic attempt in the same wallet session. `APPLICATION_BURNED` and `APPLICATION_SKIPPED` stop polling as `BridgeFailed`; `DELIVERED` stops polling as `Confirmed`. No status triggers an automatic paid retry or refund. +## Incoming deposit instructions + +Reusable deposit addresses use an immutable 50-bps slippage policy shared by the service and client. The receive response must confirm those exact terms; accepting a lower number alone would not prove that an existing standing instruction changed. Altering this policy requires coordinating new instructions and client/service behavior. Minimum and maximum USD-cent limits are optional display metadata; malformed limits are omitted without hiding a usable address or the amount-limit error. + +Detail and refund calls take the list offset at which the owned deposit was found. The service checks that page and the following page; if new deposits move it further, callers refresh the list and retry with the updated offset. Unknown provider statuses/networks remain visible for support rather than being guessed as successful or refundable. + ## Validation and bindings Run `cargo test --locked --lib modules::usdt`; CI runs these deterministic tests. They cover independent signing/address vectors, fee bounds, uncertain submission, nonce recovery and restored history. Fixtures use public test credentials. diff --git a/src/modules/usdt/deposits.rs b/src/modules/usdt/deposits.rs new file mode 100644 index 0000000..0ba8950 --- /dev/null +++ b/src/modules/usdt/deposits.rs @@ -0,0 +1,702 @@ +use super::{ + keys::{derive_owner_key, parse_address}, + rpc::{bounded_json, endpoint_client}, + user_operation::sign_hash, + UsdtDestination, UsdtError, +}; +use alloy_primitives::{eip191_hash_message, Address}; +use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize}; +use serde_json::{json, Value}; +use std::{sync::Arc, time::Duration}; +use zeroize::Zeroizing; + +const TRON_USDT: &str = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)] +#[serde(rename_all = "lowercase")] +pub enum UsdtDepositNetwork { + Ethereum, + Tron, +} + +#[derive(Clone, Debug, Deserialize, uniffi::Record)] +pub struct UsdtDepositAddress { + pub network: UsdtDepositNetwork, + pub address: String, + pub recipient: String, + #[serde(deserialize_with = "number")] + pub amount: u64, + #[serde(deserialize_with = "number")] + pub estimated_received: u64, + #[serde(default, deserialize_with = "deposit_limit")] + pub min_usd_cents: Option, + #[serde(default, deserialize_with = "deposit_limit")] + pub max_usd_cents: Option, + pub slippage_bps: u32, + #[serde(skip)] + pub uri: String, +} + +#[derive(Clone, Debug, Deserialize, uniffi::Record)] +pub struct UsdtDeposit { + pub id: String, + pub network: String, + pub asset: String, + #[serde(default, deserialize_with = "optional_number")] + pub amount: Option, + pub source_tx: String, + pub status: String, + pub code: Option, + pub refund_tx: Option, +} + +#[derive(Clone, Debug, Deserialize, uniffi::Record)] +pub struct UsdtDepositPage { + pub deposits: Vec, + pub next_offset: Option, +} + +#[derive(Clone, Debug, Deserialize, uniffi::Record)] +pub struct UsdtDepositOrder { + pub status: String, + #[serde(default, deserialize_with = "optional_number")] + pub amount_in: Option, + #[serde(default, deserialize_with = "optional_number")] + pub amount_out: Option, + pub destination_tx: Option, + pub refund_tx: Option, + pub code: Option, +} + +#[derive(Clone, Debug, Deserialize, uniffi::Record)] +pub struct UsdtDepositDetail { + pub deposit: UsdtDeposit, + pub order: Option, +} + +#[derive(uniffi::Object)] +pub struct UsdtDepositClient { + address: Address, + client: reqwest::Client, + url: String, +} + +#[uniffi::export(async_runtime = "tokio")] +impl UsdtDepositClient { + #[uniffi::constructor] + pub fn new(address: String, service_url: String) -> Result, UsdtError> { + let client = endpoint_client(&service_url, Duration::from_secs(22))?; + Ok(Arc::new(Self { + address: parse_address(&address)?, + client, + url: service_url, + })) + } + + pub async fn networks(&self) -> Result, UsdtError> { + #[derive(Deserialize)] + struct Networks { + networks: Vec, + } + Ok(self + .response::(self.client.get(&self.url)) + .await? + .networks + .into_iter() + .filter_map(|network| match network.as_str() { + "ethereum" => Some(UsdtDepositNetwork::Ethereum), + "tron" => Some(UsdtDepositNetwork::Tron), + _ => None, + }) + .collect()) + } + + pub async fn receive( + &self, + network: UsdtDepositNetwork, + amount: u64, + mnemonic: String, + passphrase: Option, + ) -> Result { + let mnemonic = Zeroizing::new(mnemonic); + let passphrase = passphrase.map(Zeroizing::new); + if amount == 0 { + return Err(UsdtError::InvalidAmount); + } + let mut result: UsdtDepositAddress = self + .call( + json!({"action":"receive", "network":network, + "amount":amount.to_string()}), + mnemonic, + passphrase, + ) + .await?; + if result.network != network + || parse_address(&result.recipient).map_err(|_| UsdtError::InvalidResponse)? + != self.address + || result.amount != amount + || result.estimated_received == 0 + || result.slippage_bps != 50 + { + return Err(UsdtError::InvalidResponse); + } + if validate_source_address(&result.address, network) + .map_err(|_| UsdtError::InvalidResponse)? + == self.address + { + return Err(UsdtError::InvalidResponse); + } + result.uri = match network { + UsdtDepositNetwork::Ethereum => format!( + "ethereum:{}@1/transfer?address={}", + UsdtDestination::Ethereum.token().to_checksum(None), + result.address + ), + UsdtDepositNetwork::Tron => result.address.clone(), + }; + Ok(result) + } + + pub async fn history( + &self, + offset: u32, + mnemonic: String, + passphrase: Option, + ) -> Result { + let page: UsdtDepositPage = self + .call( + json!({"action":"history","offset":offset}), + mnemonic.into(), + passphrase.map(Into::into), + ) + .await?; + if page.next_offset.is_some_and(|next| next <= offset) { + return Err(UsdtError::InvalidResponse); + } + Ok(page) + } + + pub async fn detail( + &self, + deposit_id: String, + offset: u32, + mnemonic: String, + passphrase: Option, + ) -> Result { + let result: UsdtDepositDetail = self + .call( + json!({"action":"detail","depositId":deposit_id,"offset":offset}), + mnemonic.into(), + passphrase.map(Into::into), + ) + .await?; + if result.deposit.id != deposit_id { + return Err(UsdtError::InvalidResponse); + } + Ok(result) + } + + pub async fn request_refund( + &self, + deposit_id: String, + offset: u32, + refund_address: String, + network: UsdtDepositNetwork, + mnemonic: String, + passphrase: Option, + ) -> Result<(), UsdtError> { + let mnemonic = Zeroizing::new(mnemonic); + let passphrase = passphrase.map(Zeroizing::new); + let refund_address = refund_address.trim(); + validate_source_address(refund_address, network)?; + let result: Value = self + .call( + json!({"action":"refund","depositId":deposit_id,"offset":offset, + "refundAddress":refund_address}), + mnemonic, + passphrase, + ) + .await?; + if result["status"] != "refund_requested" { + return Err(UsdtError::InvalidResponse); + } + Ok(()) + } +} + +impl UsdtDepositClient { + async fn call( + &self, + payload: Value, + mnemonic: Zeroizing, + passphrase: Option>, + ) -> Result { + let signed = self.authorize(payload, mnemonic, passphrase, super::wallet::now())?; + self.response(self.client.post(&self.url).json(&signed)) + .await + } + + fn authorize( + &self, + payload: Value, + mnemonic: Zeroizing, + passphrase: Option>, + timestamp: u64, + ) -> Result { + let key = derive_owner_key(mnemonic, passphrase, self.address)?; + let request = + json!({"owner":self.address.to_checksum(None),"timestamp":timestamp,"payload":payload}) + .to_string(); + let message = format!("Bitkit USDT deposits v1\n{request}"); + let signature = sign_hash(eip191_hash_message(message), &key)?; + drop(key); + Ok(json!({"request":request,"signature":signature})) + } + + async fn response( + &self, + request: reqwest::RequestBuilder, + ) -> Result { + let response = request + .send() + .await + .map_err(|_| UsdtError::NetworkUnavailable)?; + let status = response.status(); + if status == reqwest::StatusCode::TOO_MANY_REQUESTS { + return Err(UsdtError::RateLimited); + } + let value = bounded_json(response, 262144, UsdtError::InvalidResponse).await; + if !status.is_success() { + let error = value.unwrap_or_default(); + return Err(match error["error"].as_str() { + Some("not_configured") => UsdtError::NotConfigured, + Some("provider_unavailable") => UsdtError::NetworkUnavailable, + Some("invalid_authorization") => UsdtError::DepositAuthorizationRejected, + Some("not_found") => UsdtError::DepositNotFound, + Some("clock_skew") => UsdtError::ClockSkew, + Some("amount_too_small" | "amount_too_large") => { + UsdtError::DepositAmountOutOfRange { + min_usd_cents: deposit_limit(&error["min_usd_cents"])?, + max_usd_cents: deposit_limit(&error["max_usd_cents"])?, + } + } + Some("amount_exceeds_liquidity") => UsdtError::UnsupportedRoute, + Some("invalid_refund_address") => UsdtError::InvalidAddress, + Some("route_unavailable") => UsdtError::UnsupportedRoute, + Some( + "refund_not_available" + | "instruction_conflict" + | "operator_required" + | "standing_tron_refund_requires_operator", + ) => UsdtError::DepositNeedsAttention, + _ if status == reqwest::StatusCode::UNAUTHORIZED + || status == reqwest::StatusCode::FORBIDDEN => + { + UsdtError::DepositAuthorizationRejected + } + _ if status.is_client_error() || status.is_redirection() => { + UsdtError::InvalidResponse + } + _ => UsdtError::NetworkUnavailable, + }); + } + serde_json::from_value(value?).map_err(Into::into) + } +} + +fn validate_source_address(value: &str, network: UsdtDepositNetwork) -> Result { + match network { + UsdtDepositNetwork::Ethereum => { + let address = parse_address(value)?; + if address == UsdtDestination::Ethereum.token() { + return Err(UsdtError::InvalidAddress); + } + Ok(address) + } + UsdtDepositNetwork::Tron => { + if value.len() != 34 || !value.starts_with('T') || !value.is_ascii() { + return Err(UsdtError::InvalidAddress); + } + let payload = + bitcoin::base58::decode_check(value).map_err(|_| UsdtError::InvalidAddress)?; + if payload.len() != 21 + || payload[0] != 0x41 + || payload[1..].iter().all(|byte| *byte == 0) + || value == TRON_USDT + { + return Err(UsdtError::InvalidAddress); + } + Ok(Address::from_slice(&payload[1..])) + } + } +} + +fn deposit_limit<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + let value = Value::deserialize(deserializer)?; + Ok(value + .as_str() + .filter(|value| { + !value.is_empty() && value.len() <= 40 && value.bytes().all(|c| c.is_ascii_digit()) + }) + .map(str::to_owned)) +} + +fn number<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + String::deserialize(deserializer)? + .parse() + .map_err(serde::de::Error::custom) +} +fn optional_number<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + Option::::deserialize(deserializer)? + .map(|v| v.parse().map_err(serde::de::Error::custom)) + .transpose() +} + +#[cfg(test)] +mod tests { + use super::*; + const PHRASE: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + #[test] + fn deposit_authorization_matches_ethers_and_rejects_another_wallet() { + let vector: Value = + serde_json::from_str(include_str!("fixtures/deposit-signature.json")).unwrap(); + let request: Value = serde_json::from_str(vector["request"].as_str().unwrap()).unwrap(); + let client = UsdtDepositClient::new( + request["owner"].as_str().unwrap().into(), + "https://example.com/v1/usdt/deposits".into(), + ) + .unwrap(); + assert_eq!( + client + .authorize( + request["payload"].clone(), + PHRASE.to_owned().into(), + None, + 1_800_000_000 + ) + .unwrap(), + vector + ); + assert!(matches!( + client.authorize( + request["payload"].clone(), + PHRASE.to_owned().into(), + Some("other".to_owned().into()), + 1_800_000_000 + ), + Err(UsdtError::InvalidCredentials) + )); + } + + #[test] + fn deposit_addresses_and_transport_reject_wrong_networks() { + let tron = "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8"; + for bad in [ + TRON_USDT, + "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb", + &"T".repeat(10000), + ] { + assert!(validate_source_address(bad, UsdtDepositNetwork::Tron).is_err()); + } + assert!(validate_source_address( + &UsdtDestination::Ethereum.token().to_checksum(None), + UsdtDepositNetwork::Ethereum + ) + .is_err()); + assert!(validate_source_address(tron, UsdtDepositNetwork::Tron).is_ok()); + assert!(validate_source_address(tron, UsdtDepositNetwork::Ethereum).is_err()); + assert!(validate_source_address( + "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6s", + UsdtDepositNetwork::Tron + ) + .is_err()); + for url in [ + "http://example.com", + "https://user:password@example.com", + "https://example.com?key=secret", + ] { + assert!(UsdtDepositClient::new( + "0x1111111111111111111111111111111111111111".into(), + url.into() + ) + .is_err()); + } + } + + async fn service( + responses: Vec<(u16, Value)>, + ) -> (String, tokio::task::JoinHandle>) { + use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let url = format!("http://{}/v1/usdt/deposits", listener.local_addr().unwrap()); + let task = tokio::spawn(async move { + tokio::time::timeout(Duration::from_secs(10), async move { + let mut requests = Vec::new(); + for (status, value) in responses { + let (socket, _) = listener.accept().await.unwrap(); + let mut reader = BufReader::new(socket); + let mut first_line = String::new(); + reader.read_line(&mut first_line).await.unwrap(); + assert!(first_line == "GET /v1/usdt/deposits HTTP/1.1\r\n" || first_line == "POST /v1/usdt/deposits HTTP/1.1\r\n"); + let mut length = 0; + loop { + let mut line = String::new(); + assert_ne!(reader.read_line(&mut line).await.unwrap(), 0, "Request ended before its headers"); + if line == "\r\n" { + break; + } + if let Some(value) = line.to_ascii_lowercase().strip_prefix("content-length:") { + length = value.trim().parse().unwrap(); + } + } + let mut bytes = vec![0; length]; + reader.read_exact(&mut bytes).await.unwrap(); + if first_line.starts_with("POST") { + requests.push(serde_json::from_slice::(&bytes).unwrap()); + } + let body = value.to_string(); + reader.get_mut().write_all(format!("HTTP/1.1 {status} Response\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).as_bytes()).await.unwrap(); + } + requests + }).await.expect("Deposit fixture requests must complete") + }); + (url, task) + } + + #[tokio::test] + async fn service_responses_cover_receive_history_detail_and_refund() { + let owner = super::super::usdt_address(PHRASE.into(), None).unwrap(); + let address = "0x1111111111111111111111111111111111111111"; + let deposit = json!({"id":"dep_one","network":"ethereum","asset":"USDT","source_tx":"0xsource","status":"held","code":null,"refund_tx":null}); + let (url, server) = service(vec![ + (200, json!({"networks":["ethereum","tron","bitcoin"]})), + (200, json!({"network":"ethereum","address":address,"recipient":owner,"amount":"100000000","estimated_received":"98500000","slippage_bps":50,"min_usd_cents":"200","max_usd_cents":"10000","uri":123})), + (200, json!({"deposits":[deposit],"next_offset":50})), + (200, json!({"deposit":deposit,"order":{"status":"held"}})), + (202, json!({"status":"refund_requested"})), + ]).await; + let client = UsdtDepositClient::new(owner, url).unwrap(); + assert_eq!( + client.networks().await.unwrap(), + vec![UsdtDepositNetwork::Ethereum, UsdtDepositNetwork::Tron] + ); + let received = client + .receive( + UsdtDepositNetwork::Ethereum, + 100_000_000, + PHRASE.into(), + None, + ) + .await + .unwrap(); + assert_eq!(received.estimated_received, 98_500_000); + assert_eq!(received.min_usd_cents.as_deref(), Some("200")); + assert_eq!(received.max_usd_cents.as_deref(), Some("10000")); + assert!(received.uri.contains(address)); + let page = client.history(0, PHRASE.into(), None).await.unwrap(); + assert_eq!(page.deposits[0].amount, None); + assert_eq!(page.next_offset, Some(50)); + let detail = client + .detail("dep_one".into(), 0, PHRASE.into(), None) + .await + .unwrap(); + assert_eq!(detail.order.unwrap().amount_out, None); + client + .request_refund( + "dep_one".into(), + 0, + "0x2222222222222222222222222222222222222222".into(), + UsdtDepositNetwork::Ethereum, + PHRASE.into(), + None, + ) + .await + .unwrap(); + let requests = server.await.unwrap(); + let expected = [ + json!({"action":"receive","network":"ethereum","amount":"100000000"}), + json!({"action":"history","offset":0}), + json!({"action":"detail","depositId":"dep_one","offset":0}), + json!({"action":"refund","depositId":"dep_one","offset":0,"refundAddress":"0x2222222222222222222222222222222222222222"}), + ]; + assert_eq!(requests.len(), expected.len()); + for (signed, payload) in requests.into_iter().zip(expected) { + let request: Value = serde_json::from_str(signed["request"].as_str().unwrap()).unwrap(); + assert_eq!(request["payload"], payload); + assert_eq!( + signed, + client + .authorize( + payload, + PHRASE.to_string().into(), + None, + request["timestamp"].as_u64().unwrap() + ) + .unwrap() + ); + } + } + + #[tokio::test] + async fn service_errors_preserve_recovery_actions() { + for (code, expected) in [ + ("not_found", UsdtError::DepositNotFound), + ( + "invalid_authorization", + UsdtError::DepositAuthorizationRejected, + ), + ("clock_skew", UsdtError::ClockSkew), + ( + "amount_too_small", + UsdtError::DepositAmountOutOfRange { + min_usd_cents: None, + max_usd_cents: None, + }, + ), + ( + "amount_too_large", + UsdtError::DepositAmountOutOfRange { + min_usd_cents: None, + max_usd_cents: None, + }, + ), + ("amount_exceeds_liquidity", UsdtError::UnsupportedRoute), + ( + "standing_tron_refund_requires_operator", + UsdtError::DepositNeedsAttention, + ), + ("provider_unavailable", UsdtError::NetworkUnavailable), + ] { + let (url, server) = service(vec![( + 400, + json!({"error":code,"min_usd_cents":"200","max_usd_cents":"10000"}), + )]) + .await; + let owner = super::super::usdt_address(PHRASE.into(), None).unwrap(); + let client = UsdtDepositClient::new(owner, url).unwrap(); + let error = client.history(0, PHRASE.into(), None).await.unwrap_err(); + assert_eq!( + std::mem::discriminant(&error), + std::mem::discriminant(&expected) + ); + if let UsdtError::DepositAmountOutOfRange { + min_usd_cents, + max_usd_cents, + } = error + { + assert_eq!(min_usd_cents.as_deref(), Some("200")); + assert_eq!(max_usd_cents.as_deref(), Some("10000")); + } + server.await.unwrap(); + } + for (status, expected) in [ + (429, UsdtError::RateLimited), + (401, UsdtError::DepositAuthorizationRejected), + (403, UsdtError::DepositAuthorizationRejected), + (404, UsdtError::InvalidResponse), + (502, UsdtError::NetworkUnavailable), + ] { + let (url, server) = service(vec![(status, json!({}))]).await; + let client = UsdtDepositClient::new( + super::super::usdt_address(PHRASE.into(), None).unwrap(), + url, + ) + .unwrap(); + let error = client.networks().await.unwrap_err(); + assert_eq!( + std::mem::discriminant(&error), + std::mem::discriminant(&expected) + ); + server.await.unwrap(); + } + } + + #[tokio::test] + async fn optional_limits_preserve_receive_and_amount_errors() { + for (limit, expected) in [ + (json!("200"), Some("200")), + (Value::Null, None), + (json!("unknown"), None), + (json!(200), None), + (json!(""), None), + (json!("1".repeat(41)), None), + ] { + let owner = super::super::usdt_address(PHRASE.into(), None).unwrap(); + let (url, server) = service(vec![ + (200, json!({"network":"ethereum","address":"0x1111111111111111111111111111111111111111","recipient":owner,"amount":"100000000","estimated_received":"98500000","slippage_bps":50,"min_usd_cents":limit,"max_usd_cents":limit})), + (400, json!({"error":"amount_too_small","min_usd_cents":limit,"max_usd_cents":limit})), + ]).await; + let client = UsdtDepositClient::new(owner, url).unwrap(); + let received = client + .receive( + UsdtDepositNetwork::Ethereum, + 100_000_000, + PHRASE.into(), + None, + ) + .await + .unwrap(); + assert_eq!(received.min_usd_cents.as_deref(), expected); + assert_eq!(received.max_usd_cents.as_deref(), expected); + let UsdtError::DepositAmountOutOfRange { + min_usd_cents, + max_usd_cents, + } = client + .receive( + UsdtDepositNetwork::Ethereum, + 100_000_000, + PHRASE.into(), + None, + ) + .await + .unwrap_err() + else { + panic!("Optional limits must not hide the amount error"); + }; + assert_eq!(min_usd_cents.as_deref(), expected); + assert_eq!(max_usd_cents.as_deref(), expected); + server.await.unwrap(); + } + } + + #[tokio::test] + async fn invalid_receive_terms_and_nonadvancing_pages_are_rejected() { + let owner = super::super::usdt_address(PHRASE.into(), None).unwrap(); + let mut responses = vec![ + ( + 200, + json!({"network":"ethereum","address":UsdtDestination::Ethereum.token(),"recipient":owner,"amount":"100000000","estimated_received":"98500000","slippage_bps":50}), + ), + ( + 200, + json!({"network":"ethereum","address":"0x1111111111111111111111111111111111111111","recipient":"invalid","amount":"100000000","estimated_received":"98500000","slippage_bps":50}), + ), + ]; + for slippage_bps in [0, 49, 51] { + responses.push((200, json!({"network":"ethereum","address":"0x1111111111111111111111111111111111111111","recipient":owner,"amount":"100000000","estimated_received":"98500000","slippage_bps":slippage_bps}))); + } + let invalid_receives = responses.len(); + responses.push((200, json!({"deposits":[],"next_offset":0}))); + let (url, server) = service(responses).await; + let client = UsdtDepositClient::new(owner, url).unwrap(); + for _ in 0..invalid_receives { + assert!(matches!( + client + .receive( + UsdtDepositNetwork::Ethereum, + 100_000_000, + PHRASE.into(), + None + ) + .await, + Err(UsdtError::InvalidResponse) + )); + } + assert!(matches!( + client.history(0, PHRASE.into(), None).await, + Err(UsdtError::InvalidResponse) + )); + server.await.unwrap(); + } +} diff --git a/src/modules/usdt/errors.rs b/src/modules/usdt/errors.rs index 3a5cc80..bc6be4e 100644 --- a/src/modules/usdt/errors.rs +++ b/src/modules/usdt/errors.rs @@ -10,6 +10,8 @@ pub enum UsdtError { WrongNetwork, #[error("Wallet credentials do not match this USDT account")] InvalidCredentials, + #[error("Set your device date and time automatically, then try again")] + ClockSkew, #[error("This account uses another wallet's smart account. Restore its delegation before sending with Bitkit")] UnsupportedDelegation, #[error("The USDT balance does not cover the amount and maximum fee")] @@ -20,6 +22,17 @@ pub enum UsdtError { PendingTransfer, #[error("The selected USDT payment route is unavailable")] UnsupportedRoute, + #[error("This deposit needs provider assistance. Check its recovery status")] + DepositNeedsAttention, + #[error("Deposit details changed. Refresh the deposit history and select it again")] + DepositNotFound, + #[error("The deposit service could not verify this request. Try again")] + DepositAuthorizationRejected, + #[error("The amount is outside this deposit route's limits. Review the minimum and maximum")] + DepositAmountOutOfRange { + min_usd_cents: Option, + max_usd_cents: Option, + }, #[error("USDT payments are not configured for this app build")] NotConfigured, #[error("The network could not be reached. Try again")] diff --git a/src/modules/usdt/fixtures/deposit-signature.json b/src/modules/usdt/fixtures/deposit-signature.json new file mode 100644 index 0000000..66fa60b --- /dev/null +++ b/src/modules/usdt/fixtures/deposit-signature.json @@ -0,0 +1,4 @@ +{ + "request": "{\"owner\":\"0x9858EfFD232B4033E47d90003D41EC34EcaEda94\",\"payload\":{\"action\":\"receive\",\"amount\":\"100000000\",\"network\":\"ethereum\"},\"timestamp\":1800000000}", + "signature": "0xe4a2d8340e92339d4f0627a6bf6a9bdaaae55498726ed75155a1d891761cd8cc23d6555526f69ce111c584dad43fbd974669dd9778f69c75c1b869065e05a0381b" +} diff --git a/src/modules/usdt/mod.rs b/src/modules/usdt/mod.rs index 9a56f06..f324cbf 100644 --- a/src/modules/usdt/mod.rs +++ b/src/modules/usdt/mod.rs @@ -1,5 +1,6 @@ mod account; mod amount; +mod deposits; mod errors; mod history; mod keys; @@ -13,6 +14,7 @@ mod user_operation; mod wallet; pub use amount::{usdt_format_amount, usdt_parse_amount}; +pub use deposits::*; pub use errors::UsdtError; pub use keys::usdt_address; pub use payment_request::usdt_parse_payment_request;