Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions lib/privy/public_api/services/wallets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,20 @@ def create(wallet_create_params:, idempotency_key: nil, request_options: nil)
# @option wallet_update_params [String, nil] :display_name A human-readable label for the wallet. Set to nil to clear.
# @option wallet_update_params [Array<Hash>, nil] :additional_signers Additional signers for the wallet.
# @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for owned wallets.
# @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the
# request expires. Defaults to the value computed by the client's
# PrivyRequestExpiryOptions.
# @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries).
#
# @return [Privy::Models::Wallet]
def update(wallet_id, wallet_update_params:, authorization_context: nil, request_options: nil)
def update(wallet_id, wallet_update_params:, authorization_context: nil, request_expiry: nil, request_options: nil)
prepared = Privy::Authorization.prepare_request(
privy_client,
method: :patch,
url: Privy::Authorization.signed_url(privy_client, "v1/wallets/#{wallet_id}"),
body: wallet_update_params,
authorization_context: authorization_context
authorization_context: authorization_context,
request_expiry: privy_client.compute_request_expiry(request_expiry)
)
combined_params = wallet_update_params.merge(request_options: request_options)
Privy::Authorization.merge_prepared_headers!(combined_params, prepared.headers)
Expand Down Expand Up @@ -93,6 +97,9 @@ def update(wallet_id, wallet_update_params:, authorization_context: nil, request
# @option wallet_rpc_request_body [Hash] :params Method-specific parameters.
# @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for owned wallets.
# @param idempotency_key [String, nil] Ensures the request is executed only once.
# @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the
# request expires. Defaults to the value computed by the client's
# PrivyRequestExpiryOptions.
# @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries).
#
# @return [Privy::Models::WalletRpcResponse]
Expand All @@ -101,6 +108,7 @@ def rpc(
wallet_rpc_request_body:,
authorization_context: nil,
idempotency_key: nil,
request_expiry: nil,
request_options: nil
)
prepared = Privy::Authorization.prepare_request(
Expand All @@ -109,7 +117,8 @@ def rpc(
url: Privy::Authorization.signed_url(privy_client, "v1/wallets/#{wallet_id}/rpc"),
body: wallet_rpc_request_body,
authorization_context: authorization_context,
idempotency_key: idempotency_key
idempotency_key: idempotency_key,
request_expiry: privy_client.compute_request_expiry(request_expiry)
)
combined_params = {wallet_rpc_request_body: wallet_rpc_request_body, request_options: request_options}
Privy::Authorization.merge_prepared_headers!(combined_params, prepared.headers)
Expand All @@ -133,6 +142,9 @@ def rpc(
# @option raw_sign_input [Hash] :params The signing parameters (required). Either {hash:} or {bytes:, encoding:, hash_function:}.
# @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for owned wallets.
# @param idempotency_key [String, nil] Ensures the request is executed only once.
# @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the
# request expires. Defaults to the value computed by the client's
# PrivyRequestExpiryOptions.
# @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries).
#
# @return [Privy::Models::RawSignResponse]
Expand All @@ -141,6 +153,7 @@ def raw_sign(
raw_sign_input:,
authorization_context: nil,
idempotency_key: nil,
request_expiry: nil,
request_options: nil
)
prepared = Privy::Authorization.prepare_request(
Expand All @@ -149,7 +162,8 @@ def raw_sign(
url: Privy::Authorization.signed_url(privy_client, "v1/wallets/#{wallet_id}/raw_sign"),
body: raw_sign_input,
authorization_context: authorization_context,
idempotency_key: idempotency_key
idempotency_key: idempotency_key,
request_expiry: privy_client.compute_request_expiry(request_expiry)
)
combined_params = raw_sign_input.merge(request_options: request_options)
Privy::Authorization.merge_prepared_headers!(combined_params, prepared.headers)
Expand Down Expand Up @@ -180,6 +194,9 @@ def raw_sign(
# @option wallet_transfer_params [Integer] :slippage_bps Maximum allowed slippage in basis points (1 bps = 0.01%).
# @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for owned wallets.
# @param idempotency_key [String, nil] Ensures the request is executed only once.
# @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the
# request expires. Defaults to the value computed by the client's
# PrivyRequestExpiryOptions.
# @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries).
#
# @return [Privy::Models::TransferActionResponse]
Expand All @@ -188,6 +205,7 @@ def transfer(
wallet_transfer_params:,
authorization_context: nil,
idempotency_key: nil,
request_expiry: nil,
request_options: nil
)
prepared = Privy::Authorization.prepare_request(
Expand All @@ -196,7 +214,8 @@ def transfer(
url: Privy::Authorization.signed_url(privy_client, "v1/wallets/#{wallet_id}/transfer"),
body: wallet_transfer_params,
authorization_context: authorization_context,
idempotency_key: idempotency_key
idempotency_key: idempotency_key,
request_expiry: privy_client.compute_request_expiry(request_expiry)
)
combined_params = wallet_transfer_params.merge(request_options: request_options)
Privy::Authorization.merge_prepared_headers!(combined_params, prepared.headers)
Expand Down
131 changes: 128 additions & 3 deletions test/privy/public_api/services/wallets_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,21 @@ def test_update_with_auth_context_signs_canonical_payload
)

captured_sig = nil
captured_expiry = nil
assert_requested(:patch, "#{BASE_URL}/v1/wallets/w-1") do |req|
captured_sig = req.headers["Privy-Authorization-Signature"]
captured_expiry = req.headers["Privy-Request-Expiry"]
end

expected_payload = Privy::Authorization.format_request_for_authorization_signature(
Privy::Authorization::WalletApiRequestSignatureInput.build(
method: :patch,
url: "#{BASE_URL}/v1/wallets/w-1",
body: {display_name: "new"},
headers: {"privy-app-id" => "app-abc"}
headers: {
"privy-app-id" => "app-abc",
"privy-request-expiry" => captured_expiry
}
)
)
pub = OpenSSL::PKey.read(kp.public_key.unpack1("m0"))
Expand Down Expand Up @@ -153,8 +158,10 @@ def test_rpc_with_auth_context_signs_body_and_sends_sig_header
)

captured_sig = nil
captured_expiry = nil
assert_requested(:post, "#{BASE_URL}/v1/wallets/w-1/rpc") do |req|
captured_sig = req.headers["Privy-Authorization-Signature"]
captured_expiry = req.headers["Privy-Request-Expiry"]
refute(req.headers.key?("Privy-Idempotency-Key"))
end

Expand All @@ -163,7 +170,10 @@ def test_rpc_with_auth_context_signs_body_and_sends_sig_header
method: :post,
url: "#{BASE_URL}/v1/wallets/w-1/rpc",
body: rpc_body,
headers: {"privy-app-id" => "app-abc"}
headers: {
"privy-app-id" => "app-abc",
"privy-request-expiry" => captured_expiry
}
)
)
pub = OpenSSL::PKey.read(kp.public_key.unpack1("m0"))
Expand All @@ -185,9 +195,11 @@ def test_rpc_with_auth_context_and_idempotency_signs_over_idempotency_header
)

captured_sig = nil
captured_expiry = nil
assert_requested(:post, "#{BASE_URL}/v1/wallets/w-1/rpc") do |req|
assert_equal("idem-both", req.headers["Privy-Idempotency-Key"])
captured_sig = req.headers["Privy-Authorization-Signature"]
captured_expiry = req.headers["Privy-Request-Expiry"]
end

expected_payload = Privy::Authorization.format_request_for_authorization_signature(
Expand All @@ -197,7 +209,8 @@ def test_rpc_with_auth_context_and_idempotency_signs_over_idempotency_header
body: rpc_body,
headers: {
"privy-app-id" => "app-abc",
"privy-idempotency-key" => "idem-both"
"privy-idempotency-key" => "idem-both",
"privy-request-expiry" => captured_expiry
}
)
)
Expand All @@ -208,4 +221,116 @@ def test_rpc_with_auth_context_and_idempotency_signs_over_idempotency_header
"signature must cover idempotency-key header"
)
end

