clihelper.polymarket.psm1

#!/usr/bin/env pwsh
using namespace System.IO
using namespace System.Numerics
using namespace System.Net.Http
using namespace System.Management.Automation
using namespace System.Security.Cryptography

#Requires -Modules PsModuleBase, argparser, cliHelper.logger

#region Classes

enum CallType {
  Invalid = 0
  Call = 1
  DelegateCall = 2
}

class PolymarketConstants {
  static [string] $WALLET_FACTORY_SALT = "0x95b55433048f12da29cefb1c3d26bd930c115b4cebf578715c569c1b281a0c1f"

  # Pre-computed implementation hash - avoids storing full bytecode
  static [string] $WALLET_IMPLEMENTATION_HASH = "0x933430a293d31ac8fbd9852d4c46b6d23bf20a49d1c1964f30bd674e598e38b1"
}

class Transaction {
  [ValidatePattern('^0x[0-9a-fA-F]{40}$')]
  [string] $To

  [CallType] $TypeCode

  [ValidatePattern('^0x[0-9a-fA-F]*$')]
  [string] $Data

  [ValidatePattern('^\d+$')]
  [string] $Value

  Transaction([string]$to, [CallType]$typeCode, [string]$data, [string]$value = "0") {
    $this.To = $to
    $this.TypeCode = $typeCode
    $this.Data = $data
    $this.Value = $value
  }

  [bool] Equals([object]$obj) {
    if ($obj -isnot [Transaction]) { return $false }
    return $this.To -eq $obj.To -and
    $this.TypeCode -eq $obj.TypeCode -and
    $this.Data -eq $obj.Data -and
    $this.Value -eq $obj.Value
  }

  [int] GetHashCode() {
    return $this.To.GetHashCode() -bxor $this.TypeCode.GetHashCode() -bxor $this.Data.GetHashCode() -bxor $this.Value.GetHashCode()
  }
}

class Address {
  static [bool] IsValid([string]$address) {
    return $address -match '^0x[0-9a-fA-F]{40}$'
  }

  static [string] Normalize([string]$address) {
    if (-not [Address]::IsValid($address)) {
      throw [ArgumentException] "Invalid address format: $address"
    }
    return $address.ToLower()
  }
}

class HexString {
  static [bool] IsValid([string]$hex) {
    return $hex -match '^0x[0-9a-fA-F]*$'
  }

  static [byte[]] ToBytes([string]$hex) {
    if (-not [HexString]::IsValid($hex)) {
      throw [ArgumentException] "Invalid hex format: $hex"
    }
    $hex = $hex.Substring(2)
    if (($hex.Length % 2) -ne 0) {
      $hex = "0$hex"
    }
    $bytes = New-Object byte[] ($hex.Length / 2)
    for ($i = 0; $i -lt $hex.Length; $i += 2) {
      $bytes[$i / 2] = [Convert]::ToByte($hex.Substring($i, 2), 16)
    }
    return $bytes
  }

  static [string] FromBytes([byte[]]$bytes) {
    return "0x" + [BitConverter]::ToString($bytes).Replace("-", "").ToLower()
  }

  static [string] Normalize([string]$hex) {
    if (-not [HexString]::IsValid($hex)) {
      throw [ArgumentException] "Invalid hex format: $hex"
    }
    return $hex.ToLower()
  }
}

class AbiEncoding {
  static [string] Strip0x([string]$value) {
    if ($null -eq $value) { throw [ArgumentNullException]::new("value") }
    return $value.StartsWith("0x") ? $value.Substring(2) : $value
  }

  static [string] UInt256Word([string]$value) {
    if ([string]::IsNullOrWhiteSpace($value)) {
      throw [ArgumentException] "uint256 value cannot be empty"
    }
    $word = if ($value.StartsWith("0x")) {
      [AbiEncoding]::Strip0x($value).ToLower()
    } else {
      [BigInteger]::Parse($value).ToString("x")
    }
    if ($word.Length -gt 64) { throw [ArgumentException] "uint256 value out of range: $value" }
    return $word.PadLeft(64, '0')
  }

  static [string] AddressWord([string]$address) {
    return [Address]::Normalize($address).Substring(2).PadLeft(64, '0')
  }

  static [string] Bytes32Word([string]$hex) {
    if (-not [HexString]::IsValid($hex)) { throw [ArgumentException] "Invalid hex format: $hex" }
    $raw = [AbiEncoding]::Strip0x($hex).ToLower()
    if ($raw.Length -gt 64) { throw [ArgumentException] "bytes32 value too long: $hex" }
    return $raw.PadLeft(64, '0')
  }

