Module: WaveDispatch

Defined in:
lib/wave_dispatch.rb

Defined Under Namespace

Classes: Client

Constant Summary collapse

VERSION =
"0.6.3"

Class Method Summary collapse

Class Method Details

.sign_cdp_jwt(creds, accept) ⇒ Object

0.6.2 — CDP-JWT (ES256/P-256) signing via OpenSSL stdlib. Header: kid:<api_key>, typ:‘JWT’, nonce:<rand-hex16>. Payload: iss:‘cdp’, nbf:<now>, exp:<now+120>, uri:‘POST dispatch.wave.online<resource>’, claim:<accept>. ECDSA signs the base64url(header).base64url(payload) input; the DER signature is unpacked to raw 32+32 r||s (IEEE P-1363), which is exactly what JWS ES256 expects.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/wave_dispatch.rb', line 139

def self.sign_cdp_jwt(creds, accept)
  require "openssl"
  require "securerandom"
  nonce = SecureRandom.hex(16)
  now = Time.now.to_i
  uri = "POST dispatch.wave.online#{(accept || {})["resource"] || "/"}"
  header = { alg: "ES256", kid: creds[:api_key], typ: "JWT", nonce: nonce }
  payload = { sub: creds[:api_key], iss: "cdp", nbf: now, exp: now + 120, uri: uri, claim: accept }
  b64url = ->(s) { Base64.urlsafe_encode64(s).gsub("=", "") }
  to_sign = b64url.call(header.to_json) + "." + b64url.call(payload.to_json)
  key = OpenSSL::PKey.read(creds[:api_secret])
  raise "dispatch.wallet_hook(cdp): api_secret must be an EC P-256 private key" unless key.is_a?(OpenSSL::PKey::EC)
  der_sig = key.sign(OpenSSL::Digest.new("SHA256"), to_sign)
  # DER → raw r||s. OpenSSL ASN1 parses the SEQUENCE into [r, s] big-integers.
  asn1 = OpenSSL::ASN1.decode(der_sig)
  r = asn1.value[0].value.to_s(2).rjust(32, "\x00".b)
  s = asn1.value[1].value.to_s(2).rjust(32, "\x00".b)
  to_sign + "." + b64url.call(r + s)
end

.wallet_sign(provider, creds, challenge) ⇒ Object

Built-in provider sign — HTTP orchestration only; actual signing happens at the provider.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/wave_dispatch.rb', line 92

def self.wallet_sign(provider, creds, challenge)
  accepts = challenge.is_a?(Hash) ? (challenge["accepts"] || []) : []
  accept = accepts.find { |a| a.is_a?(Hash) && a["protocol"] == provider.to_s } || accepts.first || {}
  case provider
  when :cdp
    # 0.6.2 — real CDP-JWT (ES256/P-256) signing via OpenSSL stdlib. creds: {api_key, api_secret (PEM EC),
    # address?, network?}. The signed JWT is the cdp-payment header value; WAVE's CDP verifier validates
    # the signature on its side. No optional dep needed — Ruby's stdlib OpenSSL handles everything.
    raise "dispatch.wallet_hook(cdp): api_key required" unless creds[:api_key]
    raise "dispatch.wallet_hook(cdp): api_secret required (PEM EC private key)" unless creds[:api_secret]
    jwt = WaveDispatch.sign_cdp_jwt(creds, accept)
    { provider: "cdp", jwt: jwt, address: creds[:address],
      network: creds[:network] || "base", accept: accept }.to_json
  when :privy
    %i[app_id app_secret wallet_id].each { |k| raise "dispatch.wallet_hook(privy): #{k} required" unless creds[k] }
    basic = Base64.strict_encode64("#{creds[:app_id]}:#{creds[:app_secret]}")
    uri = URI("https://auth.privy.io/api/v1/wallets/#{URI.encode_www_form_component(creds[:wallet_id])}/rpc")
    req = Net::HTTP::Post.new(uri, "content-type" => "application/json",
                              "authorization" => "Basic #{basic}", "privy-app-id" => creds[:app_id])
    req.body = { method: "personal_sign", params: { message: accept.to_json }, chain_type: "ethereum" }.to_json
    res = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
                          verify_mode: OpenSSL::SSL::VERIFY_PEER) { |h| h.request(req) }
    raise "dispatch.wallet_hook(privy): provider #{res.code}" unless res.code.start_with?("2")
    j = (JSON.parse(res.body) rescue {})
    sig = (j.dig("data", "signature") || j["signature"])
    { provider: "privy", signature: sig, accept: accept }.to_json
  when :bridge
    raise "dispatch.wallet_hook(bridge): :api_key required" unless creds[:api_key]
    uri = URI("https://api.bridge.xyz/v0/transfers")
    req = Net::HTTP::Post.new(uri, "content-type" => "application/json", "api-key" => creds[:api_key])
    req.body = { source: creds[:source_wallet], destination: creds[:destination] || accept["payTo"],
                 amount: accept["maxAmountRequired"] }.to_json
    res = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
                          verify_mode: OpenSSL::SSL::VERIFY_PEER) { |h| h.request(req) }
    raise "dispatch.wallet_hook(bridge): provider #{res.code}" unless res.code.start_with?("2")
    j = (JSON.parse(res.body) rescue {})
    { provider: "bridge", id: j["id"], accept: accept }.to_json
  else
    raise "dispatch.wallet_sign: unsupported provider #{provider.inspect}"
  end
end