erpc-sdk 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,6 +30,11 @@ module ERPC
30
30
  STATUS_NOT_FOUND = "BRIDGE_STATUS_NOT_FOUND"
31
31
  TIMEOUT = "BRIDGE_TIMEOUT"
32
32
  ABORTED = "BRIDGE_ABORTED"
33
+ LOCAL_RPC_REQUIRED = "BRIDGE_LOCAL_RPC_REQUIRED"
34
+ SOURCE_RPC_TRANSPORT = "BRIDGE_SOURCE_RPC_TRANSPORT"
35
+ SOURCE_RPC_INVALID_RESPONSE = "BRIDGE_SOURCE_RPC_INVALID_RESPONSE"
36
+ LOCAL_PLAN_INVALID = "BRIDGE_LOCAL_PLAN_INVALID"
37
+ LOCAL_BUILD_INVALID = "BRIDGE_LOCAL_BUILD_INVALID"
33
38
 
34
39
  BRIDGE_INVALID_ARGUMENT = INVALID_ARGUMENT
35
40
  BRIDGE_UNSUPPORTED_ROUTE = UNSUPPORTED_ROUTE
@@ -44,6 +49,11 @@ module ERPC
44
49
  BRIDGE_STATUS_NOT_FOUND = STATUS_NOT_FOUND
45
50
  BRIDGE_TIMEOUT = TIMEOUT
46
51
  BRIDGE_ABORTED = ABORTED
52
+ BRIDGE_LOCAL_RPC_REQUIRED = LOCAL_RPC_REQUIRED
53
+ BRIDGE_SOURCE_RPC_TRANSPORT = SOURCE_RPC_TRANSPORT
54
+ BRIDGE_SOURCE_RPC_INVALID_RESPONSE = SOURCE_RPC_INVALID_RESPONSE
55
+ BRIDGE_LOCAL_PLAN_INVALID = LOCAL_PLAN_INVALID
56
+ BRIDGE_LOCAL_BUILD_INVALID = LOCAL_BUILD_INVALID
47
57
 
48
58
  ALL = [
49
59
  INVALID_ARGUMENT,
@@ -58,7 +68,12 @@ module ERPC
58
68
  BUILD_INVALID,
59
69
  STATUS_NOT_FOUND,
60
70
  TIMEOUT,
61
- ABORTED
71
+ ABORTED,
72
+ LOCAL_RPC_REQUIRED,
73
+ SOURCE_RPC_TRANSPORT,
74
+ SOURCE_RPC_INVALID_RESPONSE,
75
+ LOCAL_PLAN_INVALID,
76
+ LOCAL_BUILD_INVALID
62
77
  ].freeze
63
78
  end
64
79
 
@@ -75,7 +90,12 @@ module ERPC
75
90
  BridgeErrorCode::BUILD_INVALID => "Bridge provider build is invalid",
76
91
  BridgeErrorCode::STATUS_NOT_FOUND => "Bridge status was not found",
77
92
  BridgeErrorCode::TIMEOUT => "Bridge provider request timed out",
78
- BridgeErrorCode::ABORTED => "Bridge provider request was aborted"
93
+ BridgeErrorCode::ABORTED => "Bridge provider request was aborted",
94
+ BridgeErrorCode::LOCAL_RPC_REQUIRED => "Bridge local source RPC is required",
95
+ BridgeErrorCode::SOURCE_RPC_TRANSPORT => "Bridge source RPC transport failed",
96
+ BridgeErrorCode::SOURCE_RPC_INVALID_RESPONSE => "Bridge source RPC response is invalid",
97
+ BridgeErrorCode::LOCAL_PLAN_INVALID => "Bridge local source-swap plan is invalid",
98
+ BridgeErrorCode::LOCAL_BUILD_INVALID => "Bridge local unsigned build is invalid"
79
99
  }.freeze
80
100
 
81
101
  class BridgeError < Error
@@ -122,7 +142,7 @@ module ERPC
122
142
 
123
143
  attr_reader :builder_endpoint, :explorer_endpoint, :builder_api_key,
124
144
  :allow_unauthenticated_build, :minimum_quote_validity_seconds,
125
- :timeout, :http_adapter
145
+ :timeout, :http_adapter, :local_build
126
146
 
127
147
  def initialize(builder_endpoint: DEFAULT_BUILDER_ENDPOINT,
128
148
  explorer_endpoint: DEFAULT_EXPLORER_ENDPOINT,
@@ -131,6 +151,7 @@ module ERPC
131
151
  minimum_quote_validity_seconds: 60,
132
152
  timeout: DEFAULT_TIMEOUT,
133
153
  http_adapter: nil,
154
+ local_build: nil,
134
155
  **aliases)
135
156
  if aliases.key?(:builderEndpoint)
