Skip to content

Commit

Permalink
Merge pull request #1663 from p2p-org/feature/pwn-769
Browse files Browse the repository at this point in the history
[ETH-769] Referral program
  • Loading branch information
bigearsenal authored Feb 2, 2024
2 parents 13290ec + 7ed23c2 commit b657d8e
Show file tree
Hide file tree
Showing 43 changed files with 791 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@
"location" : "https://github.com/p2p-org/solana-swift",
"state" : {
"branch" : "main",
"revision" : "f9181079014c474c0e823a2f3f615ba953b41f1e"
"revision" : "3811cabe260e4b88e8096527a50eee8d23b41204"
}
},
{
Expand Down
6 changes: 6 additions & 0 deletions p2p_wallet/AppDelegate/AppCoordinator/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ final class AppCoordinator: Coordinator<Void> {
await Resolver.resolve(JupiterTokensRepository.self).load()
}

if available(.referralProgramEnabled) {
Task {
await Resolver.resolve(ReferralProgramService.self).register()
}
}

Task {
// load services
if available(.sellScenarioEnabled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,17 @@ final class DeeplinkAppDelegateService: NSObject, AppDelegateService {
}

// Swap via link
// https://s.key.app/swap?inputMint=<from>&outputMint=<to>
// https://s.key.app/swap?inputMint=<from>&outputMint=<to>&r=<referrer>
if urlComponents.host == "s.key.app" {
GlobalAppState.shared.swapUrl = urlComponents.url

if let referrer = urlComponents.queryItems?.first { $0.name == "r" }?.value {
setReferrerIfNeeded(r: referrer)
}
}

if urlComponents.host == "r.key.app" {
setReferrerIfNeeded(r: String(urlComponents.path.dropFirst()))
}
}

Expand Down Expand Up @@ -121,6 +129,18 @@ final class DeeplinkAppDelegateService: NSObject, AppDelegateService {
else if scheme == "keyapp", host == "swap" {
GlobalAppState.shared.swapUrl = components.url
}
// keyapp://referral
else if scheme == "keyapp", host == "referral" {
setReferrerIfNeeded(r: components.path)
}
}

private func setReferrerIfNeeded(r: String) {
guard available(.referralProgramEnabled) else { return }
let referralService: ReferralProgramService = Resolver.resolve()
Task {
_ = await referralService.setReferent(from: r)
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions p2p_wallet/Common/Services/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ extension DefaultsKeys {
var forcedFeeRelayerEndpoint: DefaultsKey<String?> { .init(#function, defaultValue: nil) }
var forcedNameServiceEndpoint: DefaultsKey<String?> { .init(#function, defaultValue: nil) }
var forcedNewSwapEndpoint: DefaultsKey<String?> { .init(#function, defaultValue: nil) }
var forcedReferralProgramEndpoint: DefaultsKey<String?> { .init(#function, defaultValue: nil) }

var didBackupOffline: DefaultsKey<Bool> { .init(#function, defaultValue: false) }
var walletName: DefaultsKey<[String: String]> { .init(#function, defaultValue: [:]) }
Expand Down Expand Up @@ -123,6 +124,10 @@ extension DefaultsKeys {
var ethBannerShouldHide: DefaultsKey<Bool> {
.init(#function, defaultValue: false)
}

var referrerRegistered: DefaultsKey<Bool> {
.init(#function, defaultValue: false)
}
}

// MARK: - Moonpay Environment
Expand Down
3 changes: 3 additions & 0 deletions p2p_wallet/Common/Services/FeatureFlags/Features.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,7 @@ public extension Feature {
static let sendViaLinkEnabled = Feature(rawValue: "send_via_link_enabled")

static let solanaEthAddressEnabled = Feature(rawValue: "solana_eth_address_enabled")

// Referral program
static let referralProgramEnabled = Feature(rawValue: "referral_program_enabled")
}
15 changes: 15 additions & 0 deletions p2p_wallet/Common/Services/GlobalAppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ class GlobalAppState: ObservableObject {
}
}

@Published var newReferralProgramEndpoint: String {
didSet {
Defaults.forcedReferralProgramEndpoint = newReferralProgramEndpoint
ResolverScope.session.reset()
}
}

@Published var referralProgramAPIEndoint = String.secretConfig("REFERRAL_PROGRAM_API_ENDPOINT_PROD")!

// TODO: Refactor!
@Published var surveyID: String?
@Published var sendViaLinkUrl: URL?
Expand All @@ -55,6 +64,12 @@ class GlobalAppState: ObservableObject {
} else {
newSwapEndpoint = "https://swap-v6.key.app"
}

if let forcedValue = Defaults.forcedReferralProgramEndpoint {
newReferralProgramEndpoint = forcedValue
} else {
newReferralProgramEndpoint = ReferralProgramViewModel.Constants.urlString
}
}

@Published var bridgeEndpoint: String = (Environment.current == .release) ?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

struct ReferralTimedSignature: Encodable {
let timestamp: Int64
let signature: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation
import SolanaSwift
import TweetNacl

struct RegisterUserRequest: Encodable {
let user: String
let timedSignature: ReferralTimedSignature

enum CodingKeys: String, CodingKey {
case user, timedSignature = "timed_signature"
}
}

struct RegisterUserSignature: BorshSerializable {
let user: String
let referrent: String?
let timestamp: Int64

func serialize(to writer: inout Data) throws {
try user.serialize(to: &writer)
try Optional(referrent)?.serialize(to: &writer)
try timestamp.serialize(to: &writer)
}

func sign(secretKey: Data) throws -> Data {
var data = Data()
try serialize(to: &data)
return try NaclSign.signDetached(message: data, secretKey: secretKey)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation
import SolanaSwift
import TweetNacl

struct SetReferentRequest: Encodable {
let user: String
let referent: String
let timedSignature: ReferralTimedSignature

enum CodingKeys: String, CodingKey {
case user, referent, timedSignature = "timed_signature"
}
}

struct SetReferentSignature: BorshSerializable {
let user: String
let referent: String
let timestamp: Int64

func serialize(to writer: inout Data) throws {
try user.serialize(to: &writer)
try referent.serialize(to: &writer)
try timestamp.serialize(to: &writer)
}

func sign(secretKey: Data) throws -> Data {
var data = Data()
try serialize(to: &data)
return try NaclSign.signDetached(message: data, secretKey: secretKey)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import Foundation
import KeyAppBusiness
import KeyAppNetworking
import Resolver
import SolanaSwift
import TweetNacl

protocol ReferralProgramService {
var referrer: String { get }
var shareLink: URL { get }

func register() async
func setReferent(from: String) async
}

enum ReferralProgramServiceError: Error {
case failedSet
}

final class ReferralProgramServiceImpl {
// MARK: - Dependencies

@Injected private var nameStorage: NameStorageType
@Injected private var userWallet: UserWalletManager

private let jsonrpcClient = JSONRPCHTTPClient()

// MARK: - Private properties

private var baseURL: String {
GlobalAppState.shared.referralProgramAPIEndoint
}

private var currentUserAddress: String {
userWallet.wallet?.account.publicKey.base58EncodedString ?? ""
}
}

// MARK: - ReferralProgramService

extension ReferralProgramServiceImpl: ReferralProgramService {
var referrer: String {
nameStorage.getName() ?? currentUserAddress
}

var shareLink: URL {
URL(string: "https://r.key.app/\(referrer)")!
}

func register() async {
guard !Defaults.referrerRegistered else { return }
do {
guard let secret = userWallet.wallet?.account.secretKey else { throw ReferralProgramServiceError.failedSet }
let timestamp = Int64(Date().timeIntervalSince1970)
let signed = try RegisterUserSignature(
user: currentUserAddress, referrent: nil, timestamp: timestamp
)
.sign(secretKey: secret)
let _: String? = try await jsonrpcClient.request(
baseURL: baseURL,
body: .init(
method: "register",
params: RegisterUserRequest(
user: currentUserAddress,
timedSignature: ReferralTimedSignature(
timestamp: timestamp, signature: signed.toHexString()
)
)
)
)
Defaults.referrerRegistered = true
} catch {
debugPrint(error)
DefaultLogManager.shared.log(
event: "\(ReferralProgramService.self)_register",
data: error.localizedDescription,
logLevel: LogLevel.error
)
}
}

func setReferent(from: String) async {
guard from != currentUserAddress else { return }
do {
guard let secret = userWallet.wallet?.account.secretKey else { throw ReferralProgramServiceError.failedSet }
let timestamp = Int64(Date().timeIntervalSince1970)
let signed = try SetReferentSignature(
user: currentUserAddress, referent: from, timestamp: timestamp
)
.sign(secretKey: secret)
let _: String? = try await jsonrpcClient.request(
baseURL: baseURL,
body: .init(
method: "set_referent",
params: SetReferentRequest(
user: currentUserAddress,
referent: from,
timedSignature: ReferralTimedSignature(
timestamp: timestamp,
signature: signed.toHexString()
)
)
)
)
} catch {
debugPrint(error)
DefaultLogManager.shared.log(
event: "\(ReferralProgramService.self)_setReferent",
data: error.localizedDescription,
logLevel: LogLevel.error
)
}
}
}
1 change: 1 addition & 0 deletions p2p_wallet/Common/Services/Storage/UserWalletManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class UserWalletManager: ObservableObject {
Defaults.isTokenInputTypeChosen = false
Defaults.fromTokenAddress = nil
Defaults.toTokenAddress = nil
Defaults.referrerRegistered = false

walletSettings.reset()

Expand Down
2 changes: 2 additions & 0 deletions p2p_wallet/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>REFERRAL_PROGRAM_API_ENDPOINT_PROD</key>
<string>$(REFERRAL_PROGRAM_API_ENDPOINT_PROD)</string>
<key>AMPLITUDE_API_KEY</key>
<string>$(AMPLITUDE_API_KEY)</string>
<key>AMPLITUDE_API_KEY_FEATURE</key>
Expand Down
3 changes: 3 additions & 0 deletions p2p_wallet/Injection/Resolver+registerAllServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ extension Resolver: ResolverRegistering {
.scope(.application)

register { Web3(rpcURL: String.secretConfig("ETH_RPC")!) }

register { ReferralProgramServiceImpl() }
.implements(ReferralProgramService.self)
}

/// Session scope: Live when user is authenticated
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "image [email protected]",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "image [email protected]",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "[email protected]",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "[email protected]",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions p2p_wallet/Resources/Base.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -593,10 +593,14 @@
"The token %@ from your swap link seems suspicious, therefore we've refreshed swap pair to default." = "The token %@ from your swap link seems suspicious, therefore we've refreshed swap pair to default.";
"Charge that you need to pay to send or receive tokenA. It helps maintain the network and ensure smooth transactions." = "Charge that you need to pay to send or receive tokenA. It helps maintain the network and ensure smooth transactions.";
"Unfortunately, you can not cashout in %@, but you can still use other Key App features" = "Unfortunately, you can not cashout in %@, but you can still use other Key App features";
"Share my link" = "Share my link";
"Open details" = "Open details";
"Token 2022 details" = "Token 2022 details";
"Token 2022 transfer fee" = "Token 2022 transfer fee";
"Calculated by subtracting the token 2022 transfer fee from your balance" = "Calculated by subtracting the token 2022 transfer fee from your balance";
"Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance" = "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance";
"Referral program" = "Referral program";
"Hey, let’s swap trendy meme coins with me!" = "Hey, let’s swap trendy meme coins with me!";
"%@ all the time" = "%@ all the time";
"Here’s how do we count your profits for total balance and every single token" = "Here’s how do we count your profits for total balance and every single token";
"Based on absolute and relative profitability of each trade. It shows the relative potential %% profits or losses of your trading strategy." = "Based on absolute and relative profitability of each trade. It shows the relative potential %% profits or losses of your trading strategy.";
Expand Down
Loading

0 comments on commit b657d8e

Please sign in to comment.