@@ -4,14 +4,20 @@ import android.app.Application
44import com.reown.android.Core
55import com.reown.android.CoreInterface
66import com.reown.android.CoreProtocol
7+ import com.reown.android.internal.common.scope
78import com.reown.android.relay.ConnectionType
89import com.reown.sign.client.Sign
910import com.reown.sign.client.SignClient
11+ import kotlinx.coroutines.delay
12+ import kotlinx.coroutines.launch
13+ import kotlinx.coroutines.supervisorScope
14+ import java.net.URI
1015
1116object POSClient {
1217 private lateinit var coreClient: CoreInterface
18+ private lateinit var posDelegate: POSDelegate
1319 private val sessionNamespaces = mutableMapOf<String , POS .Model .Namespace >()
14- lateinit private var posDelegate : POSDelegate
20+ private var paymentIntents : List < POS . Model . PaymentIntent > = emptyList()
1521
1622 /* *
1723 * POS delegate interface for handling events
@@ -59,46 +65,169 @@ object POSClient {
5965 /* *
6066 * Set the chain to use, EVM only
6167 */
62- fun setChain (chainIds : List <String >) {
63- sessionNamespaces[" eip155" ] = POS .Model .Namespace (
64- chains = chainIds,
65- methods = listOf (" eth_sendTransaction" ),
66- events = listOf (" chainChanged" , " accountsChanged" )
67- )
68+ @Throws(IllegalStateException ::class )
69+ fun setChains (chainIds : List <String >) {
70+ if (chainIds.any { chainId -> chainId.startsWith(" eip155" ) }) {
71+ sessionNamespaces[" eip155" ] = POS .Model .Namespace (
72+ chains = chainIds,
73+ methods = listOf (" eth_sendTransaction" ),
74+ events = listOf (" chainChanged" , " accountsChanged" )
75+ )
76+ } else {
77+ throw IllegalStateException (" EVM only" )
78+ }
6879 }
6980
7081 /* *
7182 * Create a payment intent
7283 */
73- fun createPaymentIntent (paymentIntents : List <POS .Model .PaymentIntent >) {
74- // - Generates the connection URL
75- // - Sends the connection proposal to the wallet
76- // - Awaits the connection result
77- // - Builds and sends the transaction to the wallet
78- // - Awaits the transaction result from the wallet
79- // - Checks the transaction status
84+ @Throws(IllegalStateException ::class )
85+ fun createPaymentIntent (intents : List <POS .Model .PaymentIntent >) {
86+ // TODO: add intent fields validation
87+ checkPOSDelegateInitialization()
88+ if (sessionNamespaces.isEmpty()) throw IllegalStateException (" No chain set, call setChains method first" )
89+ if (intents.isEmpty()) throw IllegalStateException (" No payment intents provided" )
90+ paymentIntents = intents
91+
92+
93+ val pairing = coreClient.Pairing .create { error ->
94+ posDelegate.onEvent(POS .Model .PaymentEvent .ConnectionFailed (error.throwable))
95+ }
96+
97+ if (pairing != null ) {
98+ val signNamespaces = sessionNamespaces.mapValues { (_, namespace) ->
99+ Sign .Model .Namespace .Proposal (
100+ chains = namespace.chains,
101+ methods = namespace.methods,
102+ events = namespace.events
103+ )
104+ }
105+
106+ val connectParams = Sign .Params .ConnectParams (
107+ sessionNamespaces = signNamespaces,
108+ pairing = pairing
109+ )
110+
111+ SignClient .connect(
112+ connectParams = connectParams,
113+ onSuccess = { url ->
114+ posDelegate.onEvent(POS .Model .PaymentEvent .QrReady (URI (pairing.uri)))
115+ },
116+ onError = { error ->
117+ posDelegate.onEvent(POS .Model .PaymentEvent .ConnectionFailed (error.throwable))
118+ }
119+ )
120+ } else {
121+ posDelegate.onEvent(POS .Model .PaymentEvent .ConnectionFailed (Throwable (" Pairing is null" )))
122+ }
80123 }
81124
82125 /* *
83126 * Set the delegate for handling POS events
84127 */
85128 fun setDelegate (delegate : POSDelegate ) {
86129 posDelegate = delegate
130+
87131 val dappDelegate = object : SignClient .DappDelegate {
132+
88133 override fun onSessionApproved (approvedSession : Sign .Model .ApprovedSession ) {
89- TODO (" Not yet implemented" )
134+ scope.launch {
135+ supervisorScope {
136+ posDelegate.onEvent(POS .Model .PaymentEvent .Connected )
137+ val method = sessionNamespaces.values.first().methods.first()
138+ val chainId = paymentIntents.first().chainId
139+ val amount = paymentIntents.first().amount
140+ val token = paymentIntents.first().token
141+ val recipient = paymentIntents.first().recipient
142+ var senderAddress: String? = null
143+
144+ // TODO: Build Request using server
145+ print (" kobe: Building request: $chainId , $amount , $token , $recipient " )
146+ delay(2000 )
147+ print (" kobe: Request built success" )
148+
149+
150+ approvedSession.namespaces.forEach { (namespace, session) ->
151+ // Check if the namespace key matches the chain ID from payment intent
152+ senderAddress = when {
153+ // If chains are not null and not empty, find the first account on the same chain as payment intent
154+ session.chains != null && session.chains!! .isNotEmpty() -> {
155+ val chains = session.chains
156+ if (chains != null ) {
157+ session.accounts.firstOrNull { account ->
158+ chains.any { chain ->
159+ chain == chainId || account.startsWith(" $chain :" )
160+ }
161+ }
162+ } else {
163+ null
164+ }
165+ }
166+
167+ namespace == chainId -> {
168+ session.accounts.firstOrNull()
169+ }
170+
171+ else -> null
172+ }
173+ }
174+
175+ if (senderAddress != null ) {
176+ val request = Sign .Params .Request (
177+ sessionTopic = approvedSession.topic,
178+ method = method,
179+ params = " [{\" from\" :\" $senderAddress \" ,\" to\" :\" $recipient \" ,\" data\" :\" 0x\" ,\" gasLimit\" :\" 0x5208\" ,\" gasPrice\" :\" 0x0649534e00\" ,\" value\" :\" $amount \" ,\" nonce\" :\" 0x07\" }]" ,
180+ chainId = chainId
181+ )
182+
183+ SignClient .request(
184+ request = request,
185+ onSuccess = { sentRequest -> posDelegate.onEvent(POS .Model .PaymentEvent .PaymentRequested ) },
186+ onError = { error -> posDelegate.onEvent(POS .Model .PaymentEvent .ConnectionFailed (error.throwable)) }
187+ )
188+
189+ } else {
190+ // TODO: disconnect?
191+ posDelegate.onEvent(POS .Model .PaymentEvent .Error (POS .Model .PosError .General (Throwable (" No matching account found" ))))
192+ }
193+ }
194+ }
90195 }
91196
92197 override fun onSessionRejected (rejectedSession : Sign .Model .RejectedSession ) {
93- TODO ( " Not yet implemented " )
198+ posDelegate.onEvent( POS . Model . PaymentEvent . ConnectedRejected )
94199 }
95200
96201 override fun onSessionRequestResponse (response : Sign .Model .SessionRequestResponse ) {
97- TODO (" Not yet implemented" )
202+ when (val result = response.result) {
203+ is Sign .Model .JsonRpcResponse .JsonRpcResult -> {
204+ scope.launch {
205+ supervisorScope {
206+ posDelegate.onEvent(POS .Model .PaymentEvent .PaymentBroadcasted )
207+
208+ // TODO: Check transaction status on blockchain, handle server errors and timeout
209+ print (" kobe: Checking payment status.." )
210+ delay(2000 )
211+ print (" kobe: Payment successful" )
212+
213+ val txHash = result.toString()
214+ // TODO: get txHash and receipt from server
215+ posDelegate.onEvent(POS .Model .PaymentEvent .PaymentSuccessful (txHash = txHash, receipt = " test" ))
216+
217+ // TODO: disconnect session when payment is successful
218+ }
219+ }
220+ }
221+
222+ is Sign .Model .JsonRpcResponse .JsonRpcError -> {
223+ val error = POS .Model .PosError .RejectedByUser (message = result.message)
224+ posDelegate.onEvent(POS .Model .PaymentEvent .PaymentRejected (error))
225+ }
226+ }
98227 }
99228
100229 override fun onError (error : Sign .Model .Error ) {
101- TODO ( " Not yet implemented " )
230+ posDelegate.onEvent( POS . Model . PaymentEvent . Error ( POS . Model . PosError . General (error.throwable)) )
102231 }
103232
104233 override fun onConnectionStateChange (state : Sign .Model .ConnectionState ) {}
@@ -118,4 +247,10 @@ object POSClient {
118247
119248 SignClient .setDappDelegate(dappDelegate)
120249 }
250+
251+ private fun checkPOSDelegateInitialization () {
252+ check(::posDelegate.isInitialized) {
253+ " POSDelegate needs to be initialized first"
254+ }
255+ }
121256}
0 commit comments