136
157
  builder_endpoint = aliases.delete(:builderEndpoint)
@@ -162,6 +183,9 @@ module ERPC
162
183
  if aliases.key?(:httpAdapter)
163
184
  http_adapter = aliases.delete(:httpAdapter)
164
185
  end
186
+ if aliases.key?(:localBuild)
187
+ local_build = aliases.delete(:localBuild)
188
+ end
165
189
  fail_bridge(BridgeErrorCode::INVALID_ARGUMENT) unless aliases.empty?
166
190
 
167
191
  @builder_endpoint = normalize_endpoint(builder_endpoint)
@@ -180,11 +204,31 @@ module ERPC
180
204
  if !http_adapter.nil? && !http_adapter.respond_to?(:request)
181
205
  fail_bridge(BridgeErrorCode::INVALID_ARGUMENT)
182
206
  end
207
+ if !local_build.nil?
208
+ unless local_build.is_a?(Hash) && local_build.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) }
209
+ fail_bridge(BridgeErrorCode::INVALID_ARGUMENT)
210
+ end
211
+ allowed_local = %w[sourceSwapEndpoint source_swap_endpoint ethereumRpc ethereum_rpc solanaRpc solana_rpc]
212
+ fail_bridge(BridgeErrorCode::INVALID_ARGUMENT) unless local_build.keys.all? { |key| allowed_local.include?(key.to_s) }
213
+ local_build = local_build.each_with_object({}) do |(key, value), copy|
214
+ normalized_key = {
215
+ "sourceSwapEndpoint" => :source_swap_endpoint,
216
+ "source_swap_endpoint" => :source_swap_endpoint,
217
+ "ethereumRpc" => :ethereum_rpc,
218
+ "ethereum_rpc" => :ethereum_rpc,
219
+ "solanaRpc" => :solana_rpc,
220
+ "solana_rpc" => :solana_rpc
221
+ }.fetch(key.to_s)
222
+ fail_bridge(BridgeErrorCode::INVALID_ARGUMENT) if copy.key?(normalized_key)
223
+ copy[normalized_key] = value
224
+ end
225
+ end
183
226
 
184
227
  @allow_unauthenticated_build = allow_unauthenticated_build
185
228
  @minimum_quote_validity_seconds = minimum_quote_validity_seconds
186
229
  @timeout = timeout.to_f
187
230
  @http_adapter = http_adapter
231
+ @local_build = local_build
188
232
  freeze
189
233
  end
190
234
 
@@ -196,7 +240,8 @@ module ERPC
196
240
  "builder_api_key=#{key.inspect} " \
197
241
  "allow_unauthenticated_build=#{allow_unauthenticated_build.inspect} " \
198
242
  "minimum_quote_validity_seconds=#{minimum_quote_validity_seconds.inspect} " \
199
- "timeout=#{timeout.inspect} http_adapter=#{adapter.inspect}>"
243
+ "timeout=#{timeout.inspect} http_adapter=#{adapter.inspect} " \
244
+ "local_build=#{local_build.nil? ? nil : '[configured]'}>"
200
245
  end
201
246
 
202
247
  alias to_s inspect
@@ -304,15 +349,17 @@ module ERPC
304
349
  JsonNode = Struct.new(:value, :start, :end, :object_entries, :array_items, :raw_number,
305
350
  keyword_init: true)
306
351
  DirectionFacts = Struct.new(
307
- :bridge_capability_id, :source_chain_id, :destination_chain_id,
352
+ :asset, :bridge_capability_id, :source_chain_id, :destination_chain_id,
308
353
  :source_token_deployment_id, :destination_token_deployment_id,
309
354
  :source_token_address, :destination_token_address,
310
355
  :source_token_standard, :destination_token_standard,
311
356
  :source_provider_chain_id, :destination_provider_chain_id,
312
357
  :source_wormhole_chain_id, :destination_wormhole_chain_id,
313
- :source_name, :destination_name, :source_eurc_mint, :destination_eurc_mint,
358
+ :source_name, :destination_name, :source_token_name, :destination_token_name,
359
+ :source_token_mint, :destination_token_mint,
314
360
  :source_usdc_deployment_id, :source_usdc_address, :source_usdc_standard,
315
- :swift_contract, keyword_init: true
361
+ :swift_contract, :forwarder_function_selector, :jupiter_program_address,
362
+ :dependencies, :source_swap_required, keyword_init: true
316
363
  )
317
364
  NormalizedRoute = Struct.new(:request, :facts, :capability, keyword_init: true)
318
365
 
@@ -508,7 +555,10 @@ module ERPC
508
555
  SOLANA_SWIFT_PROGRAM = "mayan34VedncxdK2XobtvWFDXQASUTBXhUVzt2kKgny"
509
556
  ETHEREUM_FORWARDER = "0x337685fdab40d39bd02028545a4ffa7d287cc3e2"
510
557
  SOLANA_JUPITER_V6 = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"
511
- ETHEREUM_FORWARDER_SELECTOR = "0x30dedc57"
558
+ ETHEREUM_EURC_FORWARDER_SELECTOR = "0x30dedc57"
559
+ ETHEREUM_USDC_FORWARDER_SELECTOR = "0xe4269fc4"
560
+ ETHEREUM_FORWARDER_SELECTOR = ETHEREUM_EURC_FORWARDER_SELECTOR
561
+ MAYAN_USDC_MINT = "A9mUU4qviSctJVPJdBJWkb28deg915LYJKrzQ19ji3FM"
512
562
  DEFAULT_BUILDER_ENDPOINT = MayanSwiftV2BridgeConfig::DEFAULT_BUILDER_ENDPOINT
513
563
  DEFAULT_EXPLORER_ENDPOINT = MayanSwiftV2BridgeConfig::DEFAULT_EXPLORER_ENDPOINT
514
564
  DEFAULT_TIMEOUT = MayanSwiftV2BridgeConfig::DEFAULT_TIMEOUT
@@ -528,6 +578,14 @@ module ERPC
528
578
  "wormhole-guardian-messaging",
529
579
  "mayan-explorer-indexer"
530
580
  ].freeze
581
+ DIRECT_USDC_DEPENDENCIES = [
582
+ "mayan-hosted-quote-api",
583
+ "mayan-hosted-transaction-builder",
584
+ "swift-auction-solvers",
585
+ "relayers",
586
+ "wormhole-guardian-messaging",
587
+ "mayan-explorer-indexer"
588
+ ].freeze
531
589
 
532
590
  EVM_ADDRESS = /\A0x[0-9a-fA-F]{40}\z/.freeze
533
591
  EVM_HASH = /\A0x[0-9a-fA-F]{64}\z/.freeze
@@ -671,7 +729,8 @@ module ERPC
671
729
  allow_unauthenticated_build: value.allow_unauthenticated_build,
672
730
  minimum_quote_validity_seconds: value.minimum_quote_validity_seconds,
673
731
  timeout: value.timeout,
674
- http_adapter: injected_adapter
732
+ http_adapter: injected_adapter,
733
+ local_build: value.local_build
675
734
  )
676
735
  end
677
736
  elsif value.is_a?(Hash)
@@ -683,7 +742,8 @@ module ERPC
683
742
  "allowUnauthenticatedBuild" => :allow_unauthenticated_build,
684
743
  "minimumQuoteValiditySeconds" => :minimum_quote_validity_seconds,
685
744
  "timeoutMs" => :timeout_ms,
686
- "httpAdapter" => :http_adapter
745
+ "httpAdapter" => :http_adapter,
746
+ "localBuild" => :local_build
687
747
  }
688
748
  normalized = {}
689
749
  source.each do |key, item|
@@ -754,39 +814,63 @@ module ERPC
754
814
  value
755
815
  end
756
816
 
757
- def direction_facts(source_chain_id, destination_chain_id)
817
+ def direction_facts(source_chain_id, destination_chain_id, source_token_id, destination_token_id)
758
818
  if source_chain_id == ETHEREUM_CHAIN_ID && destination_chain_id == SOLANA_CHAIN_ID
819
+ is_eurc = source_token_id == ETHEREUM_EURC_DEPLOYMENT_ID && destination_token_id == SOLANA_EURC_DEPLOYMENT_ID
820
+ is_usdc = source_token_id == ETHEREUM_USDC_DEPLOYMENT_ID && destination_token_id == SOLANA_USDC_DEPLOYMENT_ID
821
+ fail_bridge(BridgeErrorCode::UNSUPPORTED_ROUTE) unless is_eurc || is_usdc
822
+ asset = is_eurc ? "eurc" : "usdc"
759
823
  return DirectionFacts.new(
760
- bridge_capability_id: "bridge-mayan-swift-v2-eurc-eth-sol",
824
+ asset: asset,
825
+ bridge_capability_id: is_eurc ? "bridge-mayan-swift-v2-eurc-eth-sol" : "bridge-mayan-swift-v2-usdc-eth-sol",
761
826
  source_chain_id: source_chain_id, destination_chain_id: destination_chain_id,
762
- source_token_deployment_id: ETHEREUM_EURC_DEPLOYMENT_ID,
763
- destination_token_deployment_id: SOLANA_EURC_DEPLOYMENT_ID,
764
- source_token_address: ETHEREUM_EURC_ADDRESS, destination_token_address: SOLANA_EURC_ADDRESS,
827
+ source_token_deployment_id: source_token_id, destination_token_deployment_id: destination_token_id,
828
+ source_token_address: is_eurc ? ETHEREUM_EURC_ADDRESS : ETHEREUM_USDC_ADDRESS,
829
+ destination_token_address: is_eurc ? SOLANA_EURC_ADDRESS : SOLANA_USDC_ADDRESS,
765
830
  source_token_standard: "erc20", destination_token_standard: "spl-token",
766
831
  source_provider_chain_id: ETHEREUM_PROVIDER_CHAIN_ID, destination_provider_chain_id: SOLANA_PROVIDER_CHAIN_ID,
767
832
  source_wormhole_chain_id: ETHEREUM_WORMHOLE_CHAIN_ID, destination_wormhole_chain_id: SOLANA_WORMHOLE_CHAIN_ID,
768
833
  source_name: ETHEREUM_NAME, destination_name: SOLANA_NAME,
769
- source_eurc_mint: "", destination_eurc_mint: SOLANA_EURC_ADDRESS,
834
+ source_token_name: is_eurc ? "EuroC" : "USD Coin",
835
+ destination_token_name: is_eurc ? "EuroC" : "USD Coin",
836
+ source_token_mint: is_eurc ? "" : MAYAN_USDC_MINT,
837
+ destination_token_mint: is_eurc ? SOLANA_EURC_ADDRESS : SOLANA_USDC_ADDRESS,
770
838
  source_usdc_deployment_id: ETHEREUM_USDC_DEPLOYMENT_ID,
771
839
  source_usdc_address: ETHEREUM_USDC_ADDRESS, source_usdc_standard: "erc20",
772
- swift_contract: ETHEREUM_SWIFT_CONTRACT
840
+ swift_contract: ETHEREUM_SWIFT_CONTRACT,
841
+ forwarder_function_selector: is_eurc ? ETHEREUM_EURC_FORWARDER_SELECTOR : ETHEREUM_USDC_FORWARDER_SELECTOR,
842
+ jupiter_program_address: nil,
843
+ dependencies: is_eurc ? BASE_DEPENDENCIES : DIRECT_USDC_DEPENDENCIES,
844
+ source_swap_required: is_eurc
773
845
  )
774
846
  end
775
847
  if source_chain_id == SOLANA_CHAIN_ID && destination_chain_id == ETHEREUM_CHAIN_ID
848
+ is_eurc = source_token_id == SOLANA_EURC_DEPLOYMENT_ID && destination_token_id == ETHEREUM_EURC_DEPLOYMENT_ID
849
+ is_usdc = source_token_id == SOLANA_USDC_DEPLOYMENT_ID && destination_token_id == ETHEREUM_USDC_DEPLOYMENT_ID
850
+ fail_bridge(BridgeErrorCode::UNSUPPORTED_ROUTE) unless is_eurc || is_usdc
851
+ asset = is_eurc ? "eurc" : "usdc"
776
852
  return DirectionFacts.new(
777
- bridge_capability_id: "bridge-mayan-swift-v2-eurc-sol-eth",
853
+ asset: asset,
854
+ bridge_capability_id: is_eurc ? "bridge-mayan-swift-v2-eurc-sol-eth" : "bridge-mayan-swift-v2-usdc-sol-eth",
778
855
  source_chain_id: source_chain_id, destination_chain_id: destination_chain_id,
779
- source_token_deployment_id: SOLANA_EURC_DEPLOYMENT_ID,
780
- destination_token_deployment_id: ETHEREUM_EURC_DEPLOYMENT_ID,
781
- source_token_address: SOLANA_EURC_ADDRESS, destination_token_address: ETHEREUM_EURC_ADDRESS,
856
+ source_token_deployment_id: source_token_id, destination_token_deployment_id: destination_token_id,
857
+ source_token_address: is_eurc ? SOLANA_EURC_ADDRESS : SOLANA_USDC_ADDRESS,
858
+ destination_token_address: is_eurc ? ETHEREUM_EURC_ADDRESS : ETHEREUM_USDC_ADDRESS,
782
859
  source_token_standard: "spl-token", destination_token_standard: "erc20",
783
860
  source_provider_chain_id: SOLANA_PROVIDER_CHAIN_ID, destination_provider_chain_id: ETHEREUM_PROVIDER_CHAIN_ID,
784
861
  source_wormhole_chain_id: SOLANA_WORMHOLE_CHAIN_ID, destination_wormhole_chain_id: ETHEREUM_WORMHOLE_CHAIN_ID,
785
862
  source_name: SOLANA_NAME, destination_name: ETHEREUM_NAME,
786
- source_eurc_mint: SOLANA_EURC_ADDRESS, destination_eurc_mint: "",
863
+ source_token_name: is_eurc ? "EuroC" : "USD Coin",
864
+ destination_token_name: is_eurc ? "EuroC" : "USD Coin",
865
+ source_token_mint: is_eurc ? SOLANA_EURC_ADDRESS : SOLANA_USDC_ADDRESS,
866
+ destination_token_mint: is_eurc ? "" : MAYAN_USDC_MINT,
787
867
  source_usdc_deployment_id: SOLANA_USDC_DEPLOYMENT_ID,
788
868
  source_usdc_address: SOLANA_USDC_ADDRESS, source_usdc_standard: "spl-token",
789
- swift_contract: SOLANA_SWIFT_PROGRAM
869
+ swift_contract: SOLANA_SWIFT_PROGRAM,
870
+ forwarder_function_selector: nil,
871
+ jupiter_program_address: is_eurc ? SOLANA_JUPITER_V6 : nil,
872
+ dependencies: is_eurc ? BASE_DEPENDENCIES + ["jupiter-v6-source-swap"] : DIRECT_USDC_DEPENDENCIES,
873
+ source_swap_required: is_eurc
790
874
  )
791
875
  end
792
876
  fail_bridge(BridgeErrorCode::UNSUPPORTED_ROUTE)
@@ -834,10 +918,10 @@ module ERPC
834
918
  "sourceUsdcAddress" => facts.source_usdc_address, "sourceUsdcStandard" => facts.source_usdc_standard,
835
919
  "sourceUsdcDecimals" => 6, "swiftContract" => facts.swift_contract,
836
920
  "forwarderAddress" => facts.source_chain_id == ETHEREUM_CHAIN_ID ? ETHEREUM_FORWARDER : nil,
837
- "forwarderFunctionSelector" => facts.source_chain_id == ETHEREUM_CHAIN_ID ? ETHEREUM_FORWARDER_SELECTOR : nil,
838
- "jupiterProgramAddress" => facts.source_chain_id == SOLANA_CHAIN_ID ? SOLANA_JUPITER_V6 : nil,
921
+ "forwarderFunctionSelector" => facts.forwarder_function_selector,
922
+ "jupiterProgramAddress" => facts.jupiter_program_address,
839
923
  "builderEndpoint" => DEFAULT_BUILDER_ENDPOINT, "explorerEndpoint" => DEFAULT_EXPLORER_ENDPOINT,
840
- "dependencies" => facts.source_chain_id == SOLANA_CHAIN_ID ? BASE_DEPENDENCIES + ["jupiter-v6-source-swap"] : BASE_DEPENDENCIES,
924
+ "dependencies" => facts.dependencies,
841
925
  "status" => "active"
842
926
  }
843
927
  fail_bridge(BridgeErrorCode::UNSUPPORTED_ROUTE) unless strict_equal?(capability, expected)
@@ -853,8 +937,7 @@ module ERPC
853
937
  destination_token_id = require_string(request["destinationTokenDeploymentId"], BridgeErrorCode::INVALID_ARGUMENT)
854
938
  amount = normalize_positive_uint64(request["amountIn"], BridgeErrorCode::INVALID_ARGUMENT).first
855
939
  slippage = normalize_slippage(request["slippageBps"], BridgeErrorCode::INVALID_ARGUMENT)
856
- facts = direction_facts(source_chain_id, destination_chain_id)
857
- fail_bridge(BridgeErrorCode::UNSUPPORTED_ROUTE) unless source_token_id == facts.source_token_deployment_id && destination_token_id == facts.destination_token_deployment_id
940
+ facts = direction_facts(source_chain_id, destination_chain_id, source_token_id, destination_token_id)
858
941
  unless catalog_token_matches?(facts.source_token_deployment_id, facts.source_chain_id, facts.source_token_address, facts.source_token_standard) &&
859
942
  catalog_token_matches?(facts.destination_token_deployment_id, facts.destination_chain_id, facts.destination_token_address, facts.destination_token_standard) &&
860
943
  catalog_token_matches?(facts.source_usdc_deployment_id, facts.source_chain_id, facts.source_usdc_address, facts.source_usdc_standard)
@@ -934,12 +1017,12 @@ module ERPC
934
1017
  EVM_ADDRESS.match?(value) ? value.downcase : value
935
1018
  end
936
1019
 
937
- def provider_token(node, address:, standard:, chain_id:, wormhole_chain_id:, mint:)
1020
+ def provider_token(node, address:, standard:, chain_id:, wormhole_chain_id:, mint:, name:)
938
1021
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless node.object_entries && node.value.is_a?(Hash)
939
1022
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless provider_address_equals?(provider_string(node, "contract"), address)
940
1023
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless provider_string(node, "mint", allow_empty: true) == mint
941
1024
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless provider_address_equals?(provider_string(node, "realOriginContractAddress"), address)
942
- fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless provider_string(node, "name") == "EuroC"
1025
+ fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless provider_string(node, "name") == name
943
1026
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless provider_string(node, "standard") == standard
944
1027
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless provider_integer(node, "chainId") == chain_id
945
1028
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless provider_integer(node, "wChainId") == wormhole_chain_id
@@ -972,8 +1055,8 @@ module ERPC
972
1055
  deadline = provider_uint64(node, "deadline64", positive: true)
973
1056
  ensure_quote_deadline(deadline.last)
974
1057
 
975
- provider_token(required_node(node, "fromToken"), address: facts.source_token_address, standard: facts.source_chain_id == ETHEREUM_CHAIN_ID ? "erc20" : "spl", chain_id: facts.source_provider_chain_id, wormhole_chain_id: facts.source_wormhole_chain_id, mint: facts.source_eurc_mint)
976
- provider_token(required_node(node, "toToken"), address: facts.destination_token_address, standard: facts.destination_chain_id == ETHEREUM_CHAIN_ID ? "erc20" : "spl", chain_id: facts.destination_provider_chain_id, wormhole_chain_id: facts.destination_wormhole_chain_id, mint: facts.destination_eurc_mint)
1058
+ provider_token(required_node(node, "fromToken"), address: facts.source_token_address, standard: facts.source_chain_id == ETHEREUM_CHAIN_ID ? "erc20" : "spl", chain_id: facts.source_provider_chain_id, wormhole_chain_id: facts.source_wormhole_chain_id, mint: facts.source_token_mint, name: facts.source_token_name)
1059
+ provider_token(required_node(node, "toToken"), address: facts.destination_token_address, standard: facts.destination_chain_id == ETHEREUM_CHAIN_ID ? "erc20" : "spl", chain_id: facts.destination_provider_chain_id, wormhole_chain_id: facts.destination_wormhole_chain_id, mint: facts.destination_token_mint, name: facts.destination_token_name)
977
1060
  provider_standard = facts.source_chain_id == ETHEREUM_CHAIN_ID ? "erc20" : "spl"
978
1061
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless provider_address(node, "swiftInputContract", facts.source_usdc_address) == facts.source_usdc_address
979
1062
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless provider_string(node, "swiftInputContractStandard") == provider_standard
@@ -982,20 +1065,24 @@ module ERPC
982
1065
 
983
1066
  middle = required_node(node, "minMiddleAmount")
984
1067
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless middle.raw_number && middle.value.is_a?(Numeric) && middle.value.finite? && middle.value.positive?
985
- if facts.source_chain_id == ETHEREUM_CHAIN_ID
1068
+ evm_router = object_value(node, "evmSwapRouterAddress")
1069
+ if facts.source_swap_required && facts.source_chain_id == ETHEREUM_CHAIN_ID
986
1070
  router_address = normalize_evm_address(provider_string(node, "evmSwapRouterAddress"), BridgeErrorCode::PROVIDER_INVALID_RESPONSE)
987
1071
  router_kind = "provider-selected-evm"
988
- else
1072
+ elsif facts.source_swap_required
989
1073
  router_address = SOLANA_JUPITER_V6
990
1074
  router_kind = "jupiter-v6"
991
- evm_router = object_value(node, "evmSwapRouterAddress")
992
1075
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless evm_router == :__bridge_missing__ || evm_router.nil?
1076
+ else
1077
+ fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless evm_router == :__bridge_missing__ || evm_router.nil?
1078
+ router_address = nil
1079
+ router_kind = nil
993
1080
  end
994
1081
  quote_id = provider_string(node, "quoteId")
995
1082
  signature = provider_string(node, "signature")
996
1083
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless QUOTE_ID.match?(quote_id) && EVM_SIGNATURE.match?(signature)
997
1084
 
998
- dependencies = facts.source_chain_id == SOLANA_CHAIN_ID ? BASE_DEPENDENCIES + ["jupiter-v6-source-swap"] : BASE_DEPENDENCIES
1085
+ dependencies = facts.dependencies
999
1086
  {
1000
1087
  "quoteKind" => "mayan-swift-v2", "providerId" => "mayan-swift-v2",
1001
1088
  "sourceChainId" => request.fetch("sourceChainId"), "destinationChainId" => request.fetch("destinationChainId"),
@@ -1006,7 +1093,8 @@ module ERPC
1006
1093
  "deadline" => deadline.first, "slippageBps" => request.fetch("slippageBps"),
1007
1094
  "quoteId" => quote_id.downcase, "providerSignature" => signature.downcase,
1008
1095
  "sourceSwap" => {
1009
- "required" => true, "inputTokenDeploymentId" => facts.source_token_deployment_id,
1096
+ "required" => facts.source_swap_required,
1097
+ "inputTokenDeploymentId" => facts.source_swap_required ? facts.source_token_deployment_id : facts.source_usdc_deployment_id,
1010
1098
  "intermediateTokenDeploymentId" => facts.source_usdc_deployment_id,
1011
1099
  "intermediateTokenAddress" => facts.source_usdc_address,
1012
1100
  "intermediateTokenStandard" => provider_standard, "intermediateTokenDecimals" => 6,
@@ -1110,8 +1198,14 @@ module ERPC
1110
1198
  fail_bridge(code) unless quote["quoteKind"] == "mayan-swift-v2" && quote["providerId"] == "mayan-swift-v2"
1111
1199
  source_chain = require_string(quote["sourceChainId"], code)
1112
1200
  destination_chain = require_string(quote["destinationChainId"], code)
1113
- facts = direction_facts(source_chain, destination_chain)
1114
- fail_bridge(code) unless quote["sourceTokenDeploymentId"] == facts.source_token_deployment_id && quote["destinationTokenDeploymentId"] == facts.destination_token_deployment_id
1201
+ source_token_id = require_string(quote["sourceTokenDeploymentId"], code)
1202
+ destination_token_id = require_string(quote["destinationTokenDeploymentId"], code)
1203
+ facts = begin
1204
+ direction_facts(source_chain, destination_chain, source_token_id, destination_token_id)
1205
+ rescue BridgeError => error
1206
+ fail_bridge(code) if error.code == BridgeErrorCode::UNSUPPORTED_ROUTE
1207
+ ERPC.raise_safe_bridge_error(error)
1208
+ end
1115
1209
  amount = normalize_positive_uint64(quote["amountIn"], code)
1116
1210
  expected_amount = normalize_positive_uint64(quote["expectedAmountOut"], code)
1117
1211
  minimum_amount = normalize_positive_uint64(quote["minimumAmountOut"], code)
@@ -1126,9 +1220,13 @@ module ERPC
1126
1220
  source_swap = require_record(quote["sourceSwap"], code)
1127
1221
  exact_keys(source_swap, SOURCE_SWAP_KEYS, code)
1128
1222
  provider_standard = facts.source_chain_id == ETHEREUM_CHAIN_ID ? "erc20" : "spl"
1129
- fail_bridge(code) unless source_swap["required"] == true && source_swap["inputTokenDeploymentId"] == facts.source_token_deployment_id && source_swap["intermediateTokenDeploymentId"] == facts.source_usdc_deployment_id && source_swap["intermediateTokenAddress"] == facts.source_usdc_address && source_swap["intermediateTokenStandard"] == provider_standard && source_swap["intermediateTokenDecimals"] == 6
1223
+ fail_bridge(code) unless source_swap["required"] == facts.source_swap_required && source_swap["inputTokenDeploymentId"] == (facts.source_swap_required ? facts.source_token_deployment_id : facts.source_usdc_deployment_id) && source_swap["intermediateTokenDeploymentId"] == facts.source_usdc_deployment_id && source_swap["intermediateTokenAddress"] == facts.source_usdc_address && source_swap["intermediateTokenStandard"] == provider_standard && source_swap["intermediateTokenDecimals"] == 6
1130
1224
  provider_minimum = require_string(source_swap["providerMinimumAmount"], code)
1131
- if facts.source_chain_id == ETHEREUM_CHAIN_ID
1225
+ if !facts.source_swap_required
1226
+ fail_bridge(code) unless source_swap["routerKind"].nil? && source_swap["routerAddress"].nil?
1227
+ normalized_router = nil
1228
+ router_kind = nil
1229
+ elsif facts.source_chain_id == ETHEREUM_CHAIN_ID
1132
1230
  router = source_swap["routerAddress"]
1133
1231
  fail_bridge(code) unless source_swap["routerKind"] == "provider-selected-evm" && router.is_a?(String) && EVM_ADDRESS.match?(router) && router.downcase != "0x#{'0' * 40}"
1134
1232
  normalized_router = router.downcase
@@ -1138,7 +1236,7 @@ module ERPC
1138
1236
  normalized_router = SOLANA_JUPITER_V6
1139
1237
  router_kind = "jupiter-v6"
1140
1238
  end
1141
- dependencies = facts.source_chain_id == SOLANA_CHAIN_ID ? BASE_DEPENDENCIES + ["jupiter-v6-source-swap"] : BASE_DEPENDENCIES
1239
+ dependencies = facts.dependencies
1142
1240
  fail_bridge(code) unless source_swap.is_a?(Hash) && quote["dependencies"].is_a?(Array) && strict_equal?(quote["dependencies"], dependencies)
1143
1241
  raw = require_string(quote["rawSignedQuoteJson"], code)
1144
1242
  fail_bridge(code) if raw.bytesize > MAX_RAW_QUOTE_BYTES
@@ -1152,7 +1250,8 @@ module ERPC
1152
1250
  "deadline" => deadline.first, "slippageBps" => slippage,
1153
1251
  "quoteId" => quote_id.downcase, "providerSignature" => signature.downcase,
1154
1252
  "sourceSwap" => {
1155
- "required" => true, "inputTokenDeploymentId" => facts.source_token_deployment_id,
1253
+ "required" => facts.source_swap_required,
1254
+ "inputTokenDeploymentId" => facts.source_swap_required ? facts.source_token_deployment_id : facts.source_usdc_deployment_id,
1156
1255
  "intermediateTokenDeploymentId" => facts.source_usdc_deployment_id,
1157
1256
  "intermediateTokenAddress" => facts.source_usdc_address,
1158
1257
  "intermediateTokenStandard" => provider_standard, "intermediateTokenDecimals" => 6,
@@ -1193,7 +1292,7 @@ module ERPC
1193
1292
  value.is_a?(String) && (value == "0" || value.match?(/\A0x0+\z/i))
1194
1293
  end
1195
1294
 
1196
- def validate_evm_build_result(wrapper, swapper_address)
1295
+ def validate_evm_build_result(wrapper, swapper_address, facts)
1197
1296
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless provider_string(wrapper, "chainCategory") == "evm" && provider_string(wrapper, "quoteType") == "SWIFT"
1198
1297
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) if provider_boolean(wrapper, "gasless")
1199
1298
  transaction = required_node(wrapper, "transaction")
@@ -1203,7 +1302,12 @@ module ERPC
1203
1302
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless provider_integer(transaction, "chainId") == 1
1204
1303
  fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless numeric_zero?(object_value(transaction, "value"))
1205
1304
  data = provider_string(transaction, "data").downcase
1206
- fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless HEX_BYTES.match?(data) && data.length.even? && data.start_with?(ETHEREUM_FORWARDER_SELECTOR) && data.length >= 2 + 8 + 13 * 64
1305
+ selector = facts.forwarder_function_selector
1306
+ # EURC's swap-and-forward call has the provider's fixed 13-word
1307
+ # envelope. Direct USDC forwarding has nine static words plus the
1308
+ # dynamic bytes length word.
1309
+ minimum_words = facts.asset == "usdc" ? 10 : 13
1310
+ fail_bridge(BridgeErrorCode::PROVIDER_INVALID_RESPONSE) unless selector.is_a?(String) && HEX_BYTES.match?(data) && data.length.even? && data.start_with?(selector) && data.length >= 2 + 8 + minimum_words * 64
1207
1311
  {
1208
1312
  "kind" => "evm-unsigned-transaction", "chainId" => ETHEREUM_CHAIN_ID,
1209
1313
  "from" => swapper_address, "to" => ETHEREUM_FORWARDER, "data" => data, "value" => "0"
@@ -1343,7 +1447,7 @@ module ERPC
1343
1447
  swap_message = object_value(wrapper, "swapMessageV0Params")
1344
1448
  fail_bridge(BridgeErrorCode::BUILD_INVALID) unless swap_message == :__bridge_missing__ || swap_message.nil?
1345
1449
  transaction = begin
1346
- facts.source_chain_id == ETHEREUM_CHAIN_ID ? validate_evm_build_result(wrapper, swapper_address) : validate_solana_build_result(wrapper, swapper_address)
1450
+ facts.source_chain_id == ETHEREUM_CHAIN_ID ? validate_evm_build_result(wrapper, swapper_address, facts) : validate_solana_build_result(wrapper, swapper_address)
1347
1451
  rescue BridgeError => error
1348
1452
  ERPC.raise_safe_bridge_error(error) if error.code == BridgeErrorCode::BUILD_INVALID
1349
1453
  fail_bridge(BridgeErrorCode::BUILD_INVALID)
@@ -1353,7 +1457,7 @@ module ERPC
1353
1457
  "quote" => deep_dup(quote), "sourceChainId" => quote.fetch("sourceChainId"),
1354
1458
  "destinationChainId" => quote.fetch("destinationChainId"), "transaction" => transaction,
1355
1459
  "allowance" => facts.source_chain_id == ETHEREUM_CHAIN_ID ? {
1356
- "tokenDeploymentId" => ETHEREUM_EURC_DEPLOYMENT_ID, "tokenAddress" => ETHEREUM_EURC_ADDRESS,
1460
+ "tokenDeploymentId" => facts.source_token_deployment_id, "tokenAddress" => facts.source_token_address,
1357
1461
  "owner" => swapper_address, "spender" => ETHEREUM_FORWARDER, "requiredAmount" => quote.fetch("amountIn")
1358
1462
  } : nil,
1359
1463
  "validation" => {