Skip to content

Commit a172a86

Browse files
committed
core POS flow logic
1 parent c23099d commit a172a86

File tree

2 files changed

+171
-27
lines changed

2 files changed

+171
-27
lines changed

product/pos/src/main/kotlin/com/reown/pos/client/POS.kt

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.reown.pos.client
22

3-
import com.reown.android.CoreInterface
43
import java.net.URI
54

65
/**
@@ -27,23 +26,33 @@ object POS {
2726
val events: List<String>
2827
) : Model()
2928

30-
data class PaymentEvent(val event: String) : Model()
31-
32-
sealed interface PaymentResult {
33-
data class Success(val txHash: String, val receipt: String) : PaymentResult
34-
data class Failure(val error: PosError) : PaymentResult
29+
sealed interface PaymentEvent {
30+
data class QrReady(val uri: URI) : PaymentEvent
31+
data object Connected : PaymentEvent
32+
data object ConnectedRejected : PaymentEvent
33+
data class ConnectionFailed(val error: Throwable) : PaymentEvent
34+
data object PaymentRequested : PaymentEvent
35+
data object PaymentBroadcasted : PaymentEvent
36+
data class PaymentRejected(val error: PosError.RejectedByUser) : PaymentEvent
37+
data class PaymentSuccessful(val txHash: String, val receipt: String) : PaymentEvent
38+
data class Error(val error: PosError.General) : PaymentEvent
3539
}
3640

3741
sealed interface PosError {
3842
data object Timeout : PosError
39-
data object RejectedByUser : PosError
43+
data class RejectedByUser(val message: String) : PosError
4044
data class Backend(val code: Int, val message: String) : PosError
4145
data object Network : PosError
4246
data object WalletUnsupported : PosError
43-
data class Unknown(val cause: Throwable?) : PosError
47+
data class General(val cause: Throwable?) : PosError
4448
}
4549

46-
data class PaymentIntent(val token: String, val amount: String, val chainId: String, val recipient: String) : Model()
50+
data class PaymentIntent(
51+
val token: String,
52+
val amount: String,
53+
val chainId: String,
54+
val recipient: String
55+
) : Model()
4756
}
4857

4958
/**

product/pos/src/main/kotlin/com/reown/pos/client/POSClient.kt

Lines changed: 153 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@ import android.app.Application
44
import com.reown.android.Core
55
import com.reown.android.CoreInterface
66
import com.reown.android.CoreProtocol
7+
import com.reown.android.internal.common.scope
78
import com.reown.android.relay.ConnectionType
89
import com.reown.sign.client.Sign
910
import 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

1116
object 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

Comments
 (0)