  static [string] DynamicUint256Array([string[]]$values) {
    $encoded = [AbiEncoding]::UInt256Word(($values.Count).ToString())
    foreach ($value in $values) {
      $encoded += [AbiEncoding]::UInt256Word($value)
    }
    return $encoded
  }

  static [string] DynamicString([string]$value) {
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($value)
    $hex = [BitConverter]::ToString($bytes).Replace("-", "").ToLower()
    $padding = (64 - ($hex.Length % 64)) % 64
    return [AbiEncoding]::UInt256Word($bytes.Length.ToString()) + $hex + ("0" * $padding)
  }
}


#region Polymarket.Crypto
class PolymarketCrypto {
  static [string] Keccak256([byte[]]$data) {
    $sha3 = [System.Security.Cryptography.SHA3_256]::Create()
    $hash = $sha3.ComputeHash($data)
    return "0x" + [BitConverter]::ToString($hash).Replace("-", "").ToLower()
  }

  static [string] Keccak256([string]$hex) {
    if (-not [HexString]::IsValid($hex)) {
      throw [ArgumentException] "Invalid hex format: $hex"
    }
    $bytes = [HexString]::ToBytes($hex)
    return [PolymarketCrypto]::Keccak256($bytes)
  }

  static [string] EncodePacked($types, $values) {
    $bytes = @()
    for ($i = 0; $i -lt $types.Count; $i++) {
      $type = $types[$i]
      $value = $values[$i]

      switch ($type) {
        "string" {
          $bytes += [System.Text.Encoding]::UTF8.GetBytes($value)
        }
        "address" {
          $bytes += [HexString]::ToBytes($value)
        }
        "bytes" {
          $bytes += [HexString]::ToBytes($value)
        }
        "uint256" {
          $bigInt = [BigInteger]::Parse($value)
          $byteArray = $bigInt.ToByteArray()
          [Array]::Reverse($byteArray)
          $padded = New-Object byte[] 32
          [Array]::Copy($byteArray, 0, $padded, 32 - $byteArray.Length, $byteArray.Length)
          $bytes += $padded
        }
      }
    }
    return [HexString]::FromBytes($bytes)
  }

  static [string] GetCreate2Address([string]$from, [string]$salt, [string]$bytecodeHash) {
    $fromBytes = [HexString]::ToBytes($from)
    $saltBytes = [HexString]::ToBytes($salt)
    $bytecodeHashBytes = [HexString]::ToBytes($bytecodeHash)

    $prefix = [HexString]::ToBytes("0xff")
    $data = $prefix + $fromBytes + $saltBytes + $bytecodeHashBytes
    $hash = [PolymarketCrypto]::Keccak256($data)

    $hashBytes = [HexString]::ToBytes($hash)
    $addressBytes = $hashBytes[12..31]
    return "0x" + [BitConverter]::ToString($addressBytes).Replace("-", "").ToLower()
  }
}

#endregion Polymarket.Crypto

class PolymarketUtils {
  static [string] GetIndexSet([int[]]$indexes) {
    $result = [BigInteger]::Zero
    foreach ($index in $indexes) {
      if ($index -lt 0 -or $index -gt 255) {
        throw [ArgumentException] "Index must be between 0 and 255"
      }
      $result = $result -bor ([BigInteger]::One -shl $index)
    }

    # Convert to hex and ensure exactly 64 characters
    # BigInteger.ToString("X") may add a leading zero if the high bit is set
    $hex = $result.ToString("X").ToLower()
    if ($hex.StartsWith("0") -and $hex.Length -gt 1) {
      $hex = $hex.Substring(1)
    }
    return "0x" + $hex.PadLeft(64, '0')
  }

  static [int] GetMarketIndex([string]$marketId) {
    if (-not [HexString]::IsValid($marketId)) {
      throw [ArgumentException] "Invalid market ID format"
    }
    $bytes = [HexString]::ToBytes($marketId)
    if ($bytes.Length -ne 32) {
      throw [ArgumentException] "Market ID must be 32 bytes"
    }
    return [Convert]::ToInt32($marketId.Substring($marketId.Length - 2), 16)
  }

  static [Transaction] Erc20ApprovalTransaction([string]$token, [string]$spender, [string]$amount) {
    $token = [Address]::Normalize($token)
    $spender = [Address]::Normalize($spender)

    $data = "0x095ea7b3" + $spender.Substring(2).PadLeft(64, '0') + [AbiEncoding]::UInt256Word($amount)

    return [Transaction]::new($token, [CallType]::Call, $data, "0")
  }

  static [Transaction] Erc1155ApprovalTransaction([string]$token, [string]$spender, [bool]$approved) {
    $token = [Address]::Normalize($token)
    $spender = [Address]::Normalize($spender)
    $approvedHex = if ($approved) { "1".PadLeft(64, '0') } else { "0".PadLeft(64, '0') }

    $data = "0xa22cb465" + $spender.Substring(2).PadLeft(64, '0') + $approvedHex

    return [Transaction]::new($token, [CallType]::Call, $data, "0")
  }

  static [Transaction] Erc20TransferTransaction([string]$token, [string]$to, [string]$value) {
    $token = [Address]::Normalize($token)
    $to = [Address]::Normalize($to)

    $data = "0xa9059cbb" + $to.Substring(2).PadLeft(64, '0') + [AbiEncoding]::UInt256Word($value)

    return [Transaction]::new($token, [CallType]::Call, $data, "0")
  }

  static [Transaction] EthTransferTransaction([string]$to, [string]$value) {
    $to = [Address]::Normalize($to)
    return [Transaction]::new($to, [CallType]::Call, "0x", $value)
  }
}

class ConditionalTokens {
  static [string] $ZeroHash = "0x0000000000000000000000000000000000000000000000000000000000000000"
  static [Transaction[]] SplitPosition(
    [string]$conditionalTokensAddress,
    [string]$collateralTokenAddress,
    [string]$conditionId,
    [int]$outcomeSlotCount,
    [string]$amount
  ) {
    $conditionalTokensAddress = [Address]::Normalize($conditionalTokensAddress)
    $collateralTokenAddress = [Address]::Normalize($collateralTokenAddress)

    $parentCollectionId = [ConditionalTokens]::ZeroHash
    $partition = @()
    for ($i = 0; $i -lt $outcomeSlotCount; $i++) {
      $partition += [BigInteger]::One -shl $i
    }

    $data = [ConditionalTokens]::EncodeSplit(
      $collateralTokenAddress,
      $parentCollectionId,
      $conditionId,
      $partition,
      $amount
    )

    return @([Transaction]::new($conditionalTokensAddress, [CallType]::Call, $data, "0"))
  }

  static [Transaction[]] MergePositions(
    [string]$conditionalTokensAddress,
    [string]$collateralTokenAddress,
    [string]$conditionId,
    [int]$outcomeSlotCount,
    [string]$amount
  ) {
    $conditionalTokensAddress = [Address]::Normalize($conditionalTokensAddress)
    $collateralTokenAddress = [Address]::Normalize($collateralTokenAddress)

    $parentCollectionId = [ConditionalTokens]::ZeroHash
    $partition = @()
    for ($i = 0; $i -lt $outcomeSlotCount; $i++) {
      $partition += [BigInteger]::One -shl $i
    }

    $data = [ConditionalTokens]::EncodeMerge(
      $collateralTokenAddress,
      $parentCollectionId,
      $conditionId,
      $partition,
      $amount
    )

    return @([Transaction]::new($conditionalTokensAddress, [CallType]::Call, $data, "0"))
  }

  static [Transaction[]] RedeemPositions(
    [string]$conditionalTokensAddress,
    [string]$collateralTokenAddress,
    [string]$conditionId,
    [int]$outcomeSlotCount
  ) {
    $conditionalTokensAddress = [Address]::Normalize($conditionalTokensAddress)
    $collateralTokenAddress = [Address]::Normalize($collateralTokenAddress)

    $parentCollectionId = [ConditionalTokens]::ZeroHash
    $partition = @()
    for ($i = 0; $i -lt $outcomeSlotCount; $i++) {
      $partition += [BigInteger]::One -shl $i
    }

    $data = [ConditionalTokens]::EncodeRedeem(
      $collateralTokenAddress,
      $parentCollectionId,
      $conditionId,
      $partition
    )

    return @([Transaction]::new($conditionalTokensAddress, [CallType]::Call, $data, "0"))
  }

  hidden static [string] EncodeSplit(
    [string]$collateralTokenAddress,
    [string]$parentCollectionId,
    [string]$conditionId,
    [BigInteger[]]$partition,
    [string]$amount
  ) {
    $selector = "0x1859a15a"
    $collateral = $collateralTokenAddress.Substring(2).PadLeft(64, '0')
    $parent = $parentCollectionId.Substring(2).PadLeft(64, '0')
    $condition = $conditionId.Substring(2).PadLeft(64, '0')

    $offset = "00000000000000000000000000000000000000000000000000000000000000a0"
    $amountHex = [AbiEncoding]::UInt256Word($amount)

    $partitionLength = $partition.Count.ToString("X").PadLeft(64, '0')
    $partitionHex = ""
    foreach ($p in $partition) {
      $partitionHex += $p.ToString("X64").ToLower()
    }

    return $selector + $collateral + $parent + $condition + $offset + $amountHex + $partitionLength + $partitionHex
  }

  hidden static [string] EncodeMerge(
    [string]$collateralTokenAddress,
    [string]$parentCollectionId,
    [string]$conditionId,
    [BigInteger[]]$partition,
    [string]$amount
  ) {
    $selector = "0x7a29d439"
    $collateral = $collateralTokenAddress.Substring(2).PadLeft(64, '0')
    $parent = $parentCollectionId.Substring(2).PadLeft(64, '0')
    $condition = $conditionId.Substring(2).PadLeft(64, '0')

    $offset = "00000000000000000000000000000000000000000000000000000000000000a0"
    $amountHex = [AbiEncoding]::UInt256Word($amount)

    $partitionLength = $partition.Count.ToString("X").PadLeft(64, '0')
    $partitionHex = ""
    foreach ($p in $partition) {
      $partitionHex += $p.ToString("X64").ToLower()
    }

    return $selector + $collateral + $parent + $condition + $offset + $amountHex + $partitionLength + $partitionHex
  }

  hidden static [string] EncodeRedeem(
    [string]$collateralTokenAddress,
    [string]$parentCollectionId,
    [string]$conditionId,
    [BigInteger[]]$partition
  ) {
    $selector = "0x721bf40f"
    $collateral = $collateralTokenAddress.Substring(2).PadLeft(64, '0')
    $parent = $parentCollectionId.Substring(2).PadLeft(64, '0')
    $condition = $conditionId.Substring(2).PadLeft(64, '0')

    $offset = "0000000000000000000000000000000000000000000000000000000000000080"

    $partitionLength = $partition.Count.ToString("X").PadLeft(64, '0')
    $partitionHex = ""
    foreach ($p in $partition) {
      $partitionHex += $p.ToString("X64").ToLower()
    }

    return $selector + $collateral + $parent + $condition + $offset + $partitionLength + $partitionHex
  }
}


#region Polymarket.Debt
class Debt {
  static [Transaction[]] PayDebt([string]$debtTracker, [string]$tokenAddress, [string]$amount) {
    $debtTracker = [Address]::Normalize($debtTracker)
    $tokenAddress = [Address]::Normalize($tokenAddress)

    $approvalTx = [PolymarketUtils]::Erc20ApprovalTransaction($tokenAddress, $debtTracker, $amount)

    $selector = "0x74179a25"
    $amountHex = $amount.PadLeft(64, '0')
    $data = $selector + $amountHex

    $payTx = [Transaction]::new($debtTracker, [CallType]::Call, $data, "0")

    return @($approvalTx, $payTx)
  }

  static [Transaction[]] TakeOnDebt([string]$debtTracker, [string]$amount, [string]$txHash) {
    $debtTracker = [Address]::Normalize($debtTracker)

    $selector = "0x80b27022"
    $amountHex = [AbiEncoding]::UInt256Word($amount)
    $txHashHex = [AbiEncoding]::Bytes32Word($txHash)
    $data = $selector + $amountHex + $txHashHex

    return @([Transaction]::new($debtTracker, [CallType]::Call, $data, "0"))
  }
}

#endregion Polymarket.Debt


#region Polymarket.Markets

class Markets {
  static [Transaction[]] BuyMarketOutcome(
    [string]$marketMakerAddress,
    [string]$collateralTokenAddress,
    [string]$investmentAmount,
    [int]$outcomeIndex,
    [string]$minOutcomeTokensToBuy
  ) {
    $marketMakerAddress = [Address]::Normalize($marketMakerAddress)
    $collateralTokenAddress = [Address]::Normalize($collateralTokenAddress)

    $approvalTx = [PolymarketUtils]::Erc20ApprovalTransaction($collateralTokenAddress, $marketMakerAddress, $investmentAmount)

    $selector = "0x6d4ce63c"
    $investmentHex = [AbiEncoding]::UInt256Word($investmentAmount)
    $outcomeHex = $outcomeIndex.ToString("X64").ToLower()
    $minTokensHex = [AbiEncoding]::UInt256Word($minOutcomeTokensToBuy)
    $data = $selector + $investmentHex + $outcomeHex + $minTokensHex

    $buyTx = [Transaction]::new($marketMakerAddress, [CallType]::Call, $data, "0")

    return @($approvalTx, $buyTx)
  }

  static [Transaction[]] SellMarketOutcome(
    [string]$marketMakerAddress,
    [string]$conditionalTokensAddress,
    [string]$returnAmount,
    [int]$outcomeIndex,
    [string]$maxOutcomeTokensToSell
  ) {
    $marketMakerAddress = [Address]::Normalize($marketMakerAddress)
    $conditionalTokensAddress = [Address]::Normalize($conditionalTokensAddress)

    $approveOn = [PolymarketUtils]::Erc1155ApprovalTransaction($conditionalTokensAddress, $marketMakerAddress, $true)

    $selector = "0xd96a094a"
    $returnHex = [AbiEncoding]::UInt256Word($returnAmount)
    $outcomeHex = $outcomeIndex.ToString("X64").ToLower()
    $maxTokensHex = [AbiEncoding]::UInt256Word($maxOutcomeTokensToSell)
    $data = $selector + $returnHex + $outcomeHex + $maxTokensHex

    $sellTx = [Transaction]::new($marketMakerAddress, [CallType]::Call, $data, "0")

    $approveOff = [PolymarketUtils]::Erc1155ApprovalTransaction($conditionalTokensAddress, $marketMakerAddress, $false)

    return @($approveOn, $sellTx, $approveOff)
  }

  static [Transaction[]] AddFundingToMarket(
    [string]$marketMakerAddress,
    [string]$collateralTokenAddress,
    [string]$investmentAmount,
    [BigInteger[]]$distributionHint = @()
  ) {
    $marketMakerAddress = [Address]::Normalize($marketMakerAddress)
    $collateralTokenAddress = [Address]::Normalize($collateralTokenAddress)

    $approvalTx = [PolymarketUtils]::Erc20ApprovalTransaction($collateralTokenAddress, $marketMakerAddress, $investmentAmount)

    $selector = "0x21a0848a"
    $investmentHex = [AbiEncoding]::UInt256Word($investmentAmount)
    $offset = "0000000000000000000000000000000000000000000000000000000000000060"

    $hintLength = $distributionHint.Count.ToString("X").PadLeft(64, '0')
    $hintHex = ""
    foreach ($hint in $distributionHint) {
      $hintHex += $hint.ToString("X64").ToLower()
    }

    $data = $selector + $investmentHex + $offset + $hintLength + $hintHex

    $addFundingTx = [Transaction]::new($marketMakerAddress, [CallType]::Call, $data, "0")

    return @($approvalTx, $addFundingTx)
  }

  static [Transaction[]] SafeAddFundingToMarket(
    [string]$slippageCheckerAddress,
    [string]$marketMakerAddress,
    [string]$collateralTokenAddress,
    [string]$investmentAmount,
    [string[]]$distributionHint = @(),
    [string[]]$positionIds,
    [string[]]$minRefunds,
    [string[]]$maxRefunds
  ) {
    $slippageCheckerAddress = [Address]::Normalize($slippageCheckerAddress)
    $marketMakerAddress = [Address]::Normalize($marketMakerAddress)
    $collateralTokenAddress = [Address]::Normalize($collateralTokenAddress)

    $approvalTx = [PolymarketUtils]::Erc20ApprovalTransaction($collateralTokenAddress, $marketMakerAddress, $investmentAmount)

    $selector = "0x5ff9af63"
    $headSizeBytes = 32 * 6
    $tail1 = [AbiEncoding]::DynamicUint256Array($distributionHint)
    $tail2 = [AbiEncoding]::DynamicUint256Array($positionIds)
    $tail3 = [AbiEncoding]::DynamicUint256Array($minRefunds)
    $tail4 = [AbiEncoding]::DynamicUint256Array($maxRefunds)

    $offset1 = [AbiEncoding]::UInt256Word(($headSizeBytes).ToString())
    $offset2 = [AbiEncoding]::UInt256Word(($headSizeBytes + ($tail1.Length / 2)).ToString())
    $offset3 = [AbiEncoding]::UInt256Word(($headSizeBytes + ($tail1.Length / 2) + ($tail2.Length / 2)).ToString())
    $offset4 = [AbiEncoding]::UInt256Word(($headSizeBytes + ($tail1.Length / 2) + ($tail2.Length / 2) + ($tail3.Length / 2)).ToString())

    $data = $selector +
    [AbiEncoding]::AddressWord($marketMakerAddress) +
    [AbiEncoding]::UInt256Word($investmentAmount) +
    $offset1 + $offset2 + $offset3 + $offset4 +
    $tail1 + $tail2 + $tail3 + $tail4

    $safeAddFundingTx = [Transaction]::new($slippageCheckerAddress, [CallType]::DelegateCall, $data, "0")

    return @($approvalTx, $safeAddFundingTx)
  }

  static [Transaction[]] RemoveFundingFromMarket(
    [string]$marketMakerAddress,
    [string]$shares
  ) {
    $marketMakerAddress = [Address]::Normalize($marketMakerAddress)

    $selector = "0xc5d36504"
    $sharesHex = [AbiEncoding]::UInt256Word($shares)
    $data = $selector + $sharesHex

    return @([Transaction]::new($marketMakerAddress, [CallType]::Call, $data, "0"))
  }
}

#endregion Polymarket.Markets

#region Polymarket.Matic

class Matic {
  static [Transaction[]] WithdrawFundsOnMatic(
    [string]$tokenAddress,
    [string]$withdrawAmount
  ) {
    $tokenAddress = [Address]::Normalize($tokenAddress)

    $selector = "0x2e1a7d4d"
    $amountHex = $withdrawAmount.PadLeft(64, '0')
    $data = $selector + $amountHex

    return @([Transaction]::new($tokenAddress, [CallType]::Call, $data, "0"))
  }
}

#endregion Polymarket.Matic


#region Polymarket.NegRisk
class NegRisk {
  static [Transaction] ConvertPositions(
    [string]$negRiskAdapterAddress,
    [string]$marketId,
    [string]$indexSet,
    [string]$amount
  ) {
    $negRiskAdapterAddress = [Address]::Normalize($negRiskAdapterAddress)

    $selector = "0x18509c6b"
    $marketIdHex = [AbiEncoding]::Bytes32Word($marketId)
    $indexSetHex = [AbiEncoding]::UInt256Word($indexSet)
    $amountHex = [AbiEncoding]::UInt256Word($amount)
    $data = $selector + $marketIdHex + $indexSetHex + $amountHex

    return [Transaction]::new($negRiskAdapterAddress, [CallType]::Call, $data, "0")
  }

  static [Transaction] RedeemPositions([string]$negRiskAdapterAddress, [string]$conditionId, [string[]]$amounts) {
    $negRiskAdapterAddress = [Address]::Normalize($negRiskAdapterAddress)

    $selector = "0xb5d00289"
    $conditionIdHex = [AbiEncoding]::Bytes32Word($conditionId)
    $offset = "0000000000000000000000000000000000000000000000000000000000000040"
    $length = [AbiEncoding]::UInt256Word("2")
    $amount0 = [AbiEncoding]::UInt256Word($amounts[0])
    $amount1 = [AbiEncoding]::UInt256Word($amounts[1])
    $data = $selector + $conditionIdHex + $offset + $length + $amount0 + $amount1

    return [Transaction]::new($negRiskAdapterAddress, [CallType]::Call, $data, "0")
  }
}

class Liquidity {
  static [Transaction[]] AddLiquidityRequest(
    [string]$liquidityRequestLogAddress,
    [string]$reason,
    [string]$marketMakerAddress,
    [string]$tradeAmount
  ) {
    $liquidityRequestLogAddress = [Address]::Normalize($liquidityRequestLogAddress)
    $marketMakerAddress = [Address]::Normalize($marketMakerAddress)

    $selector = "0xde17a736"
    $offsetReason = [AbiEncoding]::UInt256Word("96")
    $head = $offsetReason + [AbiEncoding]::AddressWord($marketMakerAddress) + [AbiEncoding]::UInt256Word($tradeAmount)
    $tail = [AbiEncoding]::DynamicString($reason)
    $data = $selector + $head + $tail

    return @([Transaction]::new($liquidityRequestLogAddress, [CallType]::Call, $data, "0"))
  }
}

#endregion Polymarket.NegRisk

# Main class
class Polymarket {
  # Main Polymarket SDK class

  static [string] WriteBanner() {
    return [string][PsModuleBase]::ReadModuledata("clihelper.polymarket", "polymarketlogo")
  }

  static [string] GetProxyWalletAddress([string]$factory, [string]$user) {
    $factory = [Address]::Normalize($factory)
    $user = [Address]::Normalize($user)

    $userSalt = [PolymarketCrypto]::Keccak256([PolymarketCrypto]::EncodePacked(@("address"), @($user)))
    $deployCode = [Polymarket]::AssembleProxyWalletDeployCode($factory)
    $bytecodeHash = [PolymarketCrypto]::Keccak256($deployCode)

    return [PolymarketCrypto]::GetCreate2Address($factory, $userSalt, $bytecodeHash)
  }

  hidden static [string] AssembleProxyWalletDeployCode([string]$factory) {
    $factory = [Address]::Normalize($factory)
    $encodedBytes = "0x"
    $encodedBytes = $encodedBytes.Substring(2)

    $implAddress = [Polymarket]::GetProxyWalletImplementationAddress($factory)
    $implAddress = $implAddress.Substring(2).ToLower()
    $code = '0x3d3d606380380380913d393d73{0}5af4602a57600080fd5b602d8060366000396000f3363d3d373d3d3d363d73{1}5af43d82803e903d91602b57fd5bf352e831dd{2}' -f $($factory.Substring(2).ToLower()), $implAddress, $encodedBytes
    return $code
  }

  hidden static [string] GetProxyWalletImplementationAddress([string]$factory) {
    $factory = [Address]::Normalize($factory)
    return [PolymarketCrypto]::GetCreate2Address(
      $factory,
      [PolymarketConstants]::WALLET_FACTORY_SALT,
      [PolymarketConstants]::WALLET_IMPLEMENTATION_HASH
    )
  }

  static [string] GetIndexSet([int[]]$indexes) {
    return [PolymarketUtils]::GetIndexSet($indexes)
  }

  static [int] GetMarketIndex([string]$marketId) {
    return [PolymarketUtils]::GetMarketIndex($marketId)
  }

  static [Transaction] Erc20ApprovalTransaction([string]$token, [string]$spender, [string]$amount) {
    return [PolymarketUtils]::Erc20ApprovalTransaction($token, $spender, $amount)
  }

  static [Transaction] Erc1155ApprovalTransaction([string]$token, [string]$spender, [bool]$approved) {
    return [PolymarketUtils]::Erc1155ApprovalTransaction($token, $spender, $approved)
  }

  static [Transaction] Erc20TransferTransaction([string]$token, [string]$to, [string]$value) {
    return [PolymarketUtils]::Erc20TransferTransaction($token, $to, $value)
  }

  static [Transaction] EthTransferTransaction([string]$to, [string]$value) {
    return [PolymarketUtils]::EthTransferTransaction($to, $value)
  }

  static [Transaction[]] SplitPosition(
    [string]$conditionalTokensAddress,
    [string]$collateralTokenAddress,
    [string]$conditionId,
    [int]$outcomeSlotCount,
    [string]$amount
  ) {
    return [ConditionalTokens]::SplitPosition(
      $conditionalTokensAddress,
      $collateralTokenAddress,
      $conditionId,
      $outcomeSlotCount,
      $amount
    )
  }

  static [Transaction[]] MergePositions(
    [string]$conditionalTokensAddress,
    [string]$collateralTokenAddress,
    [string]$conditionId,
    [int]$outcomeSlotCount,
    [string]$amount
  ) {
    return [ConditionalTokens]::MergePositions(
      $conditionalTokensAddress,
      $collateralTokenAddress,
      $conditionId,
      $outcomeSlotCount,
      $amount
    )
  }

  static [Transaction[]] RedeemPositions(
    [string]$conditionalTokensAddress,
    [string]$collateralTokenAddress,
    [string]$conditionId,
    [int]$outcomeSlotCount
  ) {
    return [ConditionalTokens]::RedeemPositions(
      $conditionalTokensAddress,
      $collateralTokenAddress,
      $conditionId,
      $outcomeSlotCount
    )
  }

  static [Transaction[]] PayDebt(
    [string]$debtTracker,
    [string]$tokenAddress,
    [string]$amount
  ) {
    return [Debt]::PayDebt($debtTracker, $tokenAddress, $amount)
  }

  static [Transaction[]] TakeOnDebt(
    [string]$debtTracker,
    [string]$amount,
    [string]$txHash
  ) {
    return [Debt]::TakeOnDebt($debtTracker, $amount, $txHash)
  }

  static [Transaction[]] BuyMarketOutcome(
    [string]$marketMakerAddress,
    [string]$collateralTokenAddress,
    [string]$investmentAmount,
    [int]$outcomeIndex,
    [string]$minOutcomeTokensToBuy
  ) {
    return [Markets]::BuyMarketOutcome(
      $marketMakerAddress,
      $collateralTokenAddress,
      $investmentAmount,
      $outcomeIndex,
      $minOutcomeTokensToBuy
    )
  }

  static [Transaction[]] SellMarketOutcome(
    [string]$marketMakerAddress,
    [string]$conditionalTokensAddress,
    [string]$returnAmount,
    [int]$outcomeIndex,
    [string]$maxOutcomeTokensToSell
  ) {
    return [Markets]::SellMarketOutcome(
      $marketMakerAddress,
      $conditionalTokensAddress,
      $returnAmount,
      $outcomeIndex,
      $maxOutcomeTokensToSell
    )
  }

  static [Transaction[]] AddFundingToMarket(
    [string]$marketMakerAddress,
    [string]$collateralTokenAddress,
    [string]$investmentAmount,
    [BigInteger[]]$distributionHint = @()
  ) {
    return [Markets]::AddFundingToMarket(
      $marketMakerAddress,
      $collateralTokenAddress,
      $investmentAmount,
      $distributionHint
    )
  }

  static [Transaction[]] SafeAddFundingToMarket(
    [string]$slippageCheckerAddress,
    [string]$marketMakerAddress,
    [string]$collateralTokenAddress,
    [string]$investmentAmount,
    [string[]]$distributionHint = @(),
    [string[]]$positionIds,
    [string[]]$minRefunds,
    [string[]]$maxRefunds
  ) {
    return [Markets]::SafeAddFundingToMarket(
      $slippageCheckerAddress,
      $marketMakerAddress,
      $collateralTokenAddress,
      $investmentAmount,
      $distributionHint,
      $positionIds,
      $minRefunds,
      $maxRefunds
    )
  }

  static [Transaction[]] AddLiquidityRequest(
    [string]$liquidityRequestLogAddress,
    [string]$reason,
    [string]$marketMakerAddress,
    [string]$tradeAmount
  ) {
    return [Liquidity]::AddLiquidityRequest($liquidityRequestLogAddress, $reason, $marketMakerAddress, $tradeAmount)
  }

  static [Transaction[]] RemoveFundingFromMarket(
    [string]$marketMakerAddress,
    [string]$shares
  ) {
    return [Markets]::RemoveFundingFromMarket($marketMakerAddress, $shares)
  }

  static [Transaction[]] WithdrawFundsOnMatic(
    [string]$tokenAddress,
    [string]$withdrawAmount
  ) {
    return [Matic]::WithdrawFundsOnMatic($tokenAddress, $withdrawAmount)
  }

  static [Transaction] ConvertPositions(
    [string]$negRiskAdapterAddress,
    [string]$marketId,
    [string]$indexSet,
    [string]$amount
  ) {
    return [NegRisk]::ConvertPositions($negRiskAdapterAddress, $marketId, $indexSet, $amount)
  }

  static [Transaction] RedeemPositionsNegRisk(
    [string]$negRiskAdapterAddress,
    [string]$conditionId,
    [string[]]$amounts
  ) {
    return [NegRisk]::RedeemPositions($negRiskAdapterAddress, $conditionId, $amounts)
  }
}

#endregion Classes

# Types that will be available to users when they import the module.
$typestoExport = @(
  [Polymarket],
  [CallType],
  [Transaction],
  [Address],
  [HexString],
  [PolymarketCrypto],
  [PolymarketConstants],
  [PolymarketUtils],
  [ConditionalTokens],
  [Debt],
  [Markets],
  [Matic],
  [NegRisk],
  [Liquidity],
  [AbiEncoding]
)
$TypeAcceleratorsClass = [PsObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
foreach ($Type in $typestoExport) {
  if ($Type.FullName -in $TypeAcceleratorsClass::Get.Keys) {
    $Message = @(
      "Unable to register type accelerator '$($Type.FullName)'"
      'Accelerator already exists.'
    ) -join ' - '
    "TypeAcceleratorAlreadyExists $Message" | Write-Debug
  }
}
# Add type accelerators for every exportable type.
foreach ($Type in $typestoExport) {
  $TypeAcceleratorsClass::Add($Type.FullName, $Type)
}
# Remove type accelerators when the module is removed.
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
  foreach ($Type in $typestoExport) {
    $TypeAcceleratorsClass::Remove($Type.FullName)
  }
}.GetNewClosure();

$scripts = @();
$Public = Get-ChildItem "$PSScriptRoot/Public" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue
$scripts += Get-ChildItem "$PSScriptRoot/Private" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue
$scripts += $Public

foreach ($file in $scripts) {
  try {
    if ([string]::IsNullOrWhiteSpace($file.fullname)) { continue }
    . "$($file.fullname)"
  } catch {
    Write-Warning "Failed to import function $($file.BaseName): $_"
    $host.UI.WriteErrorLine($_)
  }
}

$Param = @{
  Function = $Public.BaseName
  Cmdlet   = '*'
  Alias    = '*'
  Verbose  = $false
}
Export-ModuleMember @Param