Source Code

(use-trait sip-010-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)

(define-constant DEPLOYER tx-sender)
(define-constant GAME_TIMEOUT u144)

(define-constant err-not-oracle (err u7001))
(define-constant err-not-deployer (err u7002))
(define-constant err-invalid-signature (err u7003))
(define-constant err-insufficient-balance (err u7004))
(define-constant err-game-not-found (err u7005))
(define-constant err-game-not-active (err u7006))
(define-constant err-game-not-expired (err u7007))
(define-constant err-invalid-amount (err u7009))
(define-constant err-signature-replay (err u7010))
(define-constant err-invalid-winner (err u7011))
(define-constant err-same-player (err u7012))
(define-constant err-token-not-whitelisted (err u7013))
(define-constant err-wallet-mismatch (err u7014))

(define-data-var oracle principal 'SP28MP1HQDJWQAFSQJN2HBAXBVP7H7THD1W2NYZVK)
(define-data-var game-nonce uint u0)
(define-data-var fee-rate uint u500)
(define-data-var withdraw-fee-rate uint u100)
(define-data-var treasury principal DEPLOYER)

(define-map balances { pubkey: (buff 33), token: principal } uint)
(define-map used-signatures (buff 32) (buff 33))
(define-map games uint {
  player-a: (buff 33),
  player-b: (buff 33),
  token: principal,
  wager-amount: uint,
  status: (string-ascii 10),
  winner: (optional (buff 33)),
  created-at: uint,
})
(define-map accumulated-fees principal uint)
(define-map whitelisted-tokens principal bool)
(define-map pubkey-wallet (buff 33) principal)

(define-read-only (get-balance (pubkey (buff 33)) (token principal))
  (default-to u0 (map-get? balances { pubkey: pubkey, token: token }))
)

(define-read-only (get-game (game-id uint))
  (map-get? games game-id)
)

(define-read-only (is-signature-used (message-hash (buff 32)))
  (is-some (map-get? used-signatures message-hash))
)

(define-read-only (get-game-nonce)
  (var-get game-nonce)
)

(define-read-only (get-oracle)
  (var-get oracle)
)

(define-read-only (get-fee-rate)
  (var-get fee-rate)
)

(define-read-only (get-withdraw-fee-rate)
  (var-get withdraw-fee-rate)
)

(define-read-only (get-accumulated-fees (token principal))
  (default-to u0 (map-get? accumulated-fees token))
)

(define-read-only (is-token-whitelisted (token principal))
  (default-to false (map-get? whitelisted-tokens token))
)

(define-read-only (get-registered-wallet (pubkey (buff 33)))
  (map-get? pubkey-wallet pubkey)
)

(define-private (verify-signature
    (message-hash (buff 32)) (signature (buff 65)) (expected-pubkey (buff 33)))
  (match (secp256k1-recover? message-hash signature)
    recovered-pubkey (begin
      (asserts! (is-eq recovered-pubkey expected-pubkey) err-invalid-signature)
      (ok true)
    )
    e err-invalid-signature
  )
)

(define-private (consume-signature
    (message-hash (buff 32)) (signature (buff 65)) (pubkey (buff 33)))
  (begin
    (try! (verify-signature message-hash signature pubkey))
    (asserts! (is-none (map-get? used-signatures message-hash)) err-signature-replay)
    (map-set used-signatures message-hash pubkey)
    (ok true)
  )
)

(define-private (credit-balance (pubkey (buff 33)) (token principal) (amount uint))
  (map-set balances
    { pubkey: pubkey, token: token }
    (+ (get-balance pubkey token) amount)
  )
)

(define-private (debit-balance (pubkey (buff 33)) (token principal) (amount uint))
  (let ((current (get-balance pubkey token)))
    (asserts! (<= amount current) err-insufficient-balance)
    (map-set balances { pubkey: pubkey, token: token } (- current amount))
    (ok true)
  )
)

(define-public (deposit (token <sip-010-trait>) (amount uint) (pubkey (buff 33)))
  (begin
    (asserts! (> amount u0) err-invalid-amount)
    (asserts! (is-token-whitelisted (contract-of token)) err-token-not-whitelisted)
    (try! (contract-call? token transfer amount tx-sender current-contract none))
    (credit-balance pubkey (contract-of token) amount)
    (print {
      event: "deposit",
      pubkey: pubkey,
      token: (contract-of token),
      amount: amount,
      sender: tx-sender
    })
    (ok true)
  )
)

(define-public (register-wallet
    (pubkey (buff 33))
    (wallet principal)
    (auth-id uint)
    (signature (buff 65)))
  (let (
    (message-hash (contract-call? 'SP28MP1HQDJWQAFSQJN2HBAXBVP7H7THD1W2NYZVK.auth-v7 build-register-wallet-hash {
      auth-id: auth-id,
      wallet: wallet,
    }))
  )
    (try! (consume-signature message-hash signature pubkey))
    (map-set pubkey-wallet pubkey wallet)
    (print {
      event: "wallet-registered",
      pubkey: pubkey,
      wallet: wallet
    })
    (ok true)
  )
)

(define-public (withdraw
    (token <sip-010-trait>)
    (token-name (string-ascii 128))
    (amount uint)
    (recipient principal)
    (pubkey (buff 33))
    (auth-id uint)
    (signature (buff 65)))
  (let (
    (tokn (contract-of token))
    (actual-recipient (default-to recipient (map-get? pubkey-wallet pubkey)))
    (message-hash (contract-call? 'SP28MP1HQDJWQAFSQJN2HBAXBVP7H7THD1W2NYZVK.auth-v7 build-withdraw-hash {
      auth-id: auth-id,
      amount: amount,
      recipient: actual-recipient,
      token: tokn,
    }))
    (fee (/ (* amount (var-get withdraw-fee-rate)) u10000))
    (payout (- amount fee))
  )
    (asserts! (> amount u0) err-invalid-amount)
    (try! (consume-signature message-hash signature pubkey))
    (try! (debit-balance pubkey tokn amount))
    (map-set accumulated-fees tokn
      (+ (get-accumulated-fees tokn) fee))
    (try! (as-contract? ((with-ft tokn token-name payout))
      (try! (contract-call? token transfer payout current-contract actual-recipient none))))
    (print {
      event: "withdraw",
      pubkey: pubkey,
      token: tokn,
      amount: amount,
      fee: fee,
      payout: payout,
      recipient: actual-recipient
    })
    (ok true)
  )
)

(define-public (create-game
    (player-a (buff 33))
    (player-b (buff 33))
    (token principal)
    (wager-amount uint)
    (sig-a { auth-id: uint, signature: (buff 65) })
    (sig-b { auth-id: uint, signature: (buff 65) }))
  (let (
    (game-id (var-get game-nonce))
    (hash-a (contract-call? 'SP28MP1HQDJWQAFSQJN2HBAXBVP7H7THD1W2NYZVK.auth-v7 build-wager-hash {
      auth-id: (get auth-id sig-a),
      opponent: player-b,
      token: token,
      wager-amount: wager-amount,
    }))
    (hash-b (contract-call? 'SP28MP1HQDJWQAFSQJN2HBAXBVP7H7THD1W2NYZVK.auth-v7 build-wager-hash {
      auth-id: (get auth-id sig-b),
      opponent: player-a,
      token: token,
      wager-amount: wager-amount,
    }))
  )
    (asserts! (is-eq tx-sender (var-get oracle)) err-not-oracle)
    (asserts! (> wager-amount u0) err-invalid-amount)
    (asserts! (not (is-eq player-a player-b)) err-same-player)
    (asserts! (is-token-whitelisted token) err-token-not-whitelisted)
    (try! (consume-signature hash-a (get signature sig-a) player-a))
    (try! (consume-signature hash-b (get signature sig-b) player-b))
    (try! (debit-balance player-a token wager-amount))
    (try! (debit-balance player-b token wager-amount))
    (map-set games game-id {
      player-a: player-a,
      player-b: player-b,
      token: token,
      wager-amount: wager-amount,
      status: "active",
      winner: none,
      created-at: burn-block-height,
    })
    (var-set game-nonce (+ game-id u1))
    (print {
      event: "game-created",
      game-id: game-id,
      player-a: player-a,
      player-b: player-b,
      token: token,
      wager-amount: wager-amount,
      created-at: burn-block-height
    })
    (ok game-id)
  )
)

(define-public (resolve-game (game-id uint) (winner (buff 33)))
  (let (
    (game (unwrap! (map-get? games game-id) err-game-not-found))
    (pot (* (get wager-amount game) u2))
    (fee (/ (* pot (var-get fee-rate)) u10000))
    (payout (- pot fee))
    (token (get token game))
  )
    (asserts! (is-eq tx-sender (var-get oracle)) err-not-oracle)
    (asserts! (is-eq (get status game) "active") err-game-not-active)
    (asserts!
      (or (is-eq winner (get player-a game))
          (is-eq winner (get player-b game)))
      err-invalid-winner)
    (credit-balance winner token payout)
    (map-set accumulated-fees token
      (+ (get-accumulated-fees token) fee))
    (map-set games game-id
      (merge game { status: "resolved", winner: (some winner) }))
    (print {
      event: "game-resolved",
      game-id: game-id,
      winner: winner,
      payout: payout,
      fee: fee
    })
    (ok true)
  )
)

(define-public (cancel-game (game-id uint))
  (let (
    (game (unwrap! (map-get? games game-id) err-game-not-found))
    (tokn (get token game))
    (wager (get wager-amount game))
    (fee (/ (* wager (var-get withdraw-fee-rate)) u10000))
    (refund (- wager fee))
  )
    (asserts! (is-eq (get status game) "active") err-game-not-active)
    (asserts!
      (or (is-eq tx-sender (var-get oracle))
          (> burn-block-height (+ (get created-at game) GAME_TIMEOUT)))
      err-game-not-expired)
    (credit-balance (get player-a game) tokn refund)
    (credit-balance (get player-b game) tokn refund)
    (map-set accumulated-fees tokn
      (+ (get-accumulated-fees tokn) (* fee u2)))
    (map-set games game-id
      (merge game { status: "cancelled" }))
    (print {
      event: "game-cancelled",
      game-id: game-id,
      fee-per-player: fee,
      refund-per-player: refund
    })
    (ok true)
  )
)

(define-public (set-oracle (new-oracle principal))
  (begin
    (asserts! (is-eq tx-sender DEPLOYER) err-not-deployer)
    (var-set oracle new-oracle)
    (ok true)
  )
)

(define-public (set-fee-rate (new-fee-rate uint))
  (begin
    (asserts! (is-eq tx-sender DEPLOYER) err-not-deployer)
    (asserts! (<= new-fee-rate u2000) err-invalid-amount)
    (var-set fee-rate new-fee-rate)
    (ok true)
  )
)

(define-public (set-withdraw-fee-rate (new-fee-rate uint))
  (begin
    (asserts! (is-eq tx-sender DEPLOYER) err-not-deployer)
    (asserts! (<= new-fee-rate u1000) err-invalid-amount)
    (var-set withdraw-fee-rate new-fee-rate)
    (ok true)
  )
)

(define-public (set-treasury (new-treasury principal))
  (begin
    (asserts! (is-eq tx-sender DEPLOYER) err-not-deployer)
    (var-set treasury new-treasury)
    (ok true)
  )
)

(define-public (set-token-whitelist (token principal) (enabled bool))
  (begin
    (asserts! (is-eq tx-sender DEPLOYER) err-not-deployer)
    (map-set whitelisted-tokens token enabled)
    (print { event: "token-whitelist-updated", token: token, enabled: enabled })
    (ok true)
  )
)

(define-public (sweep-fees (token <sip-010-trait>) (token-name (string-ascii 128)))
  (let (
    (amount (get-accumulated-fees (contract-of token)))
    (tokn (contract-of token))
    (treas (var-get treasury))
  )
    (asserts! (is-eq tx-sender DEPLOYER) err-not-deployer)
    (asserts! (> amount u0) err-invalid-amount)
    (map-set accumulated-fees tokn u0)
    (try! (as-contract? ((with-ft tokn token-name amount))
      (try! (contract-call? token transfer amount current-contract treas none))))
    (print {
      event: "fees-swept",
      token: tokn,
      amount: amount,
      treasury: treas
    })
    (ok true)
  )
)

Functions (26)

FunctionAccessArgs
get-balanceread-onlypubkey: (buff 33
get-gameread-onlygame-id: uint
is-signature-usedread-onlymessage-hash: (buff 32
get-game-nonceread-only
get-oracleread-only
get-fee-rateread-only
get-withdraw-fee-rateread-only
get-accumulated-feesread-onlytoken: principal
is-token-whitelistedread-onlytoken: principal
get-registered-walletread-onlypubkey: (buff 33
verify-signatureprivatemessage-hash: (buff 32
consume-signatureprivatemessage-hash: (buff 32
credit-balanceprivatepubkey: (buff 33
debit-balanceprivatepubkey: (buff 33
depositpublictoken: <sip-010-trait>, amount: uint, pubkey: (buff 33
register-walletpublicpubkey: (buff 33
withdrawpublictoken: <sip-010-trait>, token-name: (string-ascii 128
create-gamepublicplayer-a: (buff 33
resolve-gamepublicgame-id: uint, winner: (buff 33
cancel-gamepublicgame-id: uint
set-oraclepublicnew-oracle: principal
set-fee-ratepublicnew-fee-rate: uint
set-withdraw-fee-ratepublicnew-fee-rate: uint
set-treasurypublicnew-treasury: principal
set-token-whitelistpublictoken: principal, enabled: bool
sweep-feespublictoken: <sip-010-trait>, token-name: (string-ascii 128