# --- request_expiry on update ----------------------------------------------

def test_update_default_request_expiry_is_auto_set
stub_json(:patch, "#{BASE_URL}/v1/wallets/w-1", body: wallet_response_body(display_name: "n"))
before = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
build_client.wallets.update("w-1", wallet_update_params: {display_name: "n"})
assert_requested(:patch, "#{BASE_URL}/v1/wallets/w-1") do |req|
sent = req.headers["Privy-Request-Expiry"].to_i
assert_in_delta(before + (15 * 60 * 1_000), sent, 5_000)
end
end

def test_update_per_call_request_expiry_overrides_default
stub_json(:patch, "#{BASE_URL}/v1/wallets/w-1", body: wallet_response_body(display_name: "n"))
build_client.wallets.update(
"w-1",
wallet_update_params: {display_name: "n"},
request_expiry: 1_750_000_000_000
)
assert_requested(:patch, "#{BASE_URL}/v1/wallets/w-1") do |req|
assert_equal("1750000000000", req.headers["Privy-Request-Expiry"])
end
end

def test_update_disabled_options_omit_header
client = Privy::PrivyClient.new(
app_id: "app-abc",
app_secret: "secret",
environment: :staging,
request_expiry: Privy::PrivyRequestExpiryOptions.build(disabled: true)
)
stub_json(:patch, "#{BASE_URL}/v1/wallets/w-1", body: wallet_response_body(display_name: "n"))
client.wallets.update("w-1", wallet_update_params: {display_name: "n"})
assert_requested(:patch, "#{BASE_URL}/v1/wallets/w-1") do |req|
refute(req.headers.key?("Privy-Request-Expiry"))
end
end

def test_update_disabled_options_still_honor_explicit_override
client = Privy::PrivyClient.new(
app_id: "app-abc",
app_secret: "secret",
environment: :staging,
request_expiry: Privy::PrivyRequestExpiryOptions.build(disabled: true)
)
stub_json(:patch, "#{BASE_URL}/v1/wallets/w-1", body: wallet_response_body(display_name: "n"))
client.wallets.update(
"w-1",
wallet_update_params: {display_name: "n"},
request_expiry: 1_750_000_000_000
)
assert_requested(:patch, "#{BASE_URL}/v1/wallets/w-1") do |req|
assert_equal("1750000000000", req.headers["Privy-Request-Expiry"])
end
end

# --- request_expiry on rpc / raw_sign / transfer ---------------------------

RAW_SIGN_RESPONSE_BODY = {
method: "raw_sign",
data: {encoding: "hex", signature: "0xdeadbeef"}
}.to_json

TRANSFER_RESPONSE_BODY = {
id: "wa-1",
created_at: "2026-01-01T00:00:00Z",
destination_address: "0x0000000000000000000000000000000000000001",
source_chain: "base",
status: "submitted",
type: "transfer",
wallet_id: "w-1"
}.to_json

def test_rpc_per_call_request_expiry_overrides_default
stub_json(:post, "#{BASE_URL}/v1/wallets/w-1/rpc", body: RPC_RESPONSE_BODY)
build_client.wallets.rpc(
"w-1",
wallet_rpc_request_body: rpc_body,
request_expiry: 1_750_000_000_000
)
assert_requested(:post, "#{BASE_URL}/v1/wallets/w-1/rpc") do |req|
assert_equal("1750000000000", req.headers["Privy-Request-Expiry"])
end
end

def test_raw_sign_per_call_request_expiry_overrides_default
stub_json(:post, "#{BASE_URL}/v1/wallets/w-1/raw_sign", body: RAW_SIGN_RESPONSE_BODY)
build_client.wallets.raw_sign(
"w-1",
raw_sign_input: {params: {hash: "0x1234"}},
request_expiry: 1_750_000_000_000
)
assert_requested(:post, "#{BASE_URL}/v1/wallets/w-1/raw_sign") do |req|
assert_equal("1750000000000", req.headers["Privy-Request-Expiry"])
end
end

def test_transfer_per_call_request_expiry_overrides_default
stub_json(:post, "#{BASE_URL}/v1/wallets/w-1/transfer", body: TRANSFER_RESPONSE_BODY)
build_client.wallets.transfer(
"w-1",
wallet_transfer_params: {
source: {asset: "usdc", amount: "1", chain: "base"},
destination: {address: "0x0000000000000000000000000000000000000001"}
},
request_expiry: 1_750_000_000_000
)
assert_requested(:post, "#{BASE_URL}/v1/wallets/w-1/transfer") do |req|
assert_equal("1750000000000", req.headers["Privy-Request-Expiry"])
end
end
end
Loading