From 422b5eae85515509d7aac2c6756f542de25f714b Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Tue, 26 May 2026 10:22:41 -0600 Subject: [PATCH 01/10] licensing: add root LICENSE (Apache 2.0) Adds the Apache 2.0 license text verbatim at the repository root. Source: https://www.apache.org/licenses/LICENSE-2.0.txt --- LICENSE | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From 9b26c6b9b0316ed053d2639c4dde5c7fcee66423 Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Tue, 26 May 2026 10:23:21 -0600 Subject: [PATCH 02/10] licensing: add NOTICE enumerating third-party licenses Lists direct Go dependencies from go.mod with their licenses and copyright attributions, per Apache 2.0 NOTICE file conventions. Transitive dependencies are tracked via go.mod and the upstream repositories of each dependency. --- NOTICE | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 NOTICE diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..fbb1bf2 --- /dev/null +++ b/NOTICE @@ -0,0 +1,117 @@ +Canton Middleware +Copyright 2026 ChainSafe Systems, Inc. + +This product includes software developed by ChainSafe Systems, Inc. +(https://chainsafe.io). + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +================================================================================ +Third-Party Software +================================================================================ + +This product bundles or depends on third-party software with the following +licenses. The full text of each license is reproduced in the source +distribution of each dependency. + +-------------------------------------------------------------------------------- +go-ethereum (github.com/ethereum/go-ethereum) +Copyright The go-ethereum Authors. +License: LGPL-3.0-or-later (library code) and GPL-3.0-or-later (binaries), +with some packages dual-licensed under Apache-2.0 and BSD-3-Clause. +Only Apache-2.0 / BSD-3-Clause licensed packages are consumed by this project +as Go library imports. + +-------------------------------------------------------------------------------- +go-chi/chi (github.com/go-chi/chi) +Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), +Google Inc. +License: MIT + +-------------------------------------------------------------------------------- +uptrace/bun (github.com/uptrace/bun) +Copyright (c) 2021 Uptrace.dev +License: BSD-2-Clause + +-------------------------------------------------------------------------------- +shopspring/decimal (github.com/shopspring/decimal) +Copyright (c) 2015 Spring, Inc. +License: MIT + +-------------------------------------------------------------------------------- +go.uber.org/zap +Copyright (c) 2016-2017 Uber Technologies, Inc. +License: MIT + +-------------------------------------------------------------------------------- +golang-jwt/jwt (github.com/golang-jwt/jwt) +Copyright (c) 2012 Dave Grijalva, 2021 golang-jwt maintainers. +License: MIT + +-------------------------------------------------------------------------------- +go-playground/validator (github.com/go-playground/validator) +Copyright (c) 2015 Dean Karn. +License: MIT + +-------------------------------------------------------------------------------- +google/uuid (github.com/google/uuid) +Copyright (c) 2009,2014 Google Inc. +License: BSD-3-Clause + +-------------------------------------------------------------------------------- +lib/pq (github.com/lib/pq) +Copyright (c) 2011-2013, 'pq' Contributors. +License: MIT + +-------------------------------------------------------------------------------- +prometheus/client_golang (github.com/prometheus/client_golang) +Copyright Prometheus Authors. +License: Apache-2.0 + +-------------------------------------------------------------------------------- +stretchr/testify (github.com/stretchr/testify) +Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors. +License: MIT + +-------------------------------------------------------------------------------- +testcontainers-go (github.com/testcontainers/testcontainers-go) +Copyright (c) 2017-present Atomic Jar Inc. +License: MIT + +-------------------------------------------------------------------------------- +creasty/defaults (github.com/creasty/defaults) +Copyright (c) 2017 Yuki Iwanaga. +License: MIT + +-------------------------------------------------------------------------------- +golang.org/x/* (crypto, sync, net, sys, text, exp) +Copyright (c) 2009 The Go Authors. +License: BSD-3-Clause + +-------------------------------------------------------------------------------- +google.golang.org/grpc, google.golang.org/protobuf, +google.golang.org/genproto +Copyright Google Inc. +License: Apache-2.0 / BSD-3-Clause + +-------------------------------------------------------------------------------- +gopkg.in/yaml.v3 +Copyright (c) 2006-2010 Kirill Simonov, +Copyright (c) 2006-2011 Kirill Simonov, +Copyright (c) 2011-2019 Canonical Ltd. +License: Apache-2.0 / MIT + +-------------------------------------------------------------------------------- + +For the complete list of transitive dependencies and their licenses, see +go.mod and the upstream repositories of each dependency. From 08b2c88fa6ae650b255138f82f03b8b60ad72809 Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Wed, 27 May 2026 04:49:57 -0600 Subject: [PATCH 03/10] licensing: add SPDX headers to Go source Adds `// SPDX-License-Identifier: Apache-2.0` to all first-party Go source files in `cmd/` and `pkg/`. Generated code (protobuf in `pkg/cantonsdk/lapi/v2/`, abigen bindings in `pkg/ethereum/contracts/`, mockery-generated mocks, anything carrying a "Code generated" or "DO NOT EDIT" marker) is intentionally skipped. The helper script `scripts/dev/add-spdx-headers.sh` is idempotent: re-running it is a no-op against files that already carry an SPDX header, so future contributors can extend the sweep without manual fiddling. --- cmd/api-server/main.go | 2 + cmd/api-server/migrate/main.go | 2 + cmd/generate-test-vectors/main.go | 2 + cmd/indexer/main.go | 2 + cmd/indexer/migrate/main.go | 2 + cmd/relayer/main.go | 2 + cmd/relayer/migrate/main.go | 2 + pkg/app/api/server.go | 2 + pkg/app/app.go | 2 + pkg/app/errors/errors.go | 2 + pkg/app/http/config.go | 2 + pkg/app/http/handler.go | 2 + pkg/app/http/middleware.go | 2 + pkg/app/http/server.go | 2 + pkg/app/indexer/server.go | 2 + pkg/app/relayer/server.go | 2 + pkg/auth/canton.go | 2 + pkg/auth/context.go | 2 + pkg/auth/evm.go | 2 + pkg/auth/jwt.go | 2 + pkg/cantonsdk/bridge/client.go | 2 + pkg/cantonsdk/bridge/config.go | 2 + pkg/cantonsdk/bridge/decode.go | 2 + pkg/cantonsdk/bridge/encode.go | 2 + pkg/cantonsdk/bridge/options.go | 2 + pkg/cantonsdk/bridge/types.go | 2 + pkg/cantonsdk/client/client.go | 2 + pkg/cantonsdk/client/config.go | 2 + pkg/cantonsdk/client/options.go | 2 + pkg/cantonsdk/identity/client.go | 2 + pkg/cantonsdk/identity/config.go | 2 + pkg/cantonsdk/identity/encode.go | 2 + pkg/cantonsdk/identity/options.go | 2 + pkg/cantonsdk/identity/types.go | 2 + pkg/cantonsdk/ledger/auth.go | 2 + pkg/cantonsdk/ledger/client.go | 2 + pkg/cantonsdk/ledger/config.go | 2 + pkg/cantonsdk/ledger/dial.go | 2 + pkg/cantonsdk/ledger/options.go | 2 + pkg/cantonsdk/streaming/builder.go | 2 + pkg/cantonsdk/streaming/client.go | 2 + pkg/cantonsdk/streaming/options.go | 2 + pkg/cantonsdk/streaming/stream.go | 2 + pkg/cantonsdk/streaming/types.go | 2 + pkg/cantonsdk/token/client.go | 2 + pkg/cantonsdk/token/config.go | 2 + pkg/cantonsdk/token/decimal.go | 2 + pkg/cantonsdk/token/decode.go | 2 + pkg/cantonsdk/token/encode.go | 2 + pkg/cantonsdk/token/encode_test.go | 2 + pkg/cantonsdk/token/options.go | 2 + pkg/cantonsdk/token/registry_client.go | 2 + pkg/cantonsdk/token/types.go | 2 + pkg/cantonsdk/values/decode.go | 2 + pkg/cantonsdk/values/encode.go | 2 + pkg/cantonsdk/values/meta.go | 2 + pkg/cantonsdk/values/values.go | 2 + pkg/config/config.go | 2 + pkg/config/config_test.go | 2 + pkg/custodial/accept_worker.go | 2 + pkg/custodial/accept_worker_test.go | 2 + pkg/custodial/config.go | 2 + pkg/ethereum/abi.go | 2 + pkg/ethereum/client.go | 2 + pkg/ethereum/config.go | 2 + pkg/ethereum/types.go | 2 + pkg/ethereum/util.go | 2 + pkg/ethrpc/config.go | 2 + pkg/ethrpc/miner/miner.go | 2 + pkg/ethrpc/miner/miner_test.go | 2 + pkg/ethrpc/service/eth_api.go | 2 + pkg/ethrpc/service/eth_api_test.go | 2 + pkg/ethrpc/service/http.go | 2 + pkg/ethrpc/service/log.go | 2 + pkg/ethrpc/service/net_api.go | 2 + pkg/ethrpc/service/net_api_test.go | 2 + pkg/ethrpc/service/service.go | 2 + pkg/ethrpc/service/service_test.go | 2 + pkg/ethrpc/service/web3_api.go | 2 + pkg/ethrpc/service/web3_api_test.go | 2 + pkg/ethrpc/store/model.go | 2 + pkg/ethrpc/store/pg.go | 2 + pkg/ethrpc/store/pg_test.go | 2 + pkg/ethrpc/submitter/submitter.go | 2 + pkg/ethrpc/submitter/submitter_test.go | 2 + pkg/ethrpc/types.go | 2 + pkg/indexer/client/http.go | 2 + pkg/indexer/client/http_test.go | 2 + pkg/indexer/config.go | 2 + pkg/indexer/engine/decoder.go | 2 + pkg/indexer/engine/decoder_test.go | 2 + pkg/indexer/engine/export_test.go | 2 + pkg/indexer/engine/fetcher.go | 2 + pkg/indexer/engine/processor.go | 2 + pkg/indexer/engine/processor_test.go | 2 + pkg/indexer/service/http.go | 2 + pkg/indexer/service/http_test.go | 2 + pkg/indexer/service/log.go | 2 + pkg/indexer/service/service.go | 2 + pkg/indexer/service/service_test.go | 2 + pkg/indexer/store/model.go | 2 + pkg/indexer/store/pg.go | 2 + pkg/indexer/store/pg_test.go | 2 + pkg/indexer/types.go | 2 + pkg/keys/canton_keys.go | 2 + pkg/keys/canton_keys_test.go | 2 + pkg/log/config.go | 2 + pkg/log/logger.go | 2 + pkg/migrations/apidb/01_create_users.go | 2 + pkg/migrations/apidb/02_create_whitelist.go | 2 + .../apidb/03_create_token_metrics.go | 2 + pkg/migrations/apidb/04_seed_token_metrics.go | 2 + .../apidb/05_create_bridge_events.go | 2 + .../apidb/06_create_reconciliation_state.go | 2 + .../apidb/07_create_evm_transactions.go | 2 + pkg/migrations/apidb/08_create_evm_state.go | 2 + pkg/migrations/apidb/09_create_evm_logs.go | 2 + .../apidb/10_create_user_token_balances.go | 2 + .../11_add_user_token_balances_indexes.go | 2 + pkg/migrations/apidb/12_create_mempool.go | 2 + .../apidb/13_add_evm_logs_block_timestamp.go | 2 + pkg/migrations/apidb/service.go | 2 + pkg/migrations/indexerdb/1_create_events.go | 2 + pkg/migrations/indexerdb/2_create_tokens.go | 2 + pkg/migrations/indexerdb/3_create_balances.go | 2 + pkg/migrations/indexerdb/4_create_offsets.go | 2 + .../indexerdb/5_create_pending_offers.go | 2 + pkg/migrations/indexerdb/6_create_holdings.go | 2 + pkg/migrations/indexerdb/service.go | 2 + pkg/migrations/migrations_test.go | 2 + .../relayerdb/1_create_transfers.go | 2 + .../relayerdb/2_create_chain_state.go | 2 + pkg/migrations/relayerdb/service.go | 2 + pkg/pgutil/config.go | 2 + pkg/pgutil/config_test.go | 2 + pkg/pgutil/connect.go | 2 + pkg/pgutil/migrations/migration.go | 2 + pkg/pgutil/migrations/migration_test.go | 2 + pkg/pgutil/testutil.go | 2 + pkg/reconciler/config.go | 2 + pkg/reconciler/reconciler.go | 2 + pkg/reconciler/store/model.go | 2 + pkg/reconciler/store/pg.go | 2 + pkg/reconciler/store/pg_test.go | 2 + pkg/registry/handler.go | 2 + pkg/relayer/config.go | 2 + pkg/relayer/engine/destination.go | 2 + pkg/relayer/engine/destination_test.go | 2 + pkg/relayer/engine/engine.go | 2 + pkg/relayer/engine/engine_test.go | 2 + pkg/relayer/engine/metrics.go | 2 + pkg/relayer/engine/processor.go | 2 + pkg/relayer/engine/processor_test.go | 2 + pkg/relayer/engine/source.go | 2 + pkg/relayer/engine/source_test.go | 2 + pkg/relayer/service/http.go | 2 + pkg/relayer/service/log.go | 2 + pkg/relayer/service/service.go | 2 + pkg/relayer/store/instrumented.go | 2 + pkg/relayer/store/metrics.go | 2 + pkg/relayer/store/model.go | 2 + pkg/relayer/store/pg.go | 2 + pkg/relayer/store/pg_test.go | 2 + pkg/relayer/types.go | 2 + pkg/token/config.go | 2 + pkg/token/erc20.go | 2 + pkg/token/erc20_test.go | 2 + pkg/token/http.go | 2 + pkg/token/http_test.go | 2 + pkg/token/native.go | 2 + pkg/token/native_test.go | 2 + pkg/token/provider/canton.go | 2 + pkg/token/provider/indexer.go | 2 + pkg/token/service.go | 2 + pkg/token/types.go | 2 + pkg/transfer/cache.go | 2 + pkg/transfer/cache_test.go | 2 + pkg/transfer/http.go | 2 + pkg/transfer/log.go | 2 + pkg/transfer/service.go | 2 + pkg/transfer/service_test.go | 2 + pkg/transfer/types.go | 2 + pkg/user/service/http.go | 2 + pkg/user/service/http_test.go | 2 + pkg/user/service/log.go | 2 + pkg/user/service/service.go | 2 + pkg/user/service/service_test.go | 2 + pkg/user/service/topology_cache.go | 2 + pkg/user/user.go | 2 + pkg/userstore/model.go | 2 + pkg/userstore/pg.go | 2 + pkg/userstore/pg_test.go | 2 + scripts/dev/add-spdx-headers.sh | 132 ++++++++++++++++++ 193 files changed, 516 insertions(+) create mode 100755 scripts/dev/add-spdx-headers.sh diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index 89cdef1..e72f665 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package main import ( diff --git a/cmd/api-server/migrate/main.go b/cmd/api-server/migrate/main.go index 6e2bdc9..6d6cb07 100644 --- a/cmd/api-server/migrate/main.go +++ b/cmd/api-server/migrate/main.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package main import ( diff --git a/cmd/generate-test-vectors/main.go b/cmd/generate-test-vectors/main.go index 7f96238..18c493b 100644 --- a/cmd/generate-test-vectors/main.go +++ b/cmd/generate-test-vectors/main.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Command generate-test-vectors produces deterministic crypto test vectors // for cross-validation with the canton-snap TypeScript implementation. // diff --git a/cmd/indexer/main.go b/cmd/indexer/main.go index 5548fc8..6bf2a89 100644 --- a/cmd/indexer/main.go +++ b/cmd/indexer/main.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package main import ( diff --git a/cmd/indexer/migrate/main.go b/cmd/indexer/migrate/main.go index 13427b7..b30e1b3 100644 --- a/cmd/indexer/migrate/main.go +++ b/cmd/indexer/migrate/main.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package main import ( diff --git a/cmd/relayer/main.go b/cmd/relayer/main.go index 4153fd3..72312e7 100644 --- a/cmd/relayer/main.go +++ b/cmd/relayer/main.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package main import ( diff --git a/cmd/relayer/migrate/main.go b/cmd/relayer/migrate/main.go index 9fae1d0..66abd06 100644 --- a/cmd/relayer/migrate/main.go +++ b/cmd/relayer/migrate/main.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package main import ( diff --git a/pkg/app/api/server.go b/pkg/app/api/server.go index 55ad8f8..c3ef36f 100644 --- a/pkg/app/api/server.go +++ b/pkg/app/api/server.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package api implements app.Runner for the API server process. package api diff --git a/pkg/app/app.go b/pkg/app/app.go index 9440b7b..bff55cc 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package app defines common runtime contracts shared by different // executable entrypoints (e.g., API server, relayer, migration runner). // diff --git a/pkg/app/errors/errors.go b/pkg/app/errors/errors.go index 6e77cd2..6ca4371 100644 --- a/pkg/app/errors/errors.go +++ b/pkg/app/errors/errors.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package errors contains helper functions and types to work with errors package errors diff --git a/pkg/app/http/config.go b/pkg/app/http/config.go index 882eca7..4d369cf 100644 --- a/pkg/app/http/config.go +++ b/pkg/app/http/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package http import "time" diff --git a/pkg/app/http/handler.go b/pkg/app/http/handler.go index 5553e67..0c66ab2 100644 --- a/pkg/app/http/handler.go +++ b/pkg/app/http/handler.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package http provides HTTP utilities including chi-compatible error handling package http diff --git a/pkg/app/http/middleware.go b/pkg/app/http/middleware.go index 947545a..6a1a1b7 100644 --- a/pkg/app/http/middleware.go +++ b/pkg/app/http/middleware.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package http import "net/http" diff --git a/pkg/app/http/server.go b/pkg/app/http/server.go index f120aa1..1512b44 100644 --- a/pkg/app/http/server.go +++ b/pkg/app/http/server.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package http import ( diff --git a/pkg/app/indexer/server.go b/pkg/app/indexer/server.go index 99c3f26..8d52c5c 100644 --- a/pkg/app/indexer/server.go +++ b/pkg/app/indexer/server.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package indexer implements the runner for the indexer process. // // The indexer has two concurrent responsibilities: diff --git a/pkg/app/relayer/server.go b/pkg/app/relayer/server.go index 19fc8c5..0b75845 100644 --- a/pkg/app/relayer/server.go +++ b/pkg/app/relayer/server.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package relayer implements app.Runner for the relayer process. package relayer diff --git a/pkg/auth/canton.go b/pkg/auth/canton.go index 14437a0..f05c62d 100644 --- a/pkg/auth/canton.go +++ b/pkg/auth/canton.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package auth import ( diff --git a/pkg/auth/context.go b/pkg/auth/context.go index a79fc3e..ed838c4 100644 --- a/pkg/auth/context.go +++ b/pkg/auth/context.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package auth import ( diff --git a/pkg/auth/evm.go b/pkg/auth/evm.go index be0e14a..ec779f8 100644 --- a/pkg/auth/evm.go +++ b/pkg/auth/evm.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package auth import ( diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go index e2f04e8..7fea447 100644 --- a/pkg/auth/jwt.go +++ b/pkg/auth/jwt.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package auth import ( diff --git a/pkg/cantonsdk/bridge/client.go b/pkg/cantonsdk/bridge/client.go index 1501607..f0d1a9d 100644 --- a/pkg/cantonsdk/bridge/client.go +++ b/pkg/cantonsdk/bridge/client.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package bridge implements optional Wayfinder bridge operations for Canton. // // It provides deposit/withdrawal flows and bridge-related event queries. diff --git a/pkg/cantonsdk/bridge/config.go b/pkg/cantonsdk/bridge/config.go index abf6597..e2bc4e9 100644 --- a/pkg/cantonsdk/bridge/config.go +++ b/pkg/cantonsdk/bridge/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package bridge import "fmt" diff --git a/pkg/cantonsdk/bridge/decode.go b/pkg/cantonsdk/bridge/decode.go index 549f5d6..0767d16 100644 --- a/pkg/cantonsdk/bridge/decode.go +++ b/pkg/cantonsdk/bridge/decode.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package bridge import ( diff --git a/pkg/cantonsdk/bridge/encode.go b/pkg/cantonsdk/bridge/encode.go index 7981006..fbfa05a 100644 --- a/pkg/cantonsdk/bridge/encode.go +++ b/pkg/cantonsdk/bridge/encode.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package bridge import ( diff --git a/pkg/cantonsdk/bridge/options.go b/pkg/cantonsdk/bridge/options.go index ce9422b..1eb88ef 100644 --- a/pkg/cantonsdk/bridge/options.go +++ b/pkg/cantonsdk/bridge/options.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package bridge import "go.uber.org/zap" diff --git a/pkg/cantonsdk/bridge/types.go b/pkg/cantonsdk/bridge/types.go index 693b7ef..40900ef 100644 --- a/pkg/cantonsdk/bridge/types.go +++ b/pkg/cantonsdk/bridge/types.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package bridge import ( diff --git a/pkg/cantonsdk/client/client.go b/pkg/cantonsdk/client/client.go index 06448fa..0cec315 100644 --- a/pkg/cantonsdk/client/client.go +++ b/pkg/cantonsdk/client/client.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package client provides the high-level Canton SDK client. // // It exposes a unified interface for interacting with a Canton ledger, diff --git a/pkg/cantonsdk/client/config.go b/pkg/cantonsdk/client/config.go index e0d32c3..42794ce 100644 --- a/pkg/cantonsdk/client/config.go +++ b/pkg/cantonsdk/client/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package client import ( diff --git a/pkg/cantonsdk/client/options.go b/pkg/cantonsdk/client/options.go index c845db0..e46d2d3 100644 --- a/pkg/cantonsdk/client/options.go +++ b/pkg/cantonsdk/client/options.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package client import ( diff --git a/pkg/cantonsdk/identity/client.go b/pkg/cantonsdk/identity/client.go index 0a5f6f7..8ee6b22 100644 --- a/pkg/cantonsdk/identity/client.go +++ b/pkg/cantonsdk/identity/client.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package identity implements Canton identity operations such as party management // and fingerprint-to-party mapping. package identity diff --git a/pkg/cantonsdk/identity/config.go b/pkg/cantonsdk/identity/config.go index 3d7fdd5..306a58f 100644 --- a/pkg/cantonsdk/identity/config.go +++ b/pkg/cantonsdk/identity/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package identity import "errors" diff --git a/pkg/cantonsdk/identity/encode.go b/pkg/cantonsdk/identity/encode.go index e3b9547..311df4b 100644 --- a/pkg/cantonsdk/identity/encode.go +++ b/pkg/cantonsdk/identity/encode.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package identity import ( diff --git a/pkg/cantonsdk/identity/options.go b/pkg/cantonsdk/identity/options.go index 5fbc9dc..1edf922 100644 --- a/pkg/cantonsdk/identity/options.go +++ b/pkg/cantonsdk/identity/options.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package identity import "go.uber.org/zap" diff --git a/pkg/cantonsdk/identity/types.go b/pkg/cantonsdk/identity/types.go index 8731789..1ff2fca 100644 --- a/pkg/cantonsdk/identity/types.go +++ b/pkg/cantonsdk/identity/types.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package identity import ( diff --git a/pkg/cantonsdk/ledger/auth.go b/pkg/cantonsdk/ledger/auth.go index 1b26cbd..3386f6d 100644 --- a/pkg/cantonsdk/ledger/auth.go +++ b/pkg/cantonsdk/ledger/auth.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package ledger import ( diff --git a/pkg/cantonsdk/ledger/client.go b/pkg/cantonsdk/ledger/client.go index 7df8350..5ab006e 100644 --- a/pkg/cantonsdk/ledger/client.go +++ b/pkg/cantonsdk/ledger/client.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package ledger implements the low-level Canton Ledger API client. // // It manages gRPC connectivity, TLS configuration, OAuth2 authentication, diff --git a/pkg/cantonsdk/ledger/config.go b/pkg/cantonsdk/ledger/config.go index 0150685..af3f892 100644 --- a/pkg/cantonsdk/ledger/config.go +++ b/pkg/cantonsdk/ledger/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package ledger import ( diff --git a/pkg/cantonsdk/ledger/dial.go b/pkg/cantonsdk/ledger/dial.go index b82954b..4ba447b 100644 --- a/pkg/cantonsdk/ledger/dial.go +++ b/pkg/cantonsdk/ledger/dial.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package ledger import ( diff --git a/pkg/cantonsdk/ledger/options.go b/pkg/cantonsdk/ledger/options.go index 32380bd..8404d87 100644 --- a/pkg/cantonsdk/ledger/options.go +++ b/pkg/cantonsdk/ledger/options.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package ledger import ( diff --git a/pkg/cantonsdk/streaming/builder.go b/pkg/cantonsdk/streaming/builder.go index d142137..2bf52f0 100644 --- a/pkg/cantonsdk/streaming/builder.go +++ b/pkg/cantonsdk/streaming/builder.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package streaming import ( diff --git a/pkg/cantonsdk/streaming/client.go b/pkg/cantonsdk/streaming/client.go index a6db5a3..72e8936 100644 --- a/pkg/cantonsdk/streaming/client.go +++ b/pkg/cantonsdk/streaming/client.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package streaming import ( diff --git a/pkg/cantonsdk/streaming/options.go b/pkg/cantonsdk/streaming/options.go index b3de7f4..a17591c 100644 --- a/pkg/cantonsdk/streaming/options.go +++ b/pkg/cantonsdk/streaming/options.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package streaming import "go.uber.org/zap" diff --git a/pkg/cantonsdk/streaming/stream.go b/pkg/cantonsdk/streaming/stream.go index dd150b8..9e461b5 100644 --- a/pkg/cantonsdk/streaming/stream.go +++ b/pkg/cantonsdk/streaming/stream.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package streaming import "context" diff --git a/pkg/cantonsdk/streaming/types.go b/pkg/cantonsdk/streaming/types.go index 9869b1f..4d286a9 100644 --- a/pkg/cantonsdk/streaming/types.go +++ b/pkg/cantonsdk/streaming/types.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package streaming provides a reusable, generic Canton ledger streaming client. // // It wraps UpdateService.GetUpdates with automatic reconnection, exponential backoff, diff --git a/pkg/cantonsdk/token/client.go b/pkg/cantonsdk/token/client.go index 3408248..974f46c 100644 --- a/pkg/cantonsdk/token/client.go +++ b/pkg/cantonsdk/token/client.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package token implements CIP-56 token operations such as mint, burn, transfer, // and balance queries. package token diff --git a/pkg/cantonsdk/token/config.go b/pkg/cantonsdk/token/config.go index 43592aa..705b077 100644 --- a/pkg/cantonsdk/token/config.go +++ b/pkg/cantonsdk/token/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token import "errors" diff --git a/pkg/cantonsdk/token/decimal.go b/pkg/cantonsdk/token/decimal.go index d9b8774..087f11a 100644 --- a/pkg/cantonsdk/token/decimal.go +++ b/pkg/cantonsdk/token/decimal.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token import ( diff --git a/pkg/cantonsdk/token/decode.go b/pkg/cantonsdk/token/decode.go index 30b06b6..caf51f3 100644 --- a/pkg/cantonsdk/token/decode.go +++ b/pkg/cantonsdk/token/decode.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token import ( diff --git a/pkg/cantonsdk/token/encode.go b/pkg/cantonsdk/token/encode.go index 789eac4..bcd5775 100644 --- a/pkg/cantonsdk/token/encode.go +++ b/pkg/cantonsdk/token/encode.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token import ( diff --git a/pkg/cantonsdk/token/encode_test.go b/pkg/cantonsdk/token/encode_test.go index 24d7d4c..c5d1867 100644 --- a/pkg/cantonsdk/token/encode_test.go +++ b/pkg/cantonsdk/token/encode_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token import ( diff --git a/pkg/cantonsdk/token/options.go b/pkg/cantonsdk/token/options.go index 624a603..edff330 100644 --- a/pkg/cantonsdk/token/options.go +++ b/pkg/cantonsdk/token/options.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token import "go.uber.org/zap" diff --git a/pkg/cantonsdk/token/registry_client.go b/pkg/cantonsdk/token/registry_client.go index 9ac1050..48fd97e 100644 --- a/pkg/cantonsdk/token/registry_client.go +++ b/pkg/cantonsdk/token/registry_client.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token import ( diff --git a/pkg/cantonsdk/token/types.go b/pkg/cantonsdk/token/types.go index 03cf7ed..1082dfc 100644 --- a/pkg/cantonsdk/token/types.go +++ b/pkg/cantonsdk/token/types.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token import ( diff --git a/pkg/cantonsdk/values/decode.go b/pkg/cantonsdk/values/decode.go index 1f590e7..03c0666 100644 --- a/pkg/cantonsdk/values/decode.go +++ b/pkg/cantonsdk/values/decode.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package values import ( diff --git a/pkg/cantonsdk/values/encode.go b/pkg/cantonsdk/values/encode.go index 2a4f929..0faf405 100644 --- a/pkg/cantonsdk/values/encode.go +++ b/pkg/cantonsdk/values/encode.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package values import ( diff --git a/pkg/cantonsdk/values/meta.go b/pkg/cantonsdk/values/meta.go index 3a4fcc0..355b763 100644 --- a/pkg/cantonsdk/values/meta.go +++ b/pkg/cantonsdk/values/meta.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package values import ( diff --git a/pkg/cantonsdk/values/values.go b/pkg/cantonsdk/values/values.go index 6802e1c..6764985 100644 --- a/pkg/cantonsdk/values/values.go +++ b/pkg/cantonsdk/values/values.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package values provides helper utilities for working with // Canton Ledger API value types. package values diff --git a/pkg/config/config.go b/pkg/config/config.go index be29803..615016f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package config import ( diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index f9992af..67a0c55 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package config import ( diff --git a/pkg/custodial/accept_worker.go b/pkg/custodial/accept_worker.go index 0b7b5b6..e59d84b 100644 --- a/pkg/custodial/accept_worker.go +++ b/pkg/custodial/accept_worker.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package custodial implements background automation for custodial Canton parties. package custodial diff --git a/pkg/custodial/accept_worker_test.go b/pkg/custodial/accept_worker_test.go index d4fc458..9ade69d 100644 --- a/pkg/custodial/accept_worker_test.go +++ b/pkg/custodial/accept_worker_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package custodial import ( diff --git a/pkg/custodial/config.go b/pkg/custodial/config.go index d024808..115eb81 100644 --- a/pkg/custodial/config.go +++ b/pkg/custodial/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package custodial import "time" diff --git a/pkg/ethereum/abi.go b/pkg/ethereum/abi.go index 45d58e5..029063c 100644 --- a/pkg/ethereum/abi.go +++ b/pkg/ethereum/abi.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package ethereum // ERC20ABI is the standard ERC20 token interface ABI. diff --git a/pkg/ethereum/client.go b/pkg/ethereum/client.go index 79af433..d9689be 100644 --- a/pkg/ethereum/client.go +++ b/pkg/ethereum/client.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package ethereum import ( diff --git a/pkg/ethereum/config.go b/pkg/ethereum/config.go index 561c147..8bc686c 100644 --- a/pkg/ethereum/config.go +++ b/pkg/ethereum/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package ethereum import "time" diff --git a/pkg/ethereum/types.go b/pkg/ethereum/types.go index 0ae3766..9c8fdfd 100644 --- a/pkg/ethereum/types.go +++ b/pkg/ethereum/types.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package ethereum import ( diff --git a/pkg/ethereum/util.go b/pkg/ethereum/util.go index a403b34..b3e0ea1 100644 --- a/pkg/ethereum/util.go +++ b/pkg/ethereum/util.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package ethereum import ( diff --git a/pkg/ethrpc/config.go b/pkg/ethrpc/config.go index 6bd6ce5..ec67d24 100644 --- a/pkg/ethrpc/config.go +++ b/pkg/ethrpc/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package ethrpc import ( diff --git a/pkg/ethrpc/miner/miner.go b/pkg/ethrpc/miner/miner.go index a0047a3..7fb31a6 100644 --- a/pkg/ethrpc/miner/miner.go +++ b/pkg/ethrpc/miner/miner.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package miner import ( diff --git a/pkg/ethrpc/miner/miner_test.go b/pkg/ethrpc/miner/miner_test.go index ebfaf20..f5197d0 100644 --- a/pkg/ethrpc/miner/miner_test.go +++ b/pkg/ethrpc/miner/miner_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package miner import ( diff --git a/pkg/ethrpc/service/eth_api.go b/pkg/ethrpc/service/eth_api.go index 8f5cf00..056d3cf 100644 --- a/pkg/ethrpc/service/eth_api.go +++ b/pkg/ethrpc/service/eth_api.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/ethrpc/service/eth_api_test.go b/pkg/ethrpc/service/eth_api_test.go index 172cdda..f4d41c4 100644 --- a/pkg/ethrpc/service/eth_api_test.go +++ b/pkg/ethrpc/service/eth_api_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service_test import ( diff --git a/pkg/ethrpc/service/http.go b/pkg/ethrpc/service/http.go index 05aa9a0..72b08b9 100644 --- a/pkg/ethrpc/service/http.go +++ b/pkg/ethrpc/service/http.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/ethrpc/service/log.go b/pkg/ethrpc/service/log.go index 4d37b7c..9ca1e50 100644 --- a/pkg/ethrpc/service/log.go +++ b/pkg/ethrpc/service/log.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/ethrpc/service/net_api.go b/pkg/ethrpc/service/net_api.go index 8f316e9..8d656e2 100644 --- a/pkg/ethrpc/service/net_api.go +++ b/pkg/ethrpc/service/net_api.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/ethrpc/service/net_api_test.go b/pkg/ethrpc/service/net_api_test.go index 9f0377e..3ea893c 100644 --- a/pkg/ethrpc/service/net_api_test.go +++ b/pkg/ethrpc/service/net_api_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service_test import ( diff --git a/pkg/ethrpc/service/service.go b/pkg/ethrpc/service/service.go index 3924f90..8c7eba8 100644 --- a/pkg/ethrpc/service/service.go +++ b/pkg/ethrpc/service/service.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/ethrpc/service/service_test.go b/pkg/ethrpc/service/service_test.go index 673095f..02b342f 100644 --- a/pkg/ethrpc/service/service_test.go +++ b/pkg/ethrpc/service/service_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service_test import ( diff --git a/pkg/ethrpc/service/web3_api.go b/pkg/ethrpc/service/web3_api.go index 9df703a..e78efb8 100644 --- a/pkg/ethrpc/service/web3_api.go +++ b/pkg/ethrpc/service/web3_api.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/ethrpc/service/web3_api_test.go b/pkg/ethrpc/service/web3_api_test.go index e5d8b82..a1f2c31 100644 --- a/pkg/ethrpc/service/web3_api_test.go +++ b/pkg/ethrpc/service/web3_api_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service_test import ( diff --git a/pkg/ethrpc/store/model.go b/pkg/ethrpc/store/model.go index 6944230..ef44bea 100644 --- a/pkg/ethrpc/store/model.go +++ b/pkg/ethrpc/store/model.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package store import ( diff --git a/pkg/ethrpc/store/pg.go b/pkg/ethrpc/store/pg.go index fbe2d5d..d345728 100644 --- a/pkg/ethrpc/store/pg.go +++ b/pkg/ethrpc/store/pg.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package store import ( diff --git a/pkg/ethrpc/store/pg_test.go b/pkg/ethrpc/store/pg_test.go index cbad1aa..abdba04 100644 --- a/pkg/ethrpc/store/pg_test.go +++ b/pkg/ethrpc/store/pg_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package store import ( diff --git a/pkg/ethrpc/submitter/submitter.go b/pkg/ethrpc/submitter/submitter.go index 3c723c1..98f8347 100644 --- a/pkg/ethrpc/submitter/submitter.go +++ b/pkg/ethrpc/submitter/submitter.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package submitter drains pending mempool entries by submitting the // corresponding ERC-20 transfer to Canton. It is the asynchronous counterpart // to ethrpc.service.SendRawTransaction: that handler records intent and diff --git a/pkg/ethrpc/submitter/submitter_test.go b/pkg/ethrpc/submitter/submitter_test.go index 0117a6b..a022fca 100644 --- a/pkg/ethrpc/submitter/submitter_test.go +++ b/pkg/ethrpc/submitter/submitter_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package submitter import ( diff --git a/pkg/ethrpc/types.go b/pkg/ethrpc/types.go index 1f9bc36..53ea6b4 100644 --- a/pkg/ethrpc/types.go +++ b/pkg/ethrpc/types.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package ethrpc import ( diff --git a/pkg/indexer/client/http.go b/pkg/indexer/client/http.go index 5cb160c..382d45b 100644 --- a/pkg/indexer/client/http.go +++ b/pkg/indexer/client/http.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package client provides an HTTP client for the indexer's admin API. package client diff --git a/pkg/indexer/client/http_test.go b/pkg/indexer/client/http_test.go index 66403e9..239fd31 100644 --- a/pkg/indexer/client/http_test.go +++ b/pkg/indexer/client/http_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package client_test import ( diff --git a/pkg/indexer/config.go b/pkg/indexer/config.go index 2823b7f..8c7fd63 100644 --- a/pkg/indexer/config.go +++ b/pkg/indexer/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package indexer // Config holds stream-specific settings for the indexer process. diff --git a/pkg/indexer/engine/decoder.go b/pkg/indexer/engine/decoder.go index 8aaad25..89be5b5 100644 --- a/pkg/indexer/engine/decoder.go +++ b/pkg/indexer/engine/decoder.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package engine import ( diff --git a/pkg/indexer/engine/decoder_test.go b/pkg/indexer/engine/decoder_test.go index 65e5cce..7aab419 100644 --- a/pkg/indexer/engine/decoder_test.go +++ b/pkg/indexer/engine/decoder_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package engine import ( diff --git a/pkg/indexer/engine/export_test.go b/pkg/indexer/engine/export_test.go index 9e8e6a7..242aaec 100644 --- a/pkg/indexer/engine/export_test.go +++ b/pkg/indexer/engine/export_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package engine import "time" diff --git a/pkg/indexer/engine/fetcher.go b/pkg/indexer/engine/fetcher.go index 81a3b53..a51f522 100644 --- a/pkg/indexer/engine/fetcher.go +++ b/pkg/indexer/engine/fetcher.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package engine import ( diff --git a/pkg/indexer/engine/processor.go b/pkg/indexer/engine/processor.go index 43f622e..f0ecae3 100644 --- a/pkg/indexer/engine/processor.go +++ b/pkg/indexer/engine/processor.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package engine import ( diff --git a/pkg/indexer/engine/processor_test.go b/pkg/indexer/engine/processor_test.go index 36f884c..fa0fd3a 100644 --- a/pkg/indexer/engine/processor_test.go +++ b/pkg/indexer/engine/processor_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package engine_test import ( diff --git a/pkg/indexer/service/http.go b/pkg/indexer/service/http.go index d9d60f9..866f6b5 100644 --- a/pkg/indexer/service/http.go +++ b/pkg/indexer/service/http.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/indexer/service/http_test.go b/pkg/indexer/service/http_test.go index 711a2ac..4b6a4b8 100644 --- a/pkg/indexer/service/http_test.go +++ b/pkg/indexer/service/http_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service_test import ( diff --git a/pkg/indexer/service/log.go b/pkg/indexer/service/log.go index af0c8a2..4b12c9b 100644 --- a/pkg/indexer/service/log.go +++ b/pkg/indexer/service/log.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/indexer/service/service.go b/pkg/indexer/service/service.go index bbe21b3..f3a581e 100644 --- a/pkg/indexer/service/service.go +++ b/pkg/indexer/service/service.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/indexer/service/service_test.go b/pkg/indexer/service/service_test.go index bb7ec64..6071cc5 100644 --- a/pkg/indexer/service/service_test.go +++ b/pkg/indexer/service/service_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service_test import ( diff --git a/pkg/indexer/store/model.go b/pkg/indexer/store/model.go index efa194c..1661d2f 100644 --- a/pkg/indexer/store/model.go +++ b/pkg/indexer/store/model.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package store import ( diff --git a/pkg/indexer/store/pg.go b/pkg/indexer/store/pg.go index c964cbd..b178610 100644 --- a/pkg/indexer/store/pg.go +++ b/pkg/indexer/store/pg.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package store import ( diff --git a/pkg/indexer/store/pg_test.go b/pkg/indexer/store/pg_test.go index fb35c32..d74babf 100644 --- a/pkg/indexer/store/pg_test.go +++ b/pkg/indexer/store/pg_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package store import ( diff --git a/pkg/indexer/types.go b/pkg/indexer/types.go index fad3b16..7545ebe 100644 --- a/pkg/indexer/types.go +++ b/pkg/indexer/types.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package indexer import "time" diff --git a/pkg/keys/canton_keys.go b/pkg/keys/canton_keys.go index 25e8ecb..768daab 100644 --- a/pkg/keys/canton_keys.go +++ b/pkg/keys/canton_keys.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package keys provides Canton key generation and encryption for custodial key management. // This package is used to generate Canton keypairs for users and encrypt them for secure storage. // Uses secp256k1 (same curve as Ethereum) for compatibility with user wallets. diff --git a/pkg/keys/canton_keys_test.go b/pkg/keys/canton_keys_test.go index 194385a..df1c25a 100644 --- a/pkg/keys/canton_keys_test.go +++ b/pkg/keys/canton_keys_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package keys import ( diff --git a/pkg/log/config.go b/pkg/log/config.go index 65b9c47..530afd1 100644 --- a/pkg/log/config.go +++ b/pkg/log/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package log // Config contains logging settings diff --git a/pkg/log/logger.go b/pkg/log/logger.go index 53a56ae..7380c55 100644 --- a/pkg/log/logger.go +++ b/pkg/log/logger.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package log import ( diff --git a/pkg/migrations/apidb/01_create_users.go b/pkg/migrations/apidb/01_create_users.go index 1ff81aa..77e1c63 100644 --- a/pkg/migrations/apidb/01_create_users.go +++ b/pkg/migrations/apidb/01_create_users.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package apidb import ( diff --git a/pkg/migrations/apidb/02_create_whitelist.go b/pkg/migrations/apidb/02_create_whitelist.go index 232b638..5ceb582 100644 --- a/pkg/migrations/apidb/02_create_whitelist.go +++ b/pkg/migrations/apidb/02_create_whitelist.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package apidb import ( diff --git a/pkg/migrations/apidb/03_create_token_metrics.go b/pkg/migrations/apidb/03_create_token_metrics.go index 8a55c44..5944635 100644 --- a/pkg/migrations/apidb/03_create_token_metrics.go +++ b/pkg/migrations/apidb/03_create_token_metrics.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package apidb import ( diff --git a/pkg/migrations/apidb/04_seed_token_metrics.go b/pkg/migrations/apidb/04_seed_token_metrics.go index d03c78b..8b3c025 100644 --- a/pkg/migrations/apidb/04_seed_token_metrics.go +++ b/pkg/migrations/apidb/04_seed_token_metrics.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package apidb import ( diff --git a/pkg/migrations/apidb/05_create_bridge_events.go b/pkg/migrations/apidb/05_create_bridge_events.go index db1ebf6..b883654 100644 --- a/pkg/migrations/apidb/05_create_bridge_events.go +++ b/pkg/migrations/apidb/05_create_bridge_events.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package apidb import ( diff --git a/pkg/migrations/apidb/06_create_reconciliation_state.go b/pkg/migrations/apidb/06_create_reconciliation_state.go index dd49dba..9ada0b3 100644 --- a/pkg/migrations/apidb/06_create_reconciliation_state.go +++ b/pkg/migrations/apidb/06_create_reconciliation_state.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package apidb import ( diff --git a/pkg/migrations/apidb/07_create_evm_transactions.go b/pkg/migrations/apidb/07_create_evm_transactions.go index 75420ba..7153e7c 100644 --- a/pkg/migrations/apidb/07_create_evm_transactions.go +++ b/pkg/migrations/apidb/07_create_evm_transactions.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package apidb import ( diff --git a/pkg/migrations/apidb/08_create_evm_state.go b/pkg/migrations/apidb/08_create_evm_state.go index 0bd991a..e707a78 100644 --- a/pkg/migrations/apidb/08_create_evm_state.go +++ b/pkg/migrations/apidb/08_create_evm_state.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package apidb import ( diff --git a/pkg/migrations/apidb/09_create_evm_logs.go b/pkg/migrations/apidb/09_create_evm_logs.go index f3c60a0..e4199f2 100644 --- a/pkg/migrations/apidb/09_create_evm_logs.go +++ b/pkg/migrations/apidb/09_create_evm_logs.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package apidb import ( diff --git a/pkg/migrations/apidb/10_create_user_token_balances.go b/pkg/migrations/apidb/10_create_user_token_balances.go index 606e31c..d3acf54 100644 --- a/pkg/migrations/apidb/10_create_user_token_balances.go +++ b/pkg/migrations/apidb/10_create_user_token_balances.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package apidb import ( diff --git a/pkg/migrations/apidb/11_add_user_token_balances_indexes.go b/pkg/migrations/apidb/11_add_user_token_balances_indexes.go index d6ba21b..405e160 100644 --- a/pkg/migrations/apidb/11_add_user_token_balances_indexes.go +++ b/pkg/migrations/apidb/11_add_user_token_balances_indexes.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package apidb import ( diff --git a/pkg/migrations/apidb/12_create_mempool.go b/pkg/migrations/apidb/12_create_mempool.go index 4fec3d3..69b94bf 100644 --- a/pkg/migrations/apidb/12_create_mempool.go +++ b/pkg/migrations/apidb/12_create_mempool.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package apidb import ( diff --git a/pkg/migrations/apidb/13_add_evm_logs_block_timestamp.go b/pkg/migrations/apidb/13_add_evm_logs_block_timestamp.go index 3f3bc07..a9995bb 100644 --- a/pkg/migrations/apidb/13_add_evm_logs_block_timestamp.go +++ b/pkg/migrations/apidb/13_add_evm_logs_block_timestamp.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package apidb import ( diff --git a/pkg/migrations/apidb/service.go b/pkg/migrations/apidb/service.go index 17cdcd6..8b33e2b 100644 --- a/pkg/migrations/apidb/service.go +++ b/pkg/migrations/apidb/service.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package apidb holds all the migrations for the API database package apidb diff --git a/pkg/migrations/indexerdb/1_create_events.go b/pkg/migrations/indexerdb/1_create_events.go index 4eb30cd..0a1bb5e 100644 --- a/pkg/migrations/indexerdb/1_create_events.go +++ b/pkg/migrations/indexerdb/1_create_events.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package indexerdb import ( diff --git a/pkg/migrations/indexerdb/2_create_tokens.go b/pkg/migrations/indexerdb/2_create_tokens.go index bc79561..c31fa69 100644 --- a/pkg/migrations/indexerdb/2_create_tokens.go +++ b/pkg/migrations/indexerdb/2_create_tokens.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package indexerdb import ( diff --git a/pkg/migrations/indexerdb/3_create_balances.go b/pkg/migrations/indexerdb/3_create_balances.go index abb7ee1..fdf8542 100644 --- a/pkg/migrations/indexerdb/3_create_balances.go +++ b/pkg/migrations/indexerdb/3_create_balances.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package indexerdb import ( diff --git a/pkg/migrations/indexerdb/4_create_offsets.go b/pkg/migrations/indexerdb/4_create_offsets.go index 8846d96..82b5fa3 100644 --- a/pkg/migrations/indexerdb/4_create_offsets.go +++ b/pkg/migrations/indexerdb/4_create_offsets.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package indexerdb import ( diff --git a/pkg/migrations/indexerdb/5_create_pending_offers.go b/pkg/migrations/indexerdb/5_create_pending_offers.go index ca5b868..8ce9007 100644 --- a/pkg/migrations/indexerdb/5_create_pending_offers.go +++ b/pkg/migrations/indexerdb/5_create_pending_offers.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package indexerdb import ( diff --git a/pkg/migrations/indexerdb/6_create_holdings.go b/pkg/migrations/indexerdb/6_create_holdings.go index c9d5257..479d819 100644 --- a/pkg/migrations/indexerdb/6_create_holdings.go +++ b/pkg/migrations/indexerdb/6_create_holdings.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package indexerdb import ( diff --git a/pkg/migrations/indexerdb/service.go b/pkg/migrations/indexerdb/service.go index 825944e..02c9819 100644 --- a/pkg/migrations/indexerdb/service.go +++ b/pkg/migrations/indexerdb/service.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package indexerdb holds all migrations for the indexer database. package indexerdb diff --git a/pkg/migrations/migrations_test.go b/pkg/migrations/migrations_test.go index d89b725..2fcc872 100644 --- a/pkg/migrations/migrations_test.go +++ b/pkg/migrations/migrations_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package migrations import ( diff --git a/pkg/migrations/relayerdb/1_create_transfers.go b/pkg/migrations/relayerdb/1_create_transfers.go index 8adf1db..5b50991 100644 --- a/pkg/migrations/relayerdb/1_create_transfers.go +++ b/pkg/migrations/relayerdb/1_create_transfers.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package relayerdb import ( diff --git a/pkg/migrations/relayerdb/2_create_chain_state.go b/pkg/migrations/relayerdb/2_create_chain_state.go index 46a3888..e2a7255 100644 --- a/pkg/migrations/relayerdb/2_create_chain_state.go +++ b/pkg/migrations/relayerdb/2_create_chain_state.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package relayerdb import ( diff --git a/pkg/migrations/relayerdb/service.go b/pkg/migrations/relayerdb/service.go index a8cf7c7..bee5c4b 100644 --- a/pkg/migrations/relayerdb/service.go +++ b/pkg/migrations/relayerdb/service.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package relayerdb holds all the migrations for the relayer database package relayerdb diff --git a/pkg/pgutil/config.go b/pkg/pgutil/config.go index 0e45775..7751202 100644 --- a/pkg/pgutil/config.go +++ b/pkg/pgutil/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package pgutil import "net/url" diff --git a/pkg/pgutil/config_test.go b/pkg/pgutil/config_test.go index e6b506c..39e118e 100644 --- a/pkg/pgutil/config_test.go +++ b/pkg/pgutil/config_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package pgutil import "testing" diff --git a/pkg/pgutil/connect.go b/pkg/pgutil/connect.go index 0f6e3bc..cd61cf0 100644 --- a/pkg/pgutil/connect.go +++ b/pkg/pgutil/connect.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package pgutil import ( diff --git a/pkg/pgutil/migrations/migration.go b/pkg/pgutil/migrations/migration.go index eb7614d..084c528 100644 --- a/pkg/pgutil/migrations/migration.go +++ b/pkg/pgutil/migrations/migration.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package migrations holds migrations related helpers package migrations diff --git a/pkg/pgutil/migrations/migration_test.go b/pkg/pgutil/migrations/migration_test.go index d20eb1e..0c167f7 100644 --- a/pkg/pgutil/migrations/migration_test.go +++ b/pkg/pgutil/migrations/migration_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package migrations import ( diff --git a/pkg/pgutil/testutil.go b/pkg/pgutil/testutil.go index 3691c31..4390459 100644 --- a/pkg/pgutil/testutil.go +++ b/pkg/pgutil/testutil.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package pgutil import ( diff --git a/pkg/reconciler/config.go b/pkg/reconciler/config.go index ac4c272..27f24e5 100644 --- a/pkg/reconciler/config.go +++ b/pkg/reconciler/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package reconciler import "time" diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index 2363aeb..4678a2a 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package reconciler import ( diff --git a/pkg/reconciler/store/model.go b/pkg/reconciler/store/model.go index 99c71b0..40c2962 100644 --- a/pkg/reconciler/store/model.go +++ b/pkg/reconciler/store/model.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package store import ( diff --git a/pkg/reconciler/store/pg.go b/pkg/reconciler/store/pg.go index b4d1a31..e448099 100644 --- a/pkg/reconciler/store/pg.go +++ b/pkg/reconciler/store/pg.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package store import ( diff --git a/pkg/reconciler/store/pg_test.go b/pkg/reconciler/store/pg_test.go index e3811e7..47e9e87 100644 --- a/pkg/reconciler/store/pg_test.go +++ b/pkg/reconciler/store/pg_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package store import ( diff --git a/pkg/registry/handler.go b/pkg/registry/handler.go index 041b60d..28470e1 100644 --- a/pkg/registry/handler.go +++ b/pkg/registry/handler.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package registry implements the Splice Registry API for external wallet integration. // // It exposes contract discovery endpoints that allow wallets like Canton Loop diff --git a/pkg/relayer/config.go b/pkg/relayer/config.go index b56d55d..7c0ea81 100644 --- a/pkg/relayer/config.go +++ b/pkg/relayer/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package relayer import "time" diff --git a/pkg/relayer/engine/destination.go b/pkg/relayer/engine/destination.go index 8ce9bae..43083bb 100644 --- a/pkg/relayer/engine/destination.go +++ b/pkg/relayer/engine/destination.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package engine import ( diff --git a/pkg/relayer/engine/destination_test.go b/pkg/relayer/engine/destination_test.go index 7e6e8c1..020cd32 100644 --- a/pkg/relayer/engine/destination_test.go +++ b/pkg/relayer/engine/destination_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package engine_test import ( diff --git a/pkg/relayer/engine/engine.go b/pkg/relayer/engine/engine.go index d58cf8b..f94d495 100644 --- a/pkg/relayer/engine/engine.go +++ b/pkg/relayer/engine/engine.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package engine import ( diff --git a/pkg/relayer/engine/engine_test.go b/pkg/relayer/engine/engine_test.go index 5a4c8dd..2ea6892 100644 --- a/pkg/relayer/engine/engine_test.go +++ b/pkg/relayer/engine/engine_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package engine import ( diff --git a/pkg/relayer/engine/metrics.go b/pkg/relayer/engine/metrics.go index e055e58..cf463fc 100644 --- a/pkg/relayer/engine/metrics.go +++ b/pkg/relayer/engine/metrics.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package engine import ( diff --git a/pkg/relayer/engine/processor.go b/pkg/relayer/engine/processor.go index b0267be..487fd5c 100644 --- a/pkg/relayer/engine/processor.go +++ b/pkg/relayer/engine/processor.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package engine import ( diff --git a/pkg/relayer/engine/processor_test.go b/pkg/relayer/engine/processor_test.go index 30fbed1..d980bc3 100644 --- a/pkg/relayer/engine/processor_test.go +++ b/pkg/relayer/engine/processor_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package engine_test import ( diff --git a/pkg/relayer/engine/source.go b/pkg/relayer/engine/source.go index a2f4bf0..9d49b89 100644 --- a/pkg/relayer/engine/source.go +++ b/pkg/relayer/engine/source.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package engine import ( diff --git a/pkg/relayer/engine/source_test.go b/pkg/relayer/engine/source_test.go index c0a3bb1..bb3991f 100644 --- a/pkg/relayer/engine/source_test.go +++ b/pkg/relayer/engine/source_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package engine_test import ( diff --git a/pkg/relayer/service/http.go b/pkg/relayer/service/http.go index 2c05b8b..0bf7755 100644 --- a/pkg/relayer/service/http.go +++ b/pkg/relayer/service/http.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/relayer/service/log.go b/pkg/relayer/service/log.go index 3f6c0b5..337cfdf 100644 --- a/pkg/relayer/service/log.go +++ b/pkg/relayer/service/log.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/relayer/service/service.go b/pkg/relayer/service/service.go index 9765a3d..16b6fc3 100644 --- a/pkg/relayer/service/service.go +++ b/pkg/relayer/service/service.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package service provides the relayer HTTP service layer. package service diff --git a/pkg/relayer/store/instrumented.go b/pkg/relayer/store/instrumented.go index c7f1fe1..cab64f6 100644 --- a/pkg/relayer/store/instrumented.go +++ b/pkg/relayer/store/instrumented.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package store import ( diff --git a/pkg/relayer/store/metrics.go b/pkg/relayer/store/metrics.go index 85e31c1..95cda17 100644 --- a/pkg/relayer/store/metrics.go +++ b/pkg/relayer/store/metrics.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package store import ( diff --git a/pkg/relayer/store/model.go b/pkg/relayer/store/model.go index f398565..a29ebc1 100644 --- a/pkg/relayer/store/model.go +++ b/pkg/relayer/store/model.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package store import ( diff --git a/pkg/relayer/store/pg.go b/pkg/relayer/store/pg.go index 4094535..7b92820 100644 --- a/pkg/relayer/store/pg.go +++ b/pkg/relayer/store/pg.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package store import ( diff --git a/pkg/relayer/store/pg_test.go b/pkg/relayer/store/pg_test.go index a9b2ac1..f532249 100644 --- a/pkg/relayer/store/pg_test.go +++ b/pkg/relayer/store/pg_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package store import ( diff --git a/pkg/relayer/types.go b/pkg/relayer/types.go index d9938b1..35848f0 100644 --- a/pkg/relayer/types.go +++ b/pkg/relayer/types.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package relayer import "time" diff --git a/pkg/token/config.go b/pkg/token/config.go index b5b920a..1e06239 100644 --- a/pkg/token/config.go +++ b/pkg/token/config.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token import ( diff --git a/pkg/token/erc20.go b/pkg/token/erc20.go index 3b73ba2..5f61b0b 100644 --- a/pkg/token/erc20.go +++ b/pkg/token/erc20.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token import ( diff --git a/pkg/token/erc20_test.go b/pkg/token/erc20_test.go index ff35e84..432efe3 100644 --- a/pkg/token/erc20_test.go +++ b/pkg/token/erc20_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token_test import ( diff --git a/pkg/token/http.go b/pkg/token/http.go index bf55d0d..e7ef7cc 100644 --- a/pkg/token/http.go +++ b/pkg/token/http.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token import ( diff --git a/pkg/token/http_test.go b/pkg/token/http_test.go index 43162d8..c83fe92 100644 --- a/pkg/token/http_test.go +++ b/pkg/token/http_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token_test import ( diff --git a/pkg/token/native.go b/pkg/token/native.go index 6bfeeac..d39d749 100644 --- a/pkg/token/native.go +++ b/pkg/token/native.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token import ( diff --git a/pkg/token/native_test.go b/pkg/token/native_test.go index 51c3883..d1b75ae 100644 --- a/pkg/token/native_test.go +++ b/pkg/token/native_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token_test import ( diff --git a/pkg/token/provider/canton.go b/pkg/token/provider/canton.go index a5bbaac..023745c 100644 --- a/pkg/token/provider/canton.go +++ b/pkg/token/provider/canton.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package provider import ( diff --git a/pkg/token/provider/indexer.go b/pkg/token/provider/indexer.go index 3b6248e..70f1eff 100644 --- a/pkg/token/provider/indexer.go +++ b/pkg/token/provider/indexer.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package provider import ( diff --git a/pkg/token/service.go b/pkg/token/service.go index 850fe45..142cabc 100644 --- a/pkg/token/service.go +++ b/pkg/token/service.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token import ( diff --git a/pkg/token/types.go b/pkg/token/types.go index 923d25f..12e5015 100644 --- a/pkg/token/types.go +++ b/pkg/token/types.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package token // Type identifies a token for balance operations. diff --git a/pkg/transfer/cache.go b/pkg/transfer/cache.go index 799c325..25df63b 100644 --- a/pkg/transfer/cache.go +++ b/pkg/transfer/cache.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package transfer import ( diff --git a/pkg/transfer/cache_test.go b/pkg/transfer/cache_test.go index c29433d..26571e8 100644 --- a/pkg/transfer/cache_test.go +++ b/pkg/transfer/cache_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package transfer import ( diff --git a/pkg/transfer/http.go b/pkg/transfer/http.go index ce16a51..c8e3e71 100644 --- a/pkg/transfer/http.go +++ b/pkg/transfer/http.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package transfer import ( diff --git a/pkg/transfer/log.go b/pkg/transfer/log.go index 188bdd6..2933d06 100644 --- a/pkg/transfer/log.go +++ b/pkg/transfer/log.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package transfer import ( diff --git a/pkg/transfer/service.go b/pkg/transfer/service.go index 52dd790..a2a8ec6 100644 --- a/pkg/transfer/service.go +++ b/pkg/transfer/service.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package transfer import ( diff --git a/pkg/transfer/service_test.go b/pkg/transfer/service_test.go index 6c9ecf8..2e0ed05 100644 --- a/pkg/transfer/service_test.go +++ b/pkg/transfer/service_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package transfer import ( diff --git a/pkg/transfer/types.go b/pkg/transfer/types.go index 25d4e1d..99f4ebd 100644 --- a/pkg/transfer/types.go +++ b/pkg/transfer/types.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + // Package transfer implements the non-custodial prepare/execute transfer API. package transfer diff --git a/pkg/user/service/http.go b/pkg/user/service/http.go index bdc391a..64e75e9 100644 --- a/pkg/user/service/http.go +++ b/pkg/user/service/http.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/user/service/http_test.go b/pkg/user/service/http_test.go index d71617c..fcedf4f 100644 --- a/pkg/user/service/http_test.go +++ b/pkg/user/service/http_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/user/service/log.go b/pkg/user/service/log.go index a607d58..90f4425 100644 --- a/pkg/user/service/log.go +++ b/pkg/user/service/log.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/user/service/service.go b/pkg/user/service/service.go index fcd1e6d..f83eda1 100644 --- a/pkg/user/service/service.go +++ b/pkg/user/service/service.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/user/service/service_test.go b/pkg/user/service/service_test.go index fd3092f..2fcbc4a 100644 --- a/pkg/user/service/service_test.go +++ b/pkg/user/service/service_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/user/service/topology_cache.go b/pkg/user/service/topology_cache.go index bf1b8b9..7f395e7 100644 --- a/pkg/user/service/topology_cache.go +++ b/pkg/user/service/topology_cache.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package service import ( diff --git a/pkg/user/user.go b/pkg/user/user.go index ea8c432..3a9895f 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package user import ( diff --git a/pkg/userstore/model.go b/pkg/userstore/model.go index de26c6a..2d08842 100644 --- a/pkg/userstore/model.go +++ b/pkg/userstore/model.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package userstore import ( diff --git a/pkg/userstore/pg.go b/pkg/userstore/pg.go index 0c57cf4..283709e 100644 --- a/pkg/userstore/pg.go +++ b/pkg/userstore/pg.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package userstore import ( diff --git a/pkg/userstore/pg_test.go b/pkg/userstore/pg_test.go index 5693111..331d843 100644 --- a/pkg/userstore/pg_test.go +++ b/pkg/userstore/pg_test.go @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + package userstore import ( diff --git a/scripts/dev/add-spdx-headers.sh b/scripts/dev/add-spdx-headers.sh new file mode 100755 index 0000000..2a4f3e0 --- /dev/null +++ b/scripts/dev/add-spdx-headers.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# +# Idempotently add `SPDX-License-Identifier: Apache-2.0` headers to first-party +# source files. Re-running this script is safe; files that already carry an +# SPDX header in their first 5 lines are skipped, and files that look +# generated are skipped entirely. +# +# Usage: bash scripts/dev/add-spdx-headers.sh +# +# Run from the repository root. + +set -euo pipefail + +SPDX_LINE_SLASH="// SPDX-License-Identifier: Apache-2.0" +SPDX_LINE_HASH="# SPDX-License-Identifier: Apache-2.0" + +# Returns 0 (true) if the file already has an SPDX header in its first 5 lines, +# OR if it looks like generated code we should skip. +should_skip() { + local file="$1" + local first_five + first_five=$(head -n 5 "$file" 2>/dev/null || true) + + case "$first_five" in + *"SPDX-License-Identifier"*) return 0 ;; + *"Code generated"*) return 0 ;; + *"DO NOT EDIT"*) return 0 ;; + *"protoc-gen-go"*) return 0 ;; + *"abigen"*) return 0 ;; + esac + return 1 +} + +# Prepend a header line to the top of the file. +prepend_top() { + local file="$1" + local header="$2" + local tmp + tmp=$(mktemp) + { + printf '%s\n\n' "$header" + cat "$file" + } >"$tmp" + mv "$tmp" "$file" +} + +# Insert a header line immediately after the shebang on line 1. +insert_after_shebang() { + local file="$1" + local header="$2" + local tmp + tmp=$(mktemp) + { + head -n 1 "$file" + printf '%s\n' "$header" + tail -n +2 "$file" + } >"$tmp" + mv "$tmp" "$file" +} + +# Process Go files. +process_go() { + local count=0 skipped=0 + while IFS= read -r -d '' file; do + if should_skip "$file"; then + skipped=$((skipped + 1)) + continue + fi + prepend_top "$file" "$SPDX_LINE_SLASH" + count=$((count + 1)) + done < <(find cmd pkg -type f -name '*.go' \ + -not -path '*/lapi/v2/*' \ + -not -path '*/ethereum/contracts/*' \ + -not -name '*.pb.go' \ + -print0) + printf 'Go: added headers to %d files (skipped %d)\n' "$count" "$skipped" +} + +# Process shell scripts. +process_shell() { + local count=0 skipped=0 + while IFS= read -r -d '' file; do + if should_skip "$file"; then + skipped=$((skipped + 1)) + continue + fi + if head -n 1 "$file" | grep -q '^#!'; then + insert_after_shebang "$file" "$SPDX_LINE_HASH" + else + prepend_top "$file" "$SPDX_LINE_HASH" + fi + count=$((count + 1)) + done < <(find scripts -type f -name '*.sh' -print0) + printf 'Shell: added headers to %d files (skipped %d)\n' "$count" "$skipped" +} + +# Process Dockerfiles. Targets explicit list rather than a glob to avoid +# accidentally touching vendor/test Dockerfiles outside our scope. +process_dockerfiles() { + local count=0 skipped=0 + local dockerfiles=( + "Dockerfile.local" + "cmd/api-server/Dockerfile" + "cmd/indexer/Dockerfile" + "cmd/relayer/Dockerfile" + ) + for file in "${dockerfiles[@]}"; do + if [[ ! -f "$file" ]]; then + continue + fi + if should_skip "$file"; then + skipped=$((skipped + 1)) + continue + fi + prepend_top "$file" "$SPDX_LINE_HASH" + count=$((count + 1)) + done + printf 'Dockerfile: added headers to %d files (skipped %d)\n' "$count" "$skipped" +} + +main() { + if [[ ! -f go.mod ]]; then + echo "error: run from repository root (go.mod not found)" >&2 + exit 1 + fi + process_go + process_shell + process_dockerfiles +} + +main "$@" From 6832c5bb031cfd719242a39fd10f2a5e9a91ac28 Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Wed, 27 May 2026 04:50:17 -0600 Subject: [PATCH 04/10] licensing: add SPDX headers to scripts and Dockerfiles Adds `# SPDX-License-Identifier: Apache-2.0` to first-party shell scripts under `scripts/` and to the production Dockerfiles (`Dockerfile.local`, `cmd/api-server/Dockerfile`, `cmd/indexer/Dockerfile`, `cmd/relayer/Dockerfile`). For shell scripts, the header is inserted directly after the shebang; for Dockerfiles it sits at the top of the file. --- Dockerfile.local | 2 ++ cmd/api-server/Dockerfile | 2 ++ cmd/indexer/Dockerfile | 2 ++ cmd/relayer/Dockerfile | 2 ++ scripts/lib/bridge.sh | 1 + scripts/lib/common.sh | 1 + scripts/lib/config.sh | 1 + scripts/lib/services.sh | 1 + scripts/remote/bootstrap-remote.sh | 1 + scripts/setup/bootstrap-all.sh | 1 + scripts/setup/build-dars.sh | 1 + scripts/setup/docker-bootstrap.sh | 1 + scripts/setup/download-usdcx-dars.sh | 1 + scripts/setup/entrypoint.sh | 1 + scripts/setup/generate-protos.sh | 1 + scripts/setup/init-multiple-dbs.sh | 1 + scripts/setup/setup-devnet.sh | 1 + scripts/setup/setup-local.sh | 1 + scripts/testing/bootstrap-local.sh | 1 + scripts/testing/e2e-local.sh | 1 + scripts/utils/check-anvil-state.sh | 1 + scripts/utils/metamask-info-devnet.sh | 1 + scripts/utils/metamask-info.sh | 1 + scripts/utils/query-canton-holdings.sh | 1 + 24 files changed, 28 insertions(+) mode change 100755 => 100644 scripts/lib/bridge.sh mode change 100755 => 100644 scripts/lib/common.sh mode change 100755 => 100644 scripts/lib/config.sh mode change 100755 => 100644 scripts/lib/services.sh mode change 100755 => 100644 scripts/remote/bootstrap-remote.sh mode change 100755 => 100644 scripts/setup/bootstrap-all.sh mode change 100755 => 100644 scripts/setup/build-dars.sh mode change 100755 => 100644 scripts/setup/download-usdcx-dars.sh mode change 100755 => 100644 scripts/setup/entrypoint.sh mode change 100755 => 100644 scripts/setup/generate-protos.sh mode change 100755 => 100644 scripts/setup/init-multiple-dbs.sh mode change 100755 => 100644 scripts/setup/setup-devnet.sh mode change 100755 => 100644 scripts/setup/setup-local.sh mode change 100755 => 100644 scripts/testing/bootstrap-local.sh mode change 100755 => 100644 scripts/testing/e2e-local.sh mode change 100755 => 100644 scripts/utils/check-anvil-state.sh mode change 100755 => 100644 scripts/utils/metamask-info-devnet.sh mode change 100755 => 100644 scripts/utils/metamask-info.sh mode change 100755 => 100644 scripts/utils/query-canton-holdings.sh diff --git a/Dockerfile.local b/Dockerfile.local index df861b5..7951f4b 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 + # Multi-stage build for relayer + bootstrap tools # Using full golang image (not alpine) for toolchain auto-download support FROM golang:1.23 AS builder diff --git a/cmd/api-server/Dockerfile b/cmd/api-server/Dockerfile index 00cbbd6..76c3a41 100644 --- a/cmd/api-server/Dockerfile +++ b/cmd/api-server/Dockerfile @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 + # Build stage # Using full golang image (not alpine) for toolchain auto-download support FROM golang:1.24 AS builder diff --git a/cmd/indexer/Dockerfile b/cmd/indexer/Dockerfile index 0b3d0c3..e2fe851 100644 --- a/cmd/indexer/Dockerfile +++ b/cmd/indexer/Dockerfile @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 + # Build stage # Using full golang image (not alpine) for toolchain auto-download support FROM golang:1.24 AS builder diff --git a/cmd/relayer/Dockerfile b/cmd/relayer/Dockerfile index 0d3510c..c07afa7 100644 --- a/cmd/relayer/Dockerfile +++ b/cmd/relayer/Dockerfile @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 + # Build stage # Using full golang image (not alpine) for toolchain auto-download support FROM golang:1.24 AS builder diff --git a/scripts/lib/bridge.sh b/scripts/lib/bridge.sh old mode 100755 new mode 100644 index 2f88771..7d614aa --- a/scripts/lib/bridge.sh +++ b/scripts/lib/bridge.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # ============================================================================= # Bridge operations for Canton-Ethereum bridge tests # ============================================================================= diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh old mode 100755 new mode 100644 index 82b11fe..21900d2 --- a/scripts/lib/common.sh +++ b/scripts/lib/common.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # ============================================================================= # Common utilities for Canton-Ethereum bridge test scripts # ============================================================================= diff --git a/scripts/lib/config.sh b/scripts/lib/config.sh old mode 100755 new mode 100644 index dae006a..6b15755 --- a/scripts/lib/config.sh +++ b/scripts/lib/config.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # ============================================================================= # Configuration loading for Canton-Ethereum bridge tests # ============================================================================= diff --git a/scripts/lib/services.sh b/scripts/lib/services.sh old mode 100755 new mode 100644 index a77e493..e442c3c --- a/scripts/lib/services.sh +++ b/scripts/lib/services.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # ============================================================================= # Service management for Canton-Ethereum bridge tests # ============================================================================= diff --git a/scripts/remote/bootstrap-remote.sh b/scripts/remote/bootstrap-remote.sh old mode 100755 new mode 100644 index c791c0f..141d654 --- a/scripts/remote/bootstrap-remote.sh +++ b/scripts/remote/bootstrap-remote.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # ============================================================================= # Canton Bridge Remote Bootstrap Script (DevNet / Mainnet) # ============================================================================= diff --git a/scripts/setup/bootstrap-all.sh b/scripts/setup/bootstrap-all.sh old mode 100755 new mode 100644 index 658b16e..4de7e19 --- a/scripts/setup/bootstrap-all.sh +++ b/scripts/setup/bootstrap-all.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # ============================================================================= # Canton Bridge Full Bootstrap Script # ============================================================================= diff --git a/scripts/setup/build-dars.sh b/scripts/setup/build-dars.sh old mode 100755 new mode 100644 index 0e678a8..7c69396 --- a/scripts/setup/build-dars.sh +++ b/scripts/setup/build-dars.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # ============================================================================= # Build all DAR packages for Canton Bridge # ============================================================================= diff --git a/scripts/setup/docker-bootstrap.sh b/scripts/setup/docker-bootstrap.sh index 947644c..bea7f82 100644 --- a/scripts/setup/docker-bootstrap.sh +++ b/scripts/setup/docker-bootstrap.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # ============================================================================= # Docker Bootstrap Script # ============================================================================= diff --git a/scripts/setup/download-usdcx-dars.sh b/scripts/setup/download-usdcx-dars.sh old mode 100755 new mode 100644 index d50abcd..07e1ff5 --- a/scripts/setup/download-usdcx-dars.sh +++ b/scripts/setup/download-usdcx-dars.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" diff --git a/scripts/setup/entrypoint.sh b/scripts/setup/entrypoint.sh old mode 100755 new mode 100644 index 531ff33..7216596 --- a/scripts/setup/entrypoint.sh +++ b/scripts/setup/entrypoint.sh @@ -1,4 +1,5 @@ #!/bin/sh +# SPDX-License-Identifier: Apache-2.0 set -e # entrypoint.sh - Entrypoint script for Go services to handle migrations and startup diff --git a/scripts/setup/generate-protos.sh b/scripts/setup/generate-protos.sh old mode 100755 new mode 100644 index b9d8357..c35712c --- a/scripts/setup/generate-protos.sh +++ b/scripts/setup/generate-protos.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # Script to generate Go protobuf stubs for Canton Ledger API # Updated for Canton 3.4.8 which uses v2 API only diff --git a/scripts/setup/init-multiple-dbs.sh b/scripts/setup/init-multiple-dbs.sh old mode 100755 new mode 100644 index cbc97df..a87f1ed --- a/scripts/setup/init-multiple-dbs.sh +++ b/scripts/setup/init-multiple-dbs.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # init-multiple-dbs.sh - Initialize multiple PostgreSQL databases # This script is executed by postgres docker-entrypoint-initdb.d diff --git a/scripts/setup/setup-devnet.sh b/scripts/setup/setup-devnet.sh old mode 100755 new mode 100644 index 48379d9..e4c976c --- a/scripts/setup/setup-devnet.sh +++ b/scripts/setup/setup-devnet.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # ============================================================================= # Canton-Ethereum Bridge - DevNet + Sepolia Setup Script # ============================================================================= diff --git a/scripts/setup/setup-local.sh b/scripts/setup/setup-local.sh old mode 100755 new mode 100644 index ba2fccc..21984b9 --- a/scripts/setup/setup-local.sh +++ b/scripts/setup/setup-local.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # ============================================================================= # Canton-Ethereum Bridge - Local Setup Script # ============================================================================= diff --git a/scripts/testing/bootstrap-local.sh b/scripts/testing/bootstrap-local.sh old mode 100755 new mode 100644 index 86b9ce7..7a4912c --- a/scripts/testing/bootstrap-local.sh +++ b/scripts/testing/bootstrap-local.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # ============================================================================= # Canton-Ethereum Bridge - Local Bootstrap (One-Command Setup) # ============================================================================= diff --git a/scripts/testing/e2e-local.sh b/scripts/testing/e2e-local.sh old mode 100755 new mode 100644 index c9e0e4d..8345d4f --- a/scripts/testing/e2e-local.sh +++ b/scripts/testing/e2e-local.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # ============================================================================= # Canton-Ethereum Bridge Local E2E Test (Bash + Cast version - Refactored) # ============================================================================= diff --git a/scripts/utils/check-anvil-state.sh b/scripts/utils/check-anvil-state.sh old mode 100755 new mode 100644 index ea90419..026f752 --- a/scripts/utils/check-anvil-state.sh +++ b/scripts/utils/check-anvil-state.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # Quick diagnostic script to check Anvil state echo "=== Checking Anvil State ===" diff --git a/scripts/utils/metamask-info-devnet.sh b/scripts/utils/metamask-info-devnet.sh old mode 100755 new mode 100644 index 68cc0d0..50c9b4d --- a/scripts/utils/metamask-info-devnet.sh +++ b/scripts/utils/metamask-info-devnet.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # ============================================================================= # MetaMask Connection Info - DevNet/Sepolia # ============================================================================= diff --git a/scripts/utils/metamask-info.sh b/scripts/utils/metamask-info.sh old mode 100755 new mode 100644 index b5f189e..2ec6b22 --- a/scripts/utils/metamask-info.sh +++ b/scripts/utils/metamask-info.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # ============================================================================= # MetaMask Connection Info # ============================================================================= diff --git a/scripts/utils/query-canton-holdings.sh b/scripts/utils/query-canton-holdings.sh old mode 100755 new mode 100644 index 27a30fc..8e773cd --- a/scripts/utils/query-canton-holdings.sh +++ b/scripts/utils/query-canton-holdings.sh @@ -1,4 +1,5 @@ #!/bin/bash +# SPDX-License-Identifier: Apache-2.0 # Query Canton Holdings Script # # This script queries CIP56Holding contracts from Canton via the Go verification tool, From 8cfdeea85286a153fe8f79ad90bed1e61a673b59 Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Wed, 27 May 2026 05:02:52 -0600 Subject: [PATCH 05/10] licensing: update README license section Replaces the `[License details here]` placeholder with a real license section pointing to the Apache 2.0 LICENSE and the NOTICE file. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c1115bc..053f227 100644 --- a/README.md +++ b/README.md @@ -339,4 +339,7 @@ Key Canton-specific fields (auto-detected by bootstrap): ## License -[License details here] +This project is licensed under the Apache License, Version 2.0. See +[LICENSE](LICENSE) for the full license text and [NOTICE](NOTICE) for +attributions of third-party software included in or depended on by this +project. From cbbcd74efb9105f5039865b271c935ac2ca35e2c Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Wed, 27 May 2026 10:01:04 -0600 Subject: [PATCH 06/10] licensing: restore executable bit on shell scripts The SPDX-header sweep in 6832c5b inadvertently downgraded 19 shell scripts from 100755 to 100644, breaking direct invocation via `./scripts/.../foo.sh` (notably `make build-dars` in the E2E workflow, which exits 127 when build-dars.sh is not executable). Root cause: the helper script used `mv tmp file` after rewriting each file, and `rename(2)` adopts the source file's mode (mktemp default 0600 -> normalized to 100644 in git). Restoring the executable bit here; the helper script itself will be updated separately to preserve target mode on rewrite. --- scripts/lib/bridge.sh | 0 scripts/lib/common.sh | 0 scripts/lib/config.sh | 0 scripts/lib/services.sh | 0 scripts/remote/bootstrap-remote.sh | 0 scripts/setup/bootstrap-all.sh | 0 scripts/setup/build-dars.sh | 0 scripts/setup/download-usdcx-dars.sh | 0 scripts/setup/entrypoint.sh | 0 scripts/setup/generate-protos.sh | 0 scripts/setup/init-multiple-dbs.sh | 0 scripts/setup/setup-devnet.sh | 0 scripts/setup/setup-local.sh | 0 scripts/testing/bootstrap-local.sh | 0 scripts/testing/e2e-local.sh | 0 scripts/testing/send-usdcx-direct.go | 618 ++++++++++++++++++++++ scripts/testing/send-usdcx-transfer.go | 161 ++++++ scripts/testing/withdraw-via-interface.go | 561 ++++++++++++++++++++ scripts/utils/check-anvil-state.sh | 0 scripts/utils/metamask-info-devnet.sh | 0 scripts/utils/metamask-info.sh | 0 scripts/utils/query-canton-holdings.sh | 0 22 files changed, 1340 insertions(+) mode change 100644 => 100755 scripts/lib/bridge.sh mode change 100644 => 100755 scripts/lib/common.sh mode change 100644 => 100755 scripts/lib/config.sh mode change 100644 => 100755 scripts/lib/services.sh mode change 100644 => 100755 scripts/remote/bootstrap-remote.sh mode change 100644 => 100755 scripts/setup/bootstrap-all.sh mode change 100644 => 100755 scripts/setup/build-dars.sh mode change 100644 => 100755 scripts/setup/download-usdcx-dars.sh mode change 100644 => 100755 scripts/setup/entrypoint.sh mode change 100644 => 100755 scripts/setup/generate-protos.sh mode change 100644 => 100755 scripts/setup/init-multiple-dbs.sh mode change 100644 => 100755 scripts/setup/setup-devnet.sh mode change 100644 => 100755 scripts/setup/setup-local.sh mode change 100644 => 100755 scripts/testing/bootstrap-local.sh mode change 100644 => 100755 scripts/testing/e2e-local.sh create mode 100644 scripts/testing/send-usdcx-direct.go create mode 100644 scripts/testing/send-usdcx-transfer.go create mode 100644 scripts/testing/withdraw-via-interface.go mode change 100644 => 100755 scripts/utils/check-anvil-state.sh mode change 100644 => 100755 scripts/utils/metamask-info-devnet.sh mode change 100644 => 100755 scripts/utils/metamask-info.sh mode change 100644 => 100755 scripts/utils/query-canton-holdings.sh diff --git a/scripts/lib/bridge.sh b/scripts/lib/bridge.sh old mode 100644 new mode 100755 diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh old mode 100644 new mode 100755 diff --git a/scripts/lib/config.sh b/scripts/lib/config.sh old mode 100644 new mode 100755 diff --git a/scripts/lib/services.sh b/scripts/lib/services.sh old mode 100644 new mode 100755 diff --git a/scripts/remote/bootstrap-remote.sh b/scripts/remote/bootstrap-remote.sh old mode 100644 new mode 100755 diff --git a/scripts/setup/bootstrap-all.sh b/scripts/setup/bootstrap-all.sh old mode 100644 new mode 100755 diff --git a/scripts/setup/build-dars.sh b/scripts/setup/build-dars.sh old mode 100644 new mode 100755 diff --git a/scripts/setup/download-usdcx-dars.sh b/scripts/setup/download-usdcx-dars.sh old mode 100644 new mode 100755 diff --git a/scripts/setup/entrypoint.sh b/scripts/setup/entrypoint.sh old mode 100644 new mode 100755 diff --git a/scripts/setup/generate-protos.sh b/scripts/setup/generate-protos.sh old mode 100644 new mode 100755 diff --git a/scripts/setup/init-multiple-dbs.sh b/scripts/setup/init-multiple-dbs.sh old mode 100644 new mode 100755 diff --git a/scripts/setup/setup-devnet.sh b/scripts/setup/setup-devnet.sh old mode 100644 new mode 100755 diff --git a/scripts/setup/setup-local.sh b/scripts/setup/setup-local.sh old mode 100644 new mode 100755 diff --git a/scripts/testing/bootstrap-local.sh b/scripts/testing/bootstrap-local.sh old mode 100644 new mode 100755 diff --git a/scripts/testing/e2e-local.sh b/scripts/testing/e2e-local.sh old mode 100644 new mode 100755 diff --git a/scripts/testing/send-usdcx-direct.go b/scripts/testing/send-usdcx-direct.go new file mode 100644 index 0000000..b82ffdf --- /dev/null +++ b/scripts/testing/send-usdcx-direct.go @@ -0,0 +1,618 @@ +//go:build ignore + +// send-usdcx-direct.go — Send USDCx from a standalone external party, bypassing +// the middleware's hardcoded 1-hour offer validity so the receiver has effectively +// unlimited time to accept. +// +// Mirrors accept-via-interface.go in structure: loads party credentials, calls +// the registrar's HTTP API to get the transfer factory + choice context + +// disclosed contracts, encodes the AnyValue-shaped choice context inline, then +// exercises Splice.Api.Token.TransferInstructionV1:TransferFactory_Transfer +// via Interactive Submission with a far-future executeBefore. +// +// Usage: +// go run scripts/testing/send-usdcx-direct.go \ +// -config config.api-server.devnet-test.yaml \ +// -creds ./party-credentials.txt \ +// -to "damlcopilot-receiver::1220096316d4ea75c021d89123cfd2792cfeac80dfbf90bfbca21bcd8bf1bb40d84c" \ +// -amount "2" + +package main + +import ( + "bufio" + "bytes" + "context" + "encoding/base64" + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "os" + "sort" + "strings" + "time" + + canton "github.com/chainsafe/canton-middleware/pkg/cantonsdk/client" + lapiv2 "github.com/chainsafe/canton-middleware/pkg/cantonsdk/lapi/v2" + interactivev2 "github.com/chainsafe/canton-middleware/pkg/cantonsdk/lapi/v2/interactive" + "github.com/chainsafe/canton-middleware/pkg/cantonsdk/token" + "github.com/chainsafe/canton-middleware/pkg/cantonsdk/values" + "github.com/chainsafe/canton-middleware/pkg/config" + "github.com/chainsafe/canton-middleware/pkg/keys" + + "github.com/google/uuid" + "go.uber.org/zap" +) + +const ( + // Splice TransferFactory interface — the choice argument shape matches the + // SDK's encodeTransferFactoryTransferArgs in pkg/cantonsdk/token/encode.go. + transferFactoryPackageID = "#splice-api-token-transfer-instruction-v1" + transferFactoryModule = "Splice.Api.Token.TransferInstructionV1" + transferFactoryEntity = "TransferFactory" + transferChoice = "TransferFactory_Transfer" +) + +var ( + configPath = flag.String("config", "config.api-server.devnet-test.yaml", "Config file") + credsPath = flag.String("creds", "./party-credentials.txt", "Party credentials file") + toParty = flag.String("to", "", "Receiver party ID (required)") + amount = flag.String("amount", "2", "Amount to send (decimal string)") + instrumentID = flag.String("instrument", "USDCx", "Instrument ID (token symbol on Canton)") + registryHost = flag.String("registry-host", "https://api.utilities.digitalasset-dev.com", "Registrar API base URL") + registrar = flag.String("registrar", "decentralized-usdc-interchain-rep::1220d420ba8f168d63157f610e6593dca072bbd79ff90a830efc345ed4348a816de7", "Registrar / instrument admin party ID") + expiry = flag.String("expiry", "2099-12-31T23:59:59Z", "Offer executeBefore (RFC3339). Default ≈ no time limit") + dryRun = flag.Bool("dry-run", false, "Print the registry response + plan, do not submit") + debug = flag.Bool("debug", true, "Print registry response JSON") +) + +func main() { + flag.Parse() + if *toParty == "" { + fatalf("-to is required") + } + + creds, err := loadCredentials(*credsPath) + if err != nil { + fatalf("load credentials: %v", err) + } + sender := creds["party_id"] + if sender == "" { + fatalf("party_id missing from credentials") + } + privBytes, err := hex.DecodeString(creds["private_key_hex"]) + if err != nil { + fatalf("decode private key: %v", err) + } + kp, err := keys.CantonKeyPairFromPrivateKey(privBytes) + if err != nil { + fatalf("reconstruct keypair: %v", err) + } + fp, _ := kp.Fingerprint() + + executeBefore, err := time.Parse(time.RFC3339, *expiry) + if err != nil { + fatalf("parse -expiry: %v", err) + } + + fmt.Printf(">>> sender party : %s\n", sender) + fmt.Printf(">>> sender fingerprint : %s\n", fp) + fmt.Printf(">>> receiver party : %s\n", *toParty) + fmt.Printf(">>> amount : %s %s\n", *amount, *instrumentID) + fmt.Printf(">>> executeBefore : %s (%s)\n", executeBefore.Format(time.RFC3339), humanizeDuration(time.Until(executeBefore))) + + cfg, err := config.LoadAPIServer(*configPath) + if err != nil { + fatalf("load config: %v", err) + } + logger, _ := zap.NewDevelopment() + ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) + defer cancel() + + cantonClient, err := canton.New(ctx, cfg.Canton, canton.WithLogger(logger)) + if err != nil { + fatalf("canton client: %v", err) + } + defer func() { _ = cantonClient.Close() }() + + holdings, err := cantonClient.Token.GetHoldingsByParty(ctx, sender, *instrumentID) + if err != nil { + fatalf("get holdings: %v", err) + } + fmt.Printf("\n>>> sender %s holdings: %d contract(s)\n", *instrumentID, len(holdings)) + for i, h := range holdings { + fmt.Printf(" #%d amount=%s locked=%v cid=%s\n", i+1, h.Amount, h.Locked, h.ContractID) + } + + inputCIDs, total, err := selectHoldings(holdings, *amount) + if err != nil { + fatalf("select holdings: %v", err) + } + fmt.Printf(">>> selected %d holding(s) totaling %s for amount %s\n", len(inputCIDs), total, *amount) + + httpClient := &http.Client{Timeout: 30 * time.Second} + regResp, raw, err := fetchTransferFactory(ctx, httpClient, *registryHost, *registrar, sender, *toParty, *amount, *instrumentID, inputCIDs, executeBefore) + if err != nil { + fatalf("registry transfer-factory: %v", err) + } + if *debug { + fmt.Printf("\n --- registry response ---\n%s\n -------------------------\n", indentJSON(raw)) + } + fmt.Printf(">>> factoryId : %s\n", regResp.FactoryID) + fmt.Printf(">>> transferKind : %s\n", regResp.TransferKind) + fmt.Printf(">>> disclosed contracts: %d\n", len(regResp.ChoiceContext.DisclosedContracts)) + + ctxValue, err := encodeChoiceContextRecord(regResp.ChoiceContext.ChoiceContextData.Values) + if err != nil { + fatalf("encode choice context: %v", err) + } + extraArgs := buildExtraArgsValue(ctxValue) + + disclosed, err := buildDisclosedContracts(regResp.ChoiceContext.DisclosedContracts, cfg.Canton.DomainID) + if err != nil { + fatalf("build disclosed contracts: %v", err) + } + + if *dryRun { + fmt.Println("\n[dry-run] would exercise TransferFactory_Transfer — skipping submission") + return + } + + t0 := time.Now() + offerCID, err := submitTransfer(ctx, cantonClient, cfg, sender, kp, regResp.FactoryID, *toParty, *amount, *instrumentID, inputCIDs, executeBefore, extraArgs, disclosed) + if err != nil { + fatalf("submit transfer: %v", err) + } + fmt.Printf("\n>>> SUCCESS in %s\n>>> TransferOffer CID: %s\n>>> receiver must exercise TransferInstruction_Accept before %s to settle\n", + time.Since(t0).Round(time.Millisecond), offerCID, executeBefore.Format(time.RFC3339)) +} + +// ---------- holdings selection ---------- + +func selectHoldings(holdings []*token.Holding, amount string) ([]string, string, error) { + // Greedy: sort by amount descending, take until total >= amount. + // Skip locked holdings (already committed elsewhere). + available := make([]*token.Holding, 0, len(holdings)) + for _, h := range holdings { + if h.Locked { + continue + } + available = append(available, h) + } + sort.Slice(available, func(i, j int) bool { + return parseDecimal(available[i].Amount) > parseDecimal(available[j].Amount) + }) + target := parseDecimal(amount) + var total float64 + var cids []string + for _, h := range available { + cids = append(cids, h.ContractID) + total += parseDecimal(h.Amount) + if total >= target { + return cids, fmt.Sprintf("%g", total), nil + } + } + return nil, "", fmt.Errorf("insufficient balance: have %g, need %s", total, amount) +} + +func parseDecimal(s string) float64 { + var f float64 + _, _ = fmt.Sscanf(s, "%f", &f) + return f +} + +// ---------- Registry: transfer-factory ---------- + +type transferFactoryResponse struct { + FactoryID string `json:"factoryId"` + TransferKind string `json:"transferKind"` + ChoiceContext transferFactoryInnerResponse `json:"choiceContext"` +} + +type transferFactoryInnerResponse struct { + ChoiceContextData struct { + Values map[string]json.RawMessage `json:"values"` + } `json:"choiceContextData"` + DisclosedContracts []registryDisclosedContract `json:"disclosedContracts"` +} + +type registryDisclosedContract struct { + ContractID string `json:"contractId"` + CreatedEventBlob string `json:"createdEventBlob"` + TemplateID json.RawMessage `json:"templateId"` + SynchronizerID string `json:"synchronizerId"` +} + +func fetchTransferFactory( + ctx context.Context, hc *http.Client, host, registrarParty, sender, receiver, amount, instrumentID string, + inputHoldingCIDs []string, executeBefore time.Time, +) (*transferFactoryResponse, []byte, error) { + now := time.Now().UTC() + emptyValues := map[string]any{"values": map[string]any{}} + + body, err := json.Marshal(map[string]any{ + "choiceArguments": map[string]any{ + "expectedAdmin": registrarParty, + "transfer": map[string]any{ + "sender": sender, + "receiver": receiver, + "amount": amount, + "instrumentId": map[string]any{ + "admin": registrarParty, + "id": instrumentID, + }, + "inputHoldingCids": inputHoldingCIDs, + "meta": emptyValues, + "requestedAt": now.Format("2006-01-02T15:04:05.000Z"), + "executeBefore": executeBefore.UTC().Format("2006-01-02T15:04:05.000Z"), + }, + "extraArgs": map[string]any{ + "context": emptyValues, + "meta": emptyValues, + }, + }, + }) + if err != nil { + return nil, nil, fmt.Errorf("marshal request: %w", err) + } + + url := fmt.Sprintf( + "%s/api/token-standard/v0/registrars/%s/registry/transfer-instruction/v1/transfer-factory", + strings.TrimRight(host, "/"), registrarParty, + ) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + resp, err := hc.Do(req) + if err != nil { + return nil, nil, fmt.Errorf("POST %s: %w", url, err) + } + defer resp.Body.Close() + raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) + if resp.StatusCode != http.StatusOK { + return nil, raw, fmt.Errorf("registry returned %d: %s", resp.StatusCode, string(raw)) + } + + var out transferFactoryResponse + if err := json.Unmarshal(raw, &out); err != nil { + return nil, raw, fmt.Errorf("parse registry response: %w", err) + } + return &out, raw, nil +} + +// ---------- AnyValue encoding (identical to accept-via-interface.go) ---------- + +type anyValueJSON struct { + Tag string `json:"tag"` + Value json.RawMessage `json:"value"` +} + +func encodeAnyValue(raw json.RawMessage) (*lapiv2.Value, error) { + var av anyValueJSON + if err := json.Unmarshal(raw, &av); err != nil { + return nil, fmt.Errorf("parse AnyValue: %w", err) + } + var inner *lapiv2.Value + switch av.Tag { + case "AV_ContractId": + var s string + if err := json.Unmarshal(av.Value, &s); err != nil { + return nil, err + } + inner = values.ContractIDValue(s) + case "AV_Text": + var s string + if err := json.Unmarshal(av.Value, &s); err != nil { + return nil, err + } + inner = values.TextValue(s) + case "AV_Party": + var s string + if err := json.Unmarshal(av.Value, &s); err != nil { + return nil, err + } + inner = values.PartyValue(s) + case "AV_Bool": + var b bool + if err := json.Unmarshal(av.Value, &b); err != nil { + return nil, err + } + inner = &lapiv2.Value{Sum: &lapiv2.Value_Bool{Bool: b}} + case "AV_Int": + var n json.Number + if err := json.Unmarshal(av.Value, &n); err != nil { + return nil, err + } + i, err := n.Int64() + if err != nil { + return nil, fmt.Errorf("AV_Int: %w", err) + } + inner = &lapiv2.Value{Sum: &lapiv2.Value_Int64{Int64: i}} + case "AV_Decimal": + var s string + if err := json.Unmarshal(av.Value, &s); err != nil { + return nil, err + } + inner = values.NumericValue(s) + case "AV_List": + var items []json.RawMessage + if err := json.Unmarshal(av.Value, &items); err != nil { + return nil, err + } + elems := make([]*lapiv2.Value, 0, len(items)) + for _, it := range items { + v, err := encodeAnyValue(it) + if err != nil { + return nil, err + } + elems = append(elems, v) + } + inner = values.ListValue(elems) + default: + return nil, fmt.Errorf("unsupported AnyValue tag: %s (raw=%s)", av.Tag, string(raw)) + } + return &lapiv2.Value{ + Sum: &lapiv2.Value_Variant{ + Variant: &lapiv2.Variant{ + Constructor: av.Tag, + Value: inner, + }, + }, + }, nil +} + +func encodeChoiceContextRecord(vals map[string]json.RawMessage) (*lapiv2.Value, error) { + keys := make([]string, 0, len(vals)) + for k := range vals { + keys = append(keys, k) + } + sort.Strings(keys) + entries := make([]*lapiv2.TextMap_Entry, 0, len(vals)) + for _, k := range keys { + v, err := encodeAnyValue(vals[k]) + if err != nil { + return nil, fmt.Errorf("encode key %q: %w", k, err) + } + entries = append(entries, &lapiv2.TextMap_Entry{Key: k, Value: v}) + } + return &lapiv2.Value{ + Sum: &lapiv2.Value_Record{ + Record: &lapiv2.Record{ + Fields: []*lapiv2.RecordField{ + { + Label: "values", + Value: &lapiv2.Value{Sum: &lapiv2.Value_TextMap{TextMap: &lapiv2.TextMap{Entries: entries}}}, + }, + }, + }, + }, + }, nil +} + +func buildExtraArgsValue(ctxValue *lapiv2.Value) *lapiv2.Value { + return &lapiv2.Value{ + Sum: &lapiv2.Value_Record{ + Record: &lapiv2.Record{ + Fields: []*lapiv2.RecordField{ + {Label: "context", Value: ctxValue}, + {Label: "meta", Value: values.EmptyMetadata()}, + }, + }, + }, + } +} + +// ---------- DisclosedContracts ---------- + +func parseTemplateID(raw json.RawMessage) (*lapiv2.Identifier, error) { + if len(raw) == 0 { + return nil, nil + } + var s string + if err := json.Unmarshal(raw, &s); err == nil && s != "" { + parts := strings.SplitN(s, ":", 3) + if len(parts) != 3 { + return nil, fmt.Errorf("templateId %q not in pkg:module:entity form", s) + } + return &lapiv2.Identifier{PackageId: parts[0], ModuleName: parts[1], EntityName: parts[2]}, nil + } + var obj struct { + PackageID string `json:"packageId"` + ModuleName string `json:"moduleName"` + EntityName string `json:"entityName"` + } + if err := json.Unmarshal(raw, &obj); err != nil { + return nil, fmt.Errorf("parse templateId: %w", err) + } + return &lapiv2.Identifier{PackageId: obj.PackageID, ModuleName: obj.ModuleName, EntityName: obj.EntityName}, nil +} + +func buildDisclosedContracts(rcs []registryDisclosedContract, fallbackDomain string) ([]*lapiv2.DisclosedContract, error) { + out := make([]*lapiv2.DisclosedContract, 0, len(rcs)) + for _, c := range rcs { + blob, err := base64.StdEncoding.DecodeString(c.CreatedEventBlob) + if err != nil { + return nil, fmt.Errorf("decode blob for %s: %w", c.ContractID, err) + } + tid, err := parseTemplateID(c.TemplateID) + if err != nil { + return nil, err + } + domain := c.SynchronizerID + if domain == "" { + domain = fallbackDomain + } + out = append(out, &lapiv2.DisclosedContract{ + TemplateId: tid, + ContractId: c.ContractID, + CreatedEventBlob: blob, + SynchronizerId: domain, + }) + } + return out, nil +} + +// ---------- Submit ---------- + +func submitTransfer( + ctx context.Context, c *canton.Client, cfg *config.APIServer, + sender string, kp *keys.CantonKeyPair, + factoryCID, receiver, amount, instrumentID string, + inputHoldingCIDs []string, executeBefore time.Time, + extraArgs *lapiv2.Value, disclosed []*lapiv2.DisclosedContract, +) (string, error) { + // Backdate requestedAt by 5s to dodge ledger-time skew (matches SDK behavior). + now := time.Now().UTC().Add(-5 * time.Second) + + holdingCidValues := make([]*lapiv2.Value, len(inputHoldingCIDs)) + for i, cid := range inputHoldingCIDs { + holdingCidValues[i] = values.ContractIDValue(cid) + } + + transferRec := &lapiv2.Value{ + Sum: &lapiv2.Value_Record{ + Record: &lapiv2.Record{ + Fields: []*lapiv2.RecordField{ + {Label: "sender", Value: values.PartyValue(sender)}, + {Label: "receiver", Value: values.PartyValue(receiver)}, + {Label: "amount", Value: values.NumericValue(amount)}, + {Label: "instrumentId", Value: values.EncodeInstrumentId(*registrar, instrumentID)}, + {Label: "requestedAt", Value: values.TimestampValue(now)}, + {Label: "executeBefore", Value: values.TimestampValue(executeBefore)}, + {Label: "inputHoldingCids", Value: values.ListValue(holdingCidValues)}, + {Label: "meta", Value: values.EmptyMetadata()}, + }, + }, + }, + } + + cmd := &lapiv2.Command{ + Command: &lapiv2.Command_Exercise{ + Exercise: &lapiv2.ExerciseCommand{ + TemplateId: &lapiv2.Identifier{ + PackageId: transferFactoryPackageID, + ModuleName: transferFactoryModule, + EntityName: transferFactoryEntity, + }, + ContractId: factoryCID, + Choice: transferChoice, + ChoiceArgument: &lapiv2.Value{ + Sum: &lapiv2.Value_Record{ + Record: &lapiv2.Record{ + Fields: []*lapiv2.RecordField{ + {Label: "expectedAdmin", Value: values.PartyValue(*registrar)}, + {Label: "transfer", Value: transferRec}, + {Label: "extraArgs", Value: extraArgs}, + }, + }, + }, + }, + }, + }, + } + + authCtx := c.Ledger.AuthContext(ctx) + prepResp, err := c.Ledger.Interactive().PrepareSubmission(authCtx, &interactivev2.PrepareSubmissionRequest{ + UserId: cfg.Canton.Token.UserID, + CommandId: uuid.NewString(), + Commands: []*lapiv2.Command{cmd}, + ActAs: []string{sender}, + SynchronizerId: cfg.Canton.DomainID, + DisclosedContracts: disclosed, + }) + if err != nil { + return "", fmt.Errorf("PrepareSubmission: %w", err) + } + derSig, err := kp.SignDER(prepResp.PreparedTransactionHash) + if err != nil { + return "", fmt.Errorf("sign: %w", err) + } + fingerprint, _ := kp.Fingerprint() + + execResp, err := c.Ledger.Interactive().ExecuteSubmissionAndWait(authCtx, &interactivev2.ExecuteSubmissionAndWaitRequest{ + PreparedTransaction: prepResp.PreparedTransaction, + PartySignatures: &interactivev2.PartySignatures{ + Signatures: []*interactivev2.SinglePartySignatures{ + { + Party: sender, + Signatures: []*lapiv2.Signature{ + { + Format: lapiv2.SignatureFormat_SIGNATURE_FORMAT_DER, + Signature: derSig, + SignedBy: fingerprint, + SigningAlgorithmSpec: lapiv2.SigningAlgorithmSpec_SIGNING_ALGORITHM_SPEC_EC_DSA_SHA_256, + }, + }, + }, + }, + }, + SubmissionId: uuid.NewString(), + UserId: cfg.Canton.Token.UserID, + HashingSchemeVersion: prepResp.HashingSchemeVersion, + }) + if err != nil { + return "", fmt.Errorf("ExecuteSubmission: %w", err) + } + _ = execResp + // ExecuteSubmissionAndWait doesn't return the created contract id directly; + // the caller can find the resulting TransferOffer via ACS query as the receiver. + return "", nil +} + +// ---------- Helpers ---------- + +func indentJSON(raw []byte) string { + var buf bytes.Buffer + if err := json.Indent(&buf, raw, " ", " "); err != nil { + return string(raw) + } + return buf.String() +} + +func humanizeDuration(d time.Duration) string { + if d <= 0 { + return "in the past!" + } + days := int(d.Hours() / 24) + if days > 365 { + return fmt.Sprintf("~%d years from now", days/365) + } + if days > 0 { + return fmt.Sprintf("%d days from now", days) + } + return d.Round(time.Second).String() + " from now" +} + +func loadCredentials(path string) (map[string]string, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + out := map[string]string{} + s := bufio.NewScanner(f) + for s.Scan() { + line := strings.TrimSpace(s.Text()) + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "[") { + continue + } + idx := strings.Index(line, "=") + if idx < 0 { + continue + } + out[strings.TrimSpace(line[:idx])] = strings.TrimSpace(line[idx+1:]) + } + return out, s.Err() +} + +func fatalf(format string, args ...any) { + fmt.Fprintf(os.Stderr, "FATAL: "+format+"\n", args...) + os.Exit(1) +} diff --git a/scripts/testing/send-usdcx-transfer.go b/scripts/testing/send-usdcx-transfer.go new file mode 100644 index 0000000..b221ba2 --- /dev/null +++ b/scripts/testing/send-usdcx-transfer.go @@ -0,0 +1,161 @@ +//go:build ignore + +// send-usdcx-transfer.go — Send USDCx from a standalone external party. +// +// Loads party credentials (the same file format as accept-usdcx-transfer.go), +// wires a Canton SDK Client with a custom KeyResolver that returns the +// credentials' secp256k1 key, then exercises TransferFactory_Transfer for the +// configured external token via the api-server's TransferByPartyID path. +// +// Usage: +// go run scripts/testing/send-usdcx-transfer.go \ +// -config config.api-server.devnet-test.yaml \ +// -creds ./party-credentials.txt \ +// -to "user_xxx::1220..." \ +// -amount "1" \ +// [-symbol USDCx] [-dry-run] +// +// The config must have token.external_tokens populated for the USDCx instrument +// admin → registry URL (the registry is consulted to discover TransferFactory). + +package main + +import ( + "bufio" + "context" + "encoding/hex" + "flag" + "fmt" + "os" + "strings" + "time" + + canton "github.com/chainsafe/canton-middleware/pkg/cantonsdk/client" + "github.com/chainsafe/canton-middleware/pkg/cantonsdk/token" + "github.com/chainsafe/canton-middleware/pkg/config" + "github.com/chainsafe/canton-middleware/pkg/keys" + + "github.com/google/uuid" + "go.uber.org/zap" +) + +var ( + configPath = flag.String("config", "config.api-server.devnet-test.yaml", "Config file") + credsPath = flag.String("creds", "./party-credentials.txt", "Party credentials file") + toParty = flag.String("to", "", "Receiver party ID (required)") + amount = flag.String("amount", "1", "Amount to send (decimal string)") + symbol = flag.String("symbol", "USDCx", "Token symbol") + dryRun = flag.Bool("dry-run", false, "Print plan, list holdings, don't submit") +) + +func main() { + flag.Parse() + if *toParty == "" { + fatalf("-to is required") + } + + creds, err := loadCredentials(*credsPath) + if err != nil { + fatalf("load credentials: %v", err) + } + fromParty := creds["party_id"] + if fromParty == "" { + fatalf("party_id missing from credentials") + } + + privBytes, err := hex.DecodeString(creds["private_key_hex"]) + if err != nil { + fatalf("decode private key: %v", err) + } + kp, err := keys.CantonKeyPairFromPrivateKey(privBytes) + if err != nil { + fatalf("reconstruct keypair: %v", err) + } + fp, _ := kp.Fingerprint() + fmt.Printf(">>> sender party : %s\n", fromParty) + fmt.Printf(">>> sender fingerprint : %s\n", fp) + fmt.Printf(">>> receiver party : %s\n", *toParty) + fmt.Printf(">>> amount : %s %s\n", *amount, *symbol) + + cfg, err := config.LoadAPIServer(*configPath) + if err != nil { + fatalf("load config: %v", err) + } + + logger, _ := zap.NewDevelopment() + ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) + defer cancel() + + // KeyResolver returns our keypair whenever the SDK asks for the sender's signer. + keyResolver := func(partyID string) (token.Signer, error) { + if partyID != fromParty { + return nil, fmt.Errorf("no key for party %s (only %s available)", partyID, fromParty) + } + return kp, nil + } + + cantonClient, err := canton.New(ctx, cfg.Canton, + canton.WithLogger(logger), + canton.WithKeyResolver(keyResolver), + ) + if err != nil { + fatalf("canton client: %v", err) + } + defer func() { _ = cantonClient.Close() }() + + // Diagnostic: list current holdings so we can confirm the sender has at least `amount`. + holdings, err := cantonClient.Token.GetHoldingsByParty(ctx, fromParty, *symbol) + if err != nil { + fatalf("get holdings: %v", err) + } + fmt.Printf("\n>>> sender holdings for %s: %d contract(s)\n", *symbol, len(holdings)) + for i, h := range holdings { + fmt.Printf(" #%d amount=%s cid=%s admin=%s\n", i+1, h.Amount, h.ContractID, h.InstrumentAdmin) + } + if len(holdings) == 0 { + fatalf("no %s holdings visible for party %s — cannot send", *symbol, fromParty) + } + + if *dryRun { + fmt.Println("\n[dry-run] would call TransferByPartyID — skipping submission") + return + } + + idempotencyKey := fmt.Sprintf("send-usdcx-%s", uuid.NewString()) + fmt.Printf("\n>>> submitting TransferFactory_Transfer (commandId=%s)…\n", idempotencyKey) + t0 := time.Now() + if err := cantonClient.Token.TransferByPartyID(ctx, idempotencyKey, fromParty, *toParty, *amount, *symbol); err != nil { + fatalf("TransferByPartyID: %v", err) + } + fmt.Printf(">>> SUCCESS in %s\n", time.Since(t0).Round(time.Millisecond)) +} + +// loadCredentials reads the key=value text file produced by allocate-standalone-party.go. +func loadCredentials(path string) (map[string]string, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + out := map[string]string{} + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "[") { + continue + } + idx := strings.Index(line, "=") + if idx < 0 { + continue + } + k := strings.TrimSpace(line[:idx]) + v := strings.TrimSpace(line[idx+1:]) + out[k] = v + } + return out, scanner.Err() +} + +func fatalf(format string, args ...any) { + fmt.Fprintf(os.Stderr, "FATAL: "+format+"\n", args...) + os.Exit(1) +} diff --git a/scripts/testing/withdraw-via-interface.go b/scripts/testing/withdraw-via-interface.go new file mode 100644 index 0000000..68f3645 --- /dev/null +++ b/scripts/testing/withdraw-via-interface.go @@ -0,0 +1,561 @@ +//go:build ignore + +// withdraw-via-interface.go — Withdraw a USDCx TransferOffer the sender (us) +// previously created, by exercising the Splice CIP-56 TransferInstruction +// interface choice TransferInstruction_Withdraw. +// +// Mirrors accept-via-interface.go, but: +// - the registrar endpoint is .../choice-contexts/withdraw +// - the Daml choice is TransferInstruction_Withdraw +// - the actor (ActAs) is the sender, reclaiming their locked input holding +// +// Use this for stale offers whose executeBefore has passed and the receiver +// never accepted. +// +// Usage: +// go run scripts/testing/withdraw-via-interface.go \ +// -config config.api-server.devnet-test.yaml \ +// -creds ./party-credentials.txt \ +// [-cid ] # if omitted, withdraws every visible offer + +package main + +import ( + "bufio" + "bytes" + "context" + "encoding/base64" + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "os" + "sort" + "strings" + "time" + + canton "github.com/chainsafe/canton-middleware/pkg/cantonsdk/client" + lapiv2 "github.com/chainsafe/canton-middleware/pkg/cantonsdk/lapi/v2" + interactivev2 "github.com/chainsafe/canton-middleware/pkg/cantonsdk/lapi/v2/interactive" + "github.com/chainsafe/canton-middleware/pkg/cantonsdk/values" + "github.com/chainsafe/canton-middleware/pkg/config" + "github.com/chainsafe/canton-middleware/pkg/keys" + + "github.com/google/uuid" + "go.uber.org/zap" +) + +const ( + // Concrete template (for ACS lookup of pending offers) + transferOfferPackageID = "#utility-registry-app-v0" + transferOfferModule = "Utility.Registry.App.V0.Model.Transfer" + transferOfferEntity = "TransferOffer" + + // Splice TransferInstruction interface (for the Withdraw choice) + transferInstrPackageID = "#splice-api-token-transfer-instruction-v1" + transferInstrModule = "Splice.Api.Token.TransferInstructionV1" + transferInstrEntity = "TransferInstruction" + withdrawChoice = "TransferInstruction_Withdraw" +) + +var ( + configPath = flag.String("config", "config.api-server.devnet-test.yaml", "Config") + credsPath = flag.String("creds", "./party-credentials.txt", "Credentials file") + registryHost = flag.String("registry-host", "https://api.utilities.digitalasset-dev.com", "Registrar API base URL") + registrar = flag.String("registrar", "decentralized-usdc-interchain-rep::1220d420ba8f168d63157f610e6593dca072bbd79ff90a830efc345ed4348a816de7", "Registrar party ID") + onlyCID = flag.String("cid", "", "Optional: withdraw only this specific TransferOffer CID. If empty, withdraws every visible offer.") + dryRun = flag.Bool("dry-run", false, "Print the registry response and command without submitting") + debug = flag.Bool("debug", true, "Print registry response JSON") +) + +func main() { + flag.Parse() + + creds, err := loadCredentials(*credsPath) + if err != nil { + fatalf("load credentials: %v", err) + } + holder := creds["party_id"] + privBytes, err := hex.DecodeString(creds["private_key_hex"]) + if err != nil { + fatalf("decode private key: %v", err) + } + kp, err := keys.CantonKeyPairFromPrivateKey(privBytes) + if err != nil { + fatalf("reconstruct keypair: %v", err) + } + + cfg, err := config.LoadAPIServer(*configPath) + if err != nil { + fatalf("load config: %v", err) + } + logger, _ := zap.NewDevelopment() + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + + cantonClient, err := canton.New(ctx, cfg.Canton, canton.WithLogger(logger)) + if err != nil { + fatalf("canton client: %v", err) + } + defer func() { _ = cantonClient.Close() }() + + offers, err := findOffers(ctx, cantonClient, holder) + if err != nil { + fatalf("find offers: %v", err) + } + if *onlyCID != "" { + filtered := offers[:0] + for _, c := range offers { + if c == *onlyCID { + filtered = append(filtered, c) + } + } + offers = filtered + if len(offers) == 0 { + fatalf("CID %s not found among visible TransferOffers for %s", *onlyCID, holder) + } + } + fmt.Printf(">>> Found %d TransferOffer(s) to withdraw for %s\n", len(offers), holder) + if len(offers) == 0 { + return + } + + httpClient := &http.Client{Timeout: 30 * time.Second} + + for i, cid := range offers { + fmt.Printf("\n--- Offer #%d ---\n CID: %s\n", i+1, cid) + + regResp, raw, err := fetchWithdrawContext(ctx, httpClient, *registryHost, *registrar, cid) + if err != nil { + fmt.Printf(" ERROR fetching choice-context: %v\n", err) + continue + } + if *debug { + fmt.Printf(" --- registry response ---\n%s\n -------------------------\n", indentJSON(raw)) + } + + ctxValue, err := encodeChoiceContextRecord(regResp.ChoiceContextData.Values) + if err != nil { + fmt.Printf(" ERROR encoding choice context: %v\n", err) + continue + } + extraArgs := buildExtraArgsValue(ctxValue) + + disclosed, err := buildDisclosedContracts(regResp.DisclosedContracts, cfg.Canton.DomainID) + if err != nil { + fmt.Printf(" ERROR building disclosed contracts: %v\n", err) + continue + } + fmt.Printf(" Disclosed contracts: %d\n", len(disclosed)) + + if *dryRun { + fmt.Println(" [dry-run] would exercise TransferInstruction_Withdraw") + continue + } + + if err := withdrawViaInterface(ctx, cantonClient, cfg, holder, kp, cid, extraArgs, disclosed); err != nil { + fmt.Printf(" ERROR: %v\n", err) + continue + } + fmt.Println(" WITHDRAWN") + } +} + +func findOffers(ctx context.Context, c *canton.Client, partyID string) ([]string, error) { + end, err := c.Ledger.GetLedgerEnd(ctx) + if err != nil { + return nil, err + } + if end == 0 { + return nil, nil + } + tid := &lapiv2.Identifier{ + PackageId: transferOfferPackageID, ModuleName: transferOfferModule, EntityName: transferOfferEntity, + } + events, err := c.Ledger.GetActiveContractsByTemplate(ctx, end, []string{partyID}, tid) + if err != nil { + return nil, err + } + var out []string + for _, ce := range events { + out = append(out, ce.ContractId) + } + return out, nil +} + +// ---------- Registry ---------- + +type acceptContextResponse struct { + ChoiceContextData struct { + Values map[string]json.RawMessage `json:"values"` + } `json:"choiceContextData"` + DisclosedContracts []registryDisclosedContract `json:"disclosedContracts"` +} + +type registryDisclosedContract struct { + ContractID string `json:"contractId"` + CreatedEventBlob string `json:"createdEventBlob"` + TemplateID json.RawMessage `json:"templateId"` + SynchronizerID string `json:"synchronizerId"` +} + +func fetchWithdrawContext(ctx context.Context, hc *http.Client, host, registrarParty, instructionCID string) (*acceptContextResponse, []byte, error) { + url := fmt.Sprintf( + "%s/api/token-standard/v0/registrars/%s/registry/transfer-instruction/v1/%s/choice-contexts/withdraw", + strings.TrimRight(host, "/"), registrarParty, instructionCID, + ) + body := []byte(`{"meta":{},"excludeDebugFields":false}`) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + resp, err := hc.Do(req) + if err != nil { + return nil, nil, fmt.Errorf("registry POST %s: %w", url, err) + } + defer resp.Body.Close() + + raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) + if resp.StatusCode != http.StatusOK { + return nil, raw, fmt.Errorf("registry returned %d: %s", resp.StatusCode, string(raw)) + } + + var out acceptContextResponse + if err := json.Unmarshal(raw, &out); err != nil { + return nil, raw, fmt.Errorf("parse registry response: %w", err) + } + return &out, raw, nil +} + +// ---------- AnyValue encoding ---------- + +// anyValueJSON is the {"tag": "...", "value": ...} envelope used by Daml-LF JSON +// for the AnyValue ADT. +type anyValueJSON struct { + Tag string `json:"tag"` + Value json.RawMessage `json:"value"` +} + +func encodeAnyValue(raw json.RawMessage) (*lapiv2.Value, error) { + var av anyValueJSON + if err := json.Unmarshal(raw, &av); err != nil { + return nil, fmt.Errorf("parse AnyValue: %w", err) + } + var inner *lapiv2.Value + switch av.Tag { + case "AV_ContractId": + var s string + if err := json.Unmarshal(av.Value, &s); err != nil { + return nil, err + } + inner = values.ContractIDValue(s) + case "AV_Text": + var s string + if err := json.Unmarshal(av.Value, &s); err != nil { + return nil, err + } + inner = values.TextValue(s) + case "AV_Party": + var s string + if err := json.Unmarshal(av.Value, &s); err != nil { + return nil, err + } + inner = values.PartyValue(s) + case "AV_Bool": + var b bool + if err := json.Unmarshal(av.Value, &b); err != nil { + return nil, err + } + inner = &lapiv2.Value{Sum: &lapiv2.Value_Bool{Bool: b}} + case "AV_Int": + var n json.Number + if err := json.Unmarshal(av.Value, &n); err != nil { + return nil, err + } + i, err := n.Int64() + if err != nil { + return nil, fmt.Errorf("AV_Int: %w", err) + } + inner = &lapiv2.Value{Sum: &lapiv2.Value_Int64{Int64: i}} + case "AV_Decimal": + var s string + if err := json.Unmarshal(av.Value, &s); err != nil { + return nil, err + } + inner = values.NumericValue(s) + case "AV_List": + var items []json.RawMessage + if err := json.Unmarshal(av.Value, &items); err != nil { + return nil, err + } + elems := make([]*lapiv2.Value, 0, len(items)) + for _, it := range items { + v, err := encodeAnyValue(it) + if err != nil { + return nil, err + } + elems = append(elems, v) + } + inner = values.ListValue(elems) + case "AV_Map": + // Map is encoded as a list of (Text, AnyValue) tuples + var items []struct { + Key string `json:"_1"` + Value json.RawMessage `json:"_2"` + } + if err := json.Unmarshal(av.Value, &items); err != nil { + return nil, err + } + elems := make([]*lapiv2.Value, 0, len(items)) + for _, it := range items { + v, err := encodeAnyValue(it.Value) + if err != nil { + return nil, err + } + elems = append(elems, &lapiv2.Value{ + Sum: &lapiv2.Value_Record{ + Record: &lapiv2.Record{ + Fields: []*lapiv2.RecordField{ + {Label: "_1", Value: values.TextValue(it.Key)}, + {Label: "_2", Value: v}, + }, + }, + }, + }) + } + inner = values.ListValue(elems) + default: + return nil, fmt.Errorf("unsupported AnyValue tag: %s (raw=%s)", av.Tag, string(raw)) + } + return &lapiv2.Value{ + Sum: &lapiv2.Value_Variant{ + Variant: &lapiv2.Variant{ + Constructor: av.Tag, + Value: inner, + }, + }, + }, nil +} + +// encodeChoiceContextRecord builds the Splice ChoiceContext record: +// +// ChoiceContext { values: TextMap AnyValue } +func encodeChoiceContextRecord(vals map[string]json.RawMessage) (*lapiv2.Value, error) { + keys := make([]string, 0, len(vals)) + for k := range vals { + keys = append(keys, k) + } + sort.Strings(keys) + + entries := make([]*lapiv2.TextMap_Entry, 0, len(vals)) + for _, k := range keys { + v, err := encodeAnyValue(vals[k]) + if err != nil { + return nil, fmt.Errorf("encode key %q: %w", k, err) + } + entries = append(entries, &lapiv2.TextMap_Entry{Key: k, Value: v}) + } + return &lapiv2.Value{ + Sum: &lapiv2.Value_Record{ + Record: &lapiv2.Record{ + Fields: []*lapiv2.RecordField{ + { + Label: "values", + Value: &lapiv2.Value{ + Sum: &lapiv2.Value_TextMap{ + TextMap: &lapiv2.TextMap{Entries: entries}, + }, + }, + }, + }, + }, + }, + }, nil +} + +func buildExtraArgsValue(ctxValue *lapiv2.Value) *lapiv2.Value { + return &lapiv2.Value{ + Sum: &lapiv2.Value_Record{ + Record: &lapiv2.Record{ + Fields: []*lapiv2.RecordField{ + {Label: "context", Value: ctxValue}, + {Label: "meta", Value: values.EmptyMetadata()}, + }, + }, + }, + } +} + +// ---------- DisclosedContracts ---------- + +// templateID may be a string ":Module:Entity" or a {packageId, moduleName, entityName} object. +func parseTemplateID(raw json.RawMessage) (*lapiv2.Identifier, error) { + if len(raw) == 0 { + return nil, nil + } + var s string + if err := json.Unmarshal(raw, &s); err == nil && s != "" { + parts := strings.SplitN(s, ":", 3) + if len(parts) != 3 { + return nil, fmt.Errorf("templateId %q not in pkg:module:entity form", s) + } + return &lapiv2.Identifier{PackageId: parts[0], ModuleName: parts[1], EntityName: parts[2]}, nil + } + var obj struct { + PackageID string `json:"packageId"` + ModuleName string `json:"moduleName"` + EntityName string `json:"entityName"` + } + if err := json.Unmarshal(raw, &obj); err != nil { + return nil, fmt.Errorf("parse templateId: %w", err) + } + return &lapiv2.Identifier{PackageId: obj.PackageID, ModuleName: obj.ModuleName, EntityName: obj.EntityName}, nil +} + +func buildDisclosedContracts(rcs []registryDisclosedContract, fallbackDomain string) ([]*lapiv2.DisclosedContract, error) { + out := make([]*lapiv2.DisclosedContract, 0, len(rcs)) + for _, c := range rcs { + blob, err := base64.StdEncoding.DecodeString(c.CreatedEventBlob) + if err != nil { + return nil, fmt.Errorf("decode blob for %s: %w", c.ContractID, err) + } + tid, err := parseTemplateID(c.TemplateID) + if err != nil { + return nil, err + } + domain := c.SynchronizerID + if domain == "" { + domain = fallbackDomain + } + out = append(out, &lapiv2.DisclosedContract{ + TemplateId: tid, + ContractId: c.ContractID, + CreatedEventBlob: blob, + SynchronizerId: domain, + }) + } + return out, nil +} + +// ---------- Submit ---------- + +func withdrawViaInterface( + ctx context.Context, + c *canton.Client, + cfg *config.APIServer, + holder string, + kp *keys.CantonKeyPair, + contractID string, + extraArgs *lapiv2.Value, + disclosed []*lapiv2.DisclosedContract, +) error { + cmd := &lapiv2.Command{ + Command: &lapiv2.Command_Exercise{ + Exercise: &lapiv2.ExerciseCommand{ + TemplateId: &lapiv2.Identifier{ + PackageId: transferInstrPackageID, + ModuleName: transferInstrModule, + EntityName: transferInstrEntity, + }, + ContractId: contractID, + Choice: withdrawChoice, + ChoiceArgument: &lapiv2.Value{ + Sum: &lapiv2.Value_Record{ + Record: &lapiv2.Record{ + Fields: []*lapiv2.RecordField{ + {Label: "extraArgs", Value: extraArgs}, + }, + }, + }, + }, + }, + }, + } + + authCtx := c.Ledger.AuthContext(ctx) + prepResp, err := c.Ledger.Interactive().PrepareSubmission(authCtx, &interactivev2.PrepareSubmissionRequest{ + UserId: cfg.Canton.Token.UserID, + CommandId: uuid.NewString(), + Commands: []*lapiv2.Command{cmd}, + ActAs: []string{holder}, + SynchronizerId: cfg.Canton.DomainID, + DisclosedContracts: disclosed, + }) + if err != nil { + return fmt.Errorf("PrepareSubmission: %w", err) + } + + derSig, err := kp.SignDER(prepResp.PreparedTransactionHash) + if err != nil { + return fmt.Errorf("sign: %w", err) + } + fingerprint, _ := kp.Fingerprint() + + if _, err := c.Ledger.Interactive().ExecuteSubmissionAndWait(authCtx, &interactivev2.ExecuteSubmissionAndWaitRequest{ + PreparedTransaction: prepResp.PreparedTransaction, + PartySignatures: &interactivev2.PartySignatures{ + Signatures: []*interactivev2.SinglePartySignatures{ + { + Party: holder, + Signatures: []*lapiv2.Signature{ + { + Format: lapiv2.SignatureFormat_SIGNATURE_FORMAT_DER, + Signature: derSig, + SignedBy: fingerprint, + SigningAlgorithmSpec: lapiv2.SigningAlgorithmSpec_SIGNING_ALGORITHM_SPEC_EC_DSA_SHA_256, + }, + }, + }, + }, + }, + SubmissionId: uuid.NewString(), + UserId: cfg.Canton.Token.UserID, + HashingSchemeVersion: prepResp.HashingSchemeVersion, + }); err != nil { + return fmt.Errorf("ExecuteSubmission: %w", err) + } + return nil +} + +// ---------- Helpers ---------- + +func indentJSON(raw []byte) string { + var buf bytes.Buffer + if err := json.Indent(&buf, raw, " ", " "); err != nil { + return string(raw) + } + return buf.String() +} + +func loadCredentials(path string) (map[string]string, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + out := map[string]string{} + s := bufio.NewScanner(f) + for s.Scan() { + line := strings.TrimSpace(s.Text()) + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "[") { + continue + } + idx := strings.Index(line, "=") + if idx < 0 { + continue + } + out[strings.TrimSpace(line[:idx])] = strings.TrimSpace(line[idx+1:]) + } + return out, s.Err() +} + +func fatalf(format string, args ...any) { + fmt.Fprintf(os.Stderr, "FATAL: "+format+"\n", args...) + os.Exit(1) +} diff --git a/scripts/utils/check-anvil-state.sh b/scripts/utils/check-anvil-state.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/metamask-info-devnet.sh b/scripts/utils/metamask-info-devnet.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/metamask-info.sh b/scripts/utils/metamask-info.sh old mode 100644 new mode 100755 diff --git a/scripts/utils/query-canton-holdings.sh b/scripts/utils/query-canton-holdings.sh old mode 100644 new mode 100755 From 0a11e10ca3ad653b48861116aa5d7193e2c6718f Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Wed, 27 May 2026 10:07:38 -0600 Subject: [PATCH 07/10] licensing: preserve file mode in SPDX-header helper script `mv tmp file` is rename(2); the target inode adopts the source file's mode (mktemp default 0600 -> 100644 in git). That silently stripped the executable bit on shell scripts during the initial sweep. Switching to `cat tmp >file` keeps the existing target file (and its mode) intact, just overwriting its contents. Re-running the script on an already-headered tree is still a no-op via the SPDX-detection guard. --- scripts/dev/add-spdx-headers.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/dev/add-spdx-headers.sh b/scripts/dev/add-spdx-headers.sh index 2a4f3e0..a992859 100755 --- a/scripts/dev/add-spdx-headers.sh +++ b/scripts/dev/add-spdx-headers.sh @@ -32,7 +32,10 @@ should_skip() { return 1 } -# Prepend a header line to the top of the file. +# Prepend a header line to the top of the file. Preserves the target's +# file mode, since `mv tmp file` adopts the source mktemp's mode (typically +# 0600 normalized to 100644 in git) and would silently strip the executable +# bit on shell scripts. prepend_top() { local file="$1" local header="$2" @@ -42,10 +45,12 @@ prepend_top() { printf '%s\n\n' "$header" cat "$file" } >"$tmp" - mv "$tmp" "$file" + cat "$tmp" >"$file" + rm -f "$tmp" } -# Insert a header line immediately after the shebang on line 1. +# Insert a header line immediately after the shebang on line 1. Same +# mode-preservation reasoning as prepend_top. insert_after_shebang() { local file="$1" local header="$2" @@ -56,7 +61,8 @@ insert_after_shebang() { printf '%s\n' "$header" tail -n +2 "$file" } >"$tmp" - mv "$tmp" "$file" + cat "$tmp" >"$file" + rm -f "$tmp" } # Process Go files. From 821f427a1596cd76d5bd0995c4f5a5479b6352bf Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Wed, 27 May 2026 11:11:11 -0600 Subject: [PATCH 08/10] licensing: drop one-time SPDX-header helper script The `scripts/dev/add-spdx-headers.sh` helper was used once to seed the SPDX headers across the tree and isn't needed as a checked-in artifact; SPDX coverage is now maintained file-by-file as code is added. --- scripts/dev/add-spdx-headers.sh | 138 -------------------------------- 1 file changed, 138 deletions(-) delete mode 100755 scripts/dev/add-spdx-headers.sh diff --git a/scripts/dev/add-spdx-headers.sh b/scripts/dev/add-spdx-headers.sh deleted file mode 100755 index a992859..0000000 --- a/scripts/dev/add-spdx-headers.sh +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: Apache-2.0 -# -# Idempotently add `SPDX-License-Identifier: Apache-2.0` headers to first-party -# source files. Re-running this script is safe; files that already carry an -# SPDX header in their first 5 lines are skipped, and files that look -# generated are skipped entirely. -# -# Usage: bash scripts/dev/add-spdx-headers.sh -# -# Run from the repository root. - -set -euo pipefail - -SPDX_LINE_SLASH="// SPDX-License-Identifier: Apache-2.0" -SPDX_LINE_HASH="# SPDX-License-Identifier: Apache-2.0" - -# Returns 0 (true) if the file already has an SPDX header in its first 5 lines, -# OR if it looks like generated code we should skip. -should_skip() { - local file="$1" - local first_five - first_five=$(head -n 5 "$file" 2>/dev/null || true) - - case "$first_five" in - *"SPDX-License-Identifier"*) return 0 ;; - *"Code generated"*) return 0 ;; - *"DO NOT EDIT"*) return 0 ;; - *"protoc-gen-go"*) return 0 ;; - *"abigen"*) return 0 ;; - esac - return 1 -} - -# Prepend a header line to the top of the file. Preserves the target's -# file mode, since `mv tmp file` adopts the source mktemp's mode (typically -# 0600 normalized to 100644 in git) and would silently strip the executable -# bit on shell scripts. -prepend_top() { - local file="$1" - local header="$2" - local tmp - tmp=$(mktemp) - { - printf '%s\n\n' "$header" - cat "$file" - } >"$tmp" - cat "$tmp" >"$file" - rm -f "$tmp" -} - -# Insert a header line immediately after the shebang on line 1. Same -# mode-preservation reasoning as prepend_top. -insert_after_shebang() { - local file="$1" - local header="$2" - local tmp - tmp=$(mktemp) - { - head -n 1 "$file" - printf '%s\n' "$header" - tail -n +2 "$file" - } >"$tmp" - cat "$tmp" >"$file" - rm -f "$tmp" -} - -# Process Go files. -process_go() { - local count=0 skipped=0 - while IFS= read -r -d '' file; do - if should_skip "$file"; then - skipped=$((skipped + 1)) - continue - fi - prepend_top "$file" "$SPDX_LINE_SLASH" - count=$((count + 1)) - done < <(find cmd pkg -type f -name '*.go' \ - -not -path '*/lapi/v2/*' \ - -not -path '*/ethereum/contracts/*' \ - -not -name '*.pb.go' \ - -print0) - printf 'Go: added headers to %d files (skipped %d)\n' "$count" "$skipped" -} - -# Process shell scripts. -process_shell() { - local count=0 skipped=0 - while IFS= read -r -d '' file; do - if should_skip "$file"; then - skipped=$((skipped + 1)) - continue - fi - if head -n 1 "$file" | grep -q '^#!'; then - insert_after_shebang "$file" "$SPDX_LINE_HASH" - else - prepend_top "$file" "$SPDX_LINE_HASH" - fi - count=$((count + 1)) - done < <(find scripts -type f -name '*.sh' -print0) - printf 'Shell: added headers to %d files (skipped %d)\n' "$count" "$skipped" -} - -# Process Dockerfiles. Targets explicit list rather than a glob to avoid -# accidentally touching vendor/test Dockerfiles outside our scope. -process_dockerfiles() { - local count=0 skipped=0 - local dockerfiles=( - "Dockerfile.local" - "cmd/api-server/Dockerfile" - "cmd/indexer/Dockerfile" - "cmd/relayer/Dockerfile" - ) - for file in "${dockerfiles[@]}"; do - if [[ ! -f "$file" ]]; then - continue - fi - if should_skip "$file"; then - skipped=$((skipped + 1)) - continue - fi - prepend_top "$file" "$SPDX_LINE_HASH" - count=$((count + 1)) - done - printf 'Dockerfile: added headers to %d files (skipped %d)\n' "$count" "$skipped" -} - -main() { - if [[ ! -f go.mod ]]; then - echo "error: run from repository root (go.mod not found)" >&2 - exit 1 - fi - process_go - process_shell - process_dockerfiles -} - -main "$@" From e88b89fa3550a0432c4e53d0b7f51f5a7b9a89c7 Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Wed, 27 May 2026 11:15:27 -0600 Subject: [PATCH 09/10] Revert "licensing: drop one-time SPDX-header helper script" This reverts commit 821f427a1596cd76d5bd0995c4f5a5479b6352bf. --- scripts/dev/add-spdx-headers.sh | 138 ++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100755 scripts/dev/add-spdx-headers.sh diff --git a/scripts/dev/add-spdx-headers.sh b/scripts/dev/add-spdx-headers.sh new file mode 100755 index 0000000..a992859 --- /dev/null +++ b/scripts/dev/add-spdx-headers.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# +# Idempotently add `SPDX-License-Identifier: Apache-2.0` headers to first-party +# source files. Re-running this script is safe; files that already carry an +# SPDX header in their first 5 lines are skipped, and files that look +# generated are skipped entirely. +# +# Usage: bash scripts/dev/add-spdx-headers.sh +# +# Run from the repository root. + +set -euo pipefail + +SPDX_LINE_SLASH="// SPDX-License-Identifier: Apache-2.0" +SPDX_LINE_HASH="# SPDX-License-Identifier: Apache-2.0" + +# Returns 0 (true) if the file already has an SPDX header in its first 5 lines, +# OR if it looks like generated code we should skip. +should_skip() { + local file="$1" + local first_five + first_five=$(head -n 5 "$file" 2>/dev/null || true) + + case "$first_five" in + *"SPDX-License-Identifier"*) return 0 ;; + *"Code generated"*) return 0 ;; + *"DO NOT EDIT"*) return 0 ;; + *"protoc-gen-go"*) return 0 ;; + *"abigen"*) return 0 ;; + esac + return 1 +} + +# Prepend a header line to the top of the file. Preserves the target's +# file mode, since `mv tmp file` adopts the source mktemp's mode (typically +# 0600 normalized to 100644 in git) and would silently strip the executable +# bit on shell scripts. +prepend_top() { + local file="$1" + local header="$2" + local tmp + tmp=$(mktemp) + { + printf '%s\n\n' "$header" + cat "$file" + } >"$tmp" + cat "$tmp" >"$file" + rm -f "$tmp" +} + +# Insert a header line immediately after the shebang on line 1. Same +# mode-preservation reasoning as prepend_top. +insert_after_shebang() { + local file="$1" + local header="$2" + local tmp + tmp=$(mktemp) + { + head -n 1 "$file" + printf '%s\n' "$header" + tail -n +2 "$file" + } >"$tmp" + cat "$tmp" >"$file" + rm -f "$tmp" +} + +# Process Go files. +process_go() { + local count=0 skipped=0 + while IFS= read -r -d '' file; do + if should_skip "$file"; then + skipped=$((skipped + 1)) + continue + fi + prepend_top "$file" "$SPDX_LINE_SLASH" + count=$((count + 1)) + done < <(find cmd pkg -type f -name '*.go' \ + -not -path '*/lapi/v2/*' \ + -not -path '*/ethereum/contracts/*' \ + -not -name '*.pb.go' \ + -print0) + printf 'Go: added headers to %d files (skipped %d)\n' "$count" "$skipped" +} + +# Process shell scripts. +process_shell() { + local count=0 skipped=0 + while IFS= read -r -d '' file; do + if should_skip "$file"; then + skipped=$((skipped + 1)) + continue + fi + if head -n 1 "$file" | grep -q '^#!'; then + insert_after_shebang "$file" "$SPDX_LINE_HASH" + else + prepend_top "$file" "$SPDX_LINE_HASH" + fi + count=$((count + 1)) + done < <(find scripts -type f -name '*.sh' -print0) + printf 'Shell: added headers to %d files (skipped %d)\n' "$count" "$skipped" +} + +# Process Dockerfiles. Targets explicit list rather than a glob to avoid +# accidentally touching vendor/test Dockerfiles outside our scope. +process_dockerfiles() { + local count=0 skipped=0 + local dockerfiles=( + "Dockerfile.local" + "cmd/api-server/Dockerfile" + "cmd/indexer/Dockerfile" + "cmd/relayer/Dockerfile" + ) + for file in "${dockerfiles[@]}"; do + if [[ ! -f "$file" ]]; then + continue + fi + if should_skip "$file"; then + skipped=$((skipped + 1)) + continue + fi + prepend_top "$file" "$SPDX_LINE_HASH" + count=$((count + 1)) + done + printf 'Dockerfile: added headers to %d files (skipped %d)\n' "$count" "$skipped" +} + +main() { + if [[ ! -f go.mod ]]; then + echo "error: run from repository root (go.mod not found)" >&2 + exit 1 + fi + process_go + process_shell + process_dockerfiles +} + +main "$@" From bf6dce632a2afa9b3ac2f3972fa8ca6f6c63c8a5 Mon Sep 17 00:00:00 2001 From: Sebastian Lindner <33971232+salindne@users.noreply.github.com> Date: Wed, 27 May 2026 11:15:38 -0600 Subject: [PATCH 10/10] licensing: drop unrelated WIP scripts pulled in by an earlier git add `scripts/testing/send-usdcx-direct.go`, `scripts/testing/send-usdcx-transfer.go`, and `scripts/testing/withdraw-via-interface.go` were untracked working files at the time of branch creation and got swept into commit cbbcd74 by a `git add scripts/` invocation that was supposed to stage only the chmod-only changes on shell scripts. None of these files exist on main; removing them here so this PR stays scoped to licensing. --- scripts/testing/send-usdcx-direct.go | 618 ---------------------- scripts/testing/send-usdcx-transfer.go | 161 ------ scripts/testing/withdraw-via-interface.go | 561 -------------------- 3 files changed, 1340 deletions(-) delete mode 100644 scripts/testing/send-usdcx-direct.go delete mode 100644 scripts/testing/send-usdcx-transfer.go delete mode 100644 scripts/testing/withdraw-via-interface.go diff --git a/scripts/testing/send-usdcx-direct.go b/scripts/testing/send-usdcx-direct.go deleted file mode 100644 index b82ffdf..0000000 --- a/scripts/testing/send-usdcx-direct.go +++ /dev/null @@ -1,618 +0,0 @@ -//go:build ignore - -// send-usdcx-direct.go — Send USDCx from a standalone external party, bypassing -// the middleware's hardcoded 1-hour offer validity so the receiver has effectively -// unlimited time to accept. -// -// Mirrors accept-via-interface.go in structure: loads party credentials, calls -// the registrar's HTTP API to get the transfer factory + choice context + -// disclosed contracts, encodes the AnyValue-shaped choice context inline, then -// exercises Splice.Api.Token.TransferInstructionV1:TransferFactory_Transfer -// via Interactive Submission with a far-future executeBefore. -// -// Usage: -// go run scripts/testing/send-usdcx-direct.go \ -// -config config.api-server.devnet-test.yaml \ -// -creds ./party-credentials.txt \ -// -to "damlcopilot-receiver::1220096316d4ea75c021d89123cfd2792cfeac80dfbf90bfbca21bcd8bf1bb40d84c" \ -// -amount "2" - -package main - -import ( - "bufio" - "bytes" - "context" - "encoding/base64" - "encoding/hex" - "encoding/json" - "flag" - "fmt" - "io" - "net/http" - "os" - "sort" - "strings" - "time" - - canton "github.com/chainsafe/canton-middleware/pkg/cantonsdk/client" - lapiv2 "github.com/chainsafe/canton-middleware/pkg/cantonsdk/lapi/v2" - interactivev2 "github.com/chainsafe/canton-middleware/pkg/cantonsdk/lapi/v2/interactive" - "github.com/chainsafe/canton-middleware/pkg/cantonsdk/token" - "github.com/chainsafe/canton-middleware/pkg/cantonsdk/values" - "github.com/chainsafe/canton-middleware/pkg/config" - "github.com/chainsafe/canton-middleware/pkg/keys" - - "github.com/google/uuid" - "go.uber.org/zap" -) - -const ( - // Splice TransferFactory interface — the choice argument shape matches the - // SDK's encodeTransferFactoryTransferArgs in pkg/cantonsdk/token/encode.go. - transferFactoryPackageID = "#splice-api-token-transfer-instruction-v1" - transferFactoryModule = "Splice.Api.Token.TransferInstructionV1" - transferFactoryEntity = "TransferFactory" - transferChoice = "TransferFactory_Transfer" -) - -var ( - configPath = flag.String("config", "config.api-server.devnet-test.yaml", "Config file") - credsPath = flag.String("creds", "./party-credentials.txt", "Party credentials file") - toParty = flag.String("to", "", "Receiver party ID (required)") - amount = flag.String("amount", "2", "Amount to send (decimal string)") - instrumentID = flag.String("instrument", "USDCx", "Instrument ID (token symbol on Canton)") - registryHost = flag.String("registry-host", "https://api.utilities.digitalasset-dev.com", "Registrar API base URL") - registrar = flag.String("registrar", "decentralized-usdc-interchain-rep::1220d420ba8f168d63157f610e6593dca072bbd79ff90a830efc345ed4348a816de7", "Registrar / instrument admin party ID") - expiry = flag.String("expiry", "2099-12-31T23:59:59Z", "Offer executeBefore (RFC3339). Default ≈ no time limit") - dryRun = flag.Bool("dry-run", false, "Print the registry response + plan, do not submit") - debug = flag.Bool("debug", true, "Print registry response JSON") -) - -func main() { - flag.Parse() - if *toParty == "" { - fatalf("-to is required") - } - - creds, err := loadCredentials(*credsPath) - if err != nil { - fatalf("load credentials: %v", err) - } - sender := creds["party_id"] - if sender == "" { - fatalf("party_id missing from credentials") - } - privBytes, err := hex.DecodeString(creds["private_key_hex"]) - if err != nil { - fatalf("decode private key: %v", err) - } - kp, err := keys.CantonKeyPairFromPrivateKey(privBytes) - if err != nil { - fatalf("reconstruct keypair: %v", err) - } - fp, _ := kp.Fingerprint() - - executeBefore, err := time.Parse(time.RFC3339, *expiry) - if err != nil { - fatalf("parse -expiry: %v", err) - } - - fmt.Printf(">>> sender party : %s\n", sender) - fmt.Printf(">>> sender fingerprint : %s\n", fp) - fmt.Printf(">>> receiver party : %s\n", *toParty) - fmt.Printf(">>> amount : %s %s\n", *amount, *instrumentID) - fmt.Printf(">>> executeBefore : %s (%s)\n", executeBefore.Format(time.RFC3339), humanizeDuration(time.Until(executeBefore))) - - cfg, err := config.LoadAPIServer(*configPath) - if err != nil { - fatalf("load config: %v", err) - } - logger, _ := zap.NewDevelopment() - ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) - defer cancel() - - cantonClient, err := canton.New(ctx, cfg.Canton, canton.WithLogger(logger)) - if err != nil { - fatalf("canton client: %v", err) - } - defer func() { _ = cantonClient.Close() }() - - holdings, err := cantonClient.Token.GetHoldingsByParty(ctx, sender, *instrumentID) - if err != nil { - fatalf("get holdings: %v", err) - } - fmt.Printf("\n>>> sender %s holdings: %d contract(s)\n", *instrumentID, len(holdings)) - for i, h := range holdings { - fmt.Printf(" #%d amount=%s locked=%v cid=%s\n", i+1, h.Amount, h.Locked, h.ContractID) - } - - inputCIDs, total, err := selectHoldings(holdings, *amount) - if err != nil { - fatalf("select holdings: %v", err) - } - fmt.Printf(">>> selected %d holding(s) totaling %s for amount %s\n", len(inputCIDs), total, *amount) - - httpClient := &http.Client{Timeout: 30 * time.Second} - regResp, raw, err := fetchTransferFactory(ctx, httpClient, *registryHost, *registrar, sender, *toParty, *amount, *instrumentID, inputCIDs, executeBefore) - if err != nil { - fatalf("registry transfer-factory: %v", err) - } - if *debug { - fmt.Printf("\n --- registry response ---\n%s\n -------------------------\n", indentJSON(raw)) - } - fmt.Printf(">>> factoryId : %s\n", regResp.FactoryID) - fmt.Printf(">>> transferKind : %s\n", regResp.TransferKind) - fmt.Printf(">>> disclosed contracts: %d\n", len(regResp.ChoiceContext.DisclosedContracts)) - - ctxValue, err := encodeChoiceContextRecord(regResp.ChoiceContext.ChoiceContextData.Values) - if err != nil { - fatalf("encode choice context: %v", err) - } - extraArgs := buildExtraArgsValue(ctxValue) - - disclosed, err := buildDisclosedContracts(regResp.ChoiceContext.DisclosedContracts, cfg.Canton.DomainID) - if err != nil { - fatalf("build disclosed contracts: %v", err) - } - - if *dryRun { - fmt.Println("\n[dry-run] would exercise TransferFactory_Transfer — skipping submission") - return - } - - t0 := time.Now() - offerCID, err := submitTransfer(ctx, cantonClient, cfg, sender, kp, regResp.FactoryID, *toParty, *amount, *instrumentID, inputCIDs, executeBefore, extraArgs, disclosed) - if err != nil { - fatalf("submit transfer: %v", err) - } - fmt.Printf("\n>>> SUCCESS in %s\n>>> TransferOffer CID: %s\n>>> receiver must exercise TransferInstruction_Accept before %s to settle\n", - time.Since(t0).Round(time.Millisecond), offerCID, executeBefore.Format(time.RFC3339)) -} - -// ---------- holdings selection ---------- - -func selectHoldings(holdings []*token.Holding, amount string) ([]string, string, error) { - // Greedy: sort by amount descending, take until total >= amount. - // Skip locked holdings (already committed elsewhere). - available := make([]*token.Holding, 0, len(holdings)) - for _, h := range holdings { - if h.Locked { - continue - } - available = append(available, h) - } - sort.Slice(available, func(i, j int) bool { - return parseDecimal(available[i].Amount) > parseDecimal(available[j].Amount) - }) - target := parseDecimal(amount) - var total float64 - var cids []string - for _, h := range available { - cids = append(cids, h.ContractID) - total += parseDecimal(h.Amount) - if total >= target { - return cids, fmt.Sprintf("%g", total), nil - } - } - return nil, "", fmt.Errorf("insufficient balance: have %g, need %s", total, amount) -} - -func parseDecimal(s string) float64 { - var f float64 - _, _ = fmt.Sscanf(s, "%f", &f) - return f -} - -// ---------- Registry: transfer-factory ---------- - -type transferFactoryResponse struct { - FactoryID string `json:"factoryId"` - TransferKind string `json:"transferKind"` - ChoiceContext transferFactoryInnerResponse `json:"choiceContext"` -} - -type transferFactoryInnerResponse struct { - ChoiceContextData struct { - Values map[string]json.RawMessage `json:"values"` - } `json:"choiceContextData"` - DisclosedContracts []registryDisclosedContract `json:"disclosedContracts"` -} - -type registryDisclosedContract struct { - ContractID string `json:"contractId"` - CreatedEventBlob string `json:"createdEventBlob"` - TemplateID json.RawMessage `json:"templateId"` - SynchronizerID string `json:"synchronizerId"` -} - -func fetchTransferFactory( - ctx context.Context, hc *http.Client, host, registrarParty, sender, receiver, amount, instrumentID string, - inputHoldingCIDs []string, executeBefore time.Time, -) (*transferFactoryResponse, []byte, error) { - now := time.Now().UTC() - emptyValues := map[string]any{"values": map[string]any{}} - - body, err := json.Marshal(map[string]any{ - "choiceArguments": map[string]any{ - "expectedAdmin": registrarParty, - "transfer": map[string]any{ - "sender": sender, - "receiver": receiver, - "amount": amount, - "instrumentId": map[string]any{ - "admin": registrarParty, - "id": instrumentID, - }, - "inputHoldingCids": inputHoldingCIDs, - "meta": emptyValues, - "requestedAt": now.Format("2006-01-02T15:04:05.000Z"), - "executeBefore": executeBefore.UTC().Format("2006-01-02T15:04:05.000Z"), - }, - "extraArgs": map[string]any{ - "context": emptyValues, - "meta": emptyValues, - }, - }, - }) - if err != nil { - return nil, nil, fmt.Errorf("marshal request: %w", err) - } - - url := fmt.Sprintf( - "%s/api/token-standard/v0/registrars/%s/registry/transfer-instruction/v1/transfer-factory", - strings.TrimRight(host, "/"), registrarParty, - ) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) - if err != nil { - return nil, nil, err - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - - resp, err := hc.Do(req) - if err != nil { - return nil, nil, fmt.Errorf("POST %s: %w", url, err) - } - defer resp.Body.Close() - raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) - if resp.StatusCode != http.StatusOK { - return nil, raw, fmt.Errorf("registry returned %d: %s", resp.StatusCode, string(raw)) - } - - var out transferFactoryResponse - if err := json.Unmarshal(raw, &out); err != nil { - return nil, raw, fmt.Errorf("parse registry response: %w", err) - } - return &out, raw, nil -} - -// ---------- AnyValue encoding (identical to accept-via-interface.go) ---------- - -type anyValueJSON struct { - Tag string `json:"tag"` - Value json.RawMessage `json:"value"` -} - -func encodeAnyValue(raw json.RawMessage) (*lapiv2.Value, error) { - var av anyValueJSON - if err := json.Unmarshal(raw, &av); err != nil { - return nil, fmt.Errorf("parse AnyValue: %w", err) - } - var inner *lapiv2.Value - switch av.Tag { - case "AV_ContractId": - var s string - if err := json.Unmarshal(av.Value, &s); err != nil { - return nil, err - } - inner = values.ContractIDValue(s) - case "AV_Text": - var s string - if err := json.Unmarshal(av.Value, &s); err != nil { - return nil, err - } - inner = values.TextValue(s) - case "AV_Party": - var s string - if err := json.Unmarshal(av.Value, &s); err != nil { - return nil, err - } - inner = values.PartyValue(s) - case "AV_Bool": - var b bool - if err := json.Unmarshal(av.Value, &b); err != nil { - return nil, err - } - inner = &lapiv2.Value{Sum: &lapiv2.Value_Bool{Bool: b}} - case "AV_Int": - var n json.Number - if err := json.Unmarshal(av.Value, &n); err != nil { - return nil, err - } - i, err := n.Int64() - if err != nil { - return nil, fmt.Errorf("AV_Int: %w", err) - } - inner = &lapiv2.Value{Sum: &lapiv2.Value_Int64{Int64: i}} - case "AV_Decimal": - var s string - if err := json.Unmarshal(av.Value, &s); err != nil { - return nil, err - } - inner = values.NumericValue(s) - case "AV_List": - var items []json.RawMessage - if err := json.Unmarshal(av.Value, &items); err != nil { - return nil, err - } - elems := make([]*lapiv2.Value, 0, len(items)) - for _, it := range items { - v, err := encodeAnyValue(it) - if err != nil { - return nil, err - } - elems = append(elems, v) - } - inner = values.ListValue(elems) - default: - return nil, fmt.Errorf("unsupported AnyValue tag: %s (raw=%s)", av.Tag, string(raw)) - } - return &lapiv2.Value{ - Sum: &lapiv2.Value_Variant{ - Variant: &lapiv2.Variant{ - Constructor: av.Tag, - Value: inner, - }, - }, - }, nil -} - -func encodeChoiceContextRecord(vals map[string]json.RawMessage) (*lapiv2.Value, error) { - keys := make([]string, 0, len(vals)) - for k := range vals { - keys = append(keys, k) - } - sort.Strings(keys) - entries := make([]*lapiv2.TextMap_Entry, 0, len(vals)) - for _, k := range keys { - v, err := encodeAnyValue(vals[k]) - if err != nil { - return nil, fmt.Errorf("encode key %q: %w", k, err) - } - entries = append(entries, &lapiv2.TextMap_Entry{Key: k, Value: v}) - } - return &lapiv2.Value{ - Sum: &lapiv2.Value_Record{ - Record: &lapiv2.Record{ - Fields: []*lapiv2.RecordField{ - { - Label: "values", - Value: &lapiv2.Value{Sum: &lapiv2.Value_TextMap{TextMap: &lapiv2.TextMap{Entries: entries}}}, - }, - }, - }, - }, - }, nil -} - -func buildExtraArgsValue(ctxValue *lapiv2.Value) *lapiv2.Value { - return &lapiv2.Value{ - Sum: &lapiv2.Value_Record{ - Record: &lapiv2.Record{ - Fields: []*lapiv2.RecordField{ - {Label: "context", Value: ctxValue}, - {Label: "meta", Value: values.EmptyMetadata()}, - }, - }, - }, - } -} - -// ---------- DisclosedContracts ---------- - -func parseTemplateID(raw json.RawMessage) (*lapiv2.Identifier, error) { - if len(raw) == 0 { - return nil, nil - } - var s string - if err := json.Unmarshal(raw, &s); err == nil && s != "" { - parts := strings.SplitN(s, ":", 3) - if len(parts) != 3 { - return nil, fmt.Errorf("templateId %q not in pkg:module:entity form", s) - } - return &lapiv2.Identifier{PackageId: parts[0], ModuleName: parts[1], EntityName: parts[2]}, nil - } - var obj struct { - PackageID string `json:"packageId"` - ModuleName string `json:"moduleName"` - EntityName string `json:"entityName"` - } - if err := json.Unmarshal(raw, &obj); err != nil { - return nil, fmt.Errorf("parse templateId: %w", err) - } - return &lapiv2.Identifier{PackageId: obj.PackageID, ModuleName: obj.ModuleName, EntityName: obj.EntityName}, nil -} - -func buildDisclosedContracts(rcs []registryDisclosedContract, fallbackDomain string) ([]*lapiv2.DisclosedContract, error) { - out := make([]*lapiv2.DisclosedContract, 0, len(rcs)) - for _, c := range rcs { - blob, err := base64.StdEncoding.DecodeString(c.CreatedEventBlob) - if err != nil { - return nil, fmt.Errorf("decode blob for %s: %w", c.ContractID, err) - } - tid, err := parseTemplateID(c.TemplateID) - if err != nil { - return nil, err - } - domain := c.SynchronizerID - if domain == "" { - domain = fallbackDomain - } - out = append(out, &lapiv2.DisclosedContract{ - TemplateId: tid, - ContractId: c.ContractID, - CreatedEventBlob: blob, - SynchronizerId: domain, - }) - } - return out, nil -} - -// ---------- Submit ---------- - -func submitTransfer( - ctx context.Context, c *canton.Client, cfg *config.APIServer, - sender string, kp *keys.CantonKeyPair, - factoryCID, receiver, amount, instrumentID string, - inputHoldingCIDs []string, executeBefore time.Time, - extraArgs *lapiv2.Value, disclosed []*lapiv2.DisclosedContract, -) (string, error) { - // Backdate requestedAt by 5s to dodge ledger-time skew (matches SDK behavior). - now := time.Now().UTC().Add(-5 * time.Second) - - holdingCidValues := make([]*lapiv2.Value, len(inputHoldingCIDs)) - for i, cid := range inputHoldingCIDs { - holdingCidValues[i] = values.ContractIDValue(cid) - } - - transferRec := &lapiv2.Value{ - Sum: &lapiv2.Value_Record{ - Record: &lapiv2.Record{ - Fields: []*lapiv2.RecordField{ - {Label: "sender", Value: values.PartyValue(sender)}, - {Label: "receiver", Value: values.PartyValue(receiver)}, - {Label: "amount", Value: values.NumericValue(amount)}, - {Label: "instrumentId", Value: values.EncodeInstrumentId(*registrar, instrumentID)}, - {Label: "requestedAt", Value: values.TimestampValue(now)}, - {Label: "executeBefore", Value: values.TimestampValue(executeBefore)}, - {Label: "inputHoldingCids", Value: values.ListValue(holdingCidValues)}, - {Label: "meta", Value: values.EmptyMetadata()}, - }, - }, - }, - } - - cmd := &lapiv2.Command{ - Command: &lapiv2.Command_Exercise{ - Exercise: &lapiv2.ExerciseCommand{ - TemplateId: &lapiv2.Identifier{ - PackageId: transferFactoryPackageID, - ModuleName: transferFactoryModule, - EntityName: transferFactoryEntity, - }, - ContractId: factoryCID, - Choice: transferChoice, - ChoiceArgument: &lapiv2.Value{ - Sum: &lapiv2.Value_Record{ - Record: &lapiv2.Record{ - Fields: []*lapiv2.RecordField{ - {Label: "expectedAdmin", Value: values.PartyValue(*registrar)}, - {Label: "transfer", Value: transferRec}, - {Label: "extraArgs", Value: extraArgs}, - }, - }, - }, - }, - }, - }, - } - - authCtx := c.Ledger.AuthContext(ctx) - prepResp, err := c.Ledger.Interactive().PrepareSubmission(authCtx, &interactivev2.PrepareSubmissionRequest{ - UserId: cfg.Canton.Token.UserID, - CommandId: uuid.NewString(), - Commands: []*lapiv2.Command{cmd}, - ActAs: []string{sender}, - SynchronizerId: cfg.Canton.DomainID, - DisclosedContracts: disclosed, - }) - if err != nil { - return "", fmt.Errorf("PrepareSubmission: %w", err) - } - derSig, err := kp.SignDER(prepResp.PreparedTransactionHash) - if err != nil { - return "", fmt.Errorf("sign: %w", err) - } - fingerprint, _ := kp.Fingerprint() - - execResp, err := c.Ledger.Interactive().ExecuteSubmissionAndWait(authCtx, &interactivev2.ExecuteSubmissionAndWaitRequest{ - PreparedTransaction: prepResp.PreparedTransaction, - PartySignatures: &interactivev2.PartySignatures{ - Signatures: []*interactivev2.SinglePartySignatures{ - { - Party: sender, - Signatures: []*lapiv2.Signature{ - { - Format: lapiv2.SignatureFormat_SIGNATURE_FORMAT_DER, - Signature: derSig, - SignedBy: fingerprint, - SigningAlgorithmSpec: lapiv2.SigningAlgorithmSpec_SIGNING_ALGORITHM_SPEC_EC_DSA_SHA_256, - }, - }, - }, - }, - }, - SubmissionId: uuid.NewString(), - UserId: cfg.Canton.Token.UserID, - HashingSchemeVersion: prepResp.HashingSchemeVersion, - }) - if err != nil { - return "", fmt.Errorf("ExecuteSubmission: %w", err) - } - _ = execResp - // ExecuteSubmissionAndWait doesn't return the created contract id directly; - // the caller can find the resulting TransferOffer via ACS query as the receiver. - return "", nil -} - -// ---------- Helpers ---------- - -func indentJSON(raw []byte) string { - var buf bytes.Buffer - if err := json.Indent(&buf, raw, " ", " "); err != nil { - return string(raw) - } - return buf.String() -} - -func humanizeDuration(d time.Duration) string { - if d <= 0 { - return "in the past!" - } - days := int(d.Hours() / 24) - if days > 365 { - return fmt.Sprintf("~%d years from now", days/365) - } - if days > 0 { - return fmt.Sprintf("%d days from now", days) - } - return d.Round(time.Second).String() + " from now" -} - -func loadCredentials(path string) (map[string]string, error) { - f, err := os.Open(path) - if err != nil { - return nil, err - } - defer f.Close() - out := map[string]string{} - s := bufio.NewScanner(f) - for s.Scan() { - line := strings.TrimSpace(s.Text()) - if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "[") { - continue - } - idx := strings.Index(line, "=") - if idx < 0 { - continue - } - out[strings.TrimSpace(line[:idx])] = strings.TrimSpace(line[idx+1:]) - } - return out, s.Err() -} - -func fatalf(format string, args ...any) { - fmt.Fprintf(os.Stderr, "FATAL: "+format+"\n", args...) - os.Exit(1) -} diff --git a/scripts/testing/send-usdcx-transfer.go b/scripts/testing/send-usdcx-transfer.go deleted file mode 100644 index b221ba2..0000000 --- a/scripts/testing/send-usdcx-transfer.go +++ /dev/null @@ -1,161 +0,0 @@ -//go:build ignore - -// send-usdcx-transfer.go — Send USDCx from a standalone external party. -// -// Loads party credentials (the same file format as accept-usdcx-transfer.go), -// wires a Canton SDK Client with a custom KeyResolver that returns the -// credentials' secp256k1 key, then exercises TransferFactory_Transfer for the -// configured external token via the api-server's TransferByPartyID path. -// -// Usage: -// go run scripts/testing/send-usdcx-transfer.go \ -// -config config.api-server.devnet-test.yaml \ -// -creds ./party-credentials.txt \ -// -to "user_xxx::1220..." \ -// -amount "1" \ -// [-symbol USDCx] [-dry-run] -// -// The config must have token.external_tokens populated for the USDCx instrument -// admin → registry URL (the registry is consulted to discover TransferFactory). - -package main - -import ( - "bufio" - "context" - "encoding/hex" - "flag" - "fmt" - "os" - "strings" - "time" - - canton "github.com/chainsafe/canton-middleware/pkg/cantonsdk/client" - "github.com/chainsafe/canton-middleware/pkg/cantonsdk/token" - "github.com/chainsafe/canton-middleware/pkg/config" - "github.com/chainsafe/canton-middleware/pkg/keys" - - "github.com/google/uuid" - "go.uber.org/zap" -) - -var ( - configPath = flag.String("config", "config.api-server.devnet-test.yaml", "Config file") - credsPath = flag.String("creds", "./party-credentials.txt", "Party credentials file") - toParty = flag.String("to", "", "Receiver party ID (required)") - amount = flag.String("amount", "1", "Amount to send (decimal string)") - symbol = flag.String("symbol", "USDCx", "Token symbol") - dryRun = flag.Bool("dry-run", false, "Print plan, list holdings, don't submit") -) - -func main() { - flag.Parse() - if *toParty == "" { - fatalf("-to is required") - } - - creds, err := loadCredentials(*credsPath) - if err != nil { - fatalf("load credentials: %v", err) - } - fromParty := creds["party_id"] - if fromParty == "" { - fatalf("party_id missing from credentials") - } - - privBytes, err := hex.DecodeString(creds["private_key_hex"]) - if err != nil { - fatalf("decode private key: %v", err) - } - kp, err := keys.CantonKeyPairFromPrivateKey(privBytes) - if err != nil { - fatalf("reconstruct keypair: %v", err) - } - fp, _ := kp.Fingerprint() - fmt.Printf(">>> sender party : %s\n", fromParty) - fmt.Printf(">>> sender fingerprint : %s\n", fp) - fmt.Printf(">>> receiver party : %s\n", *toParty) - fmt.Printf(">>> amount : %s %s\n", *amount, *symbol) - - cfg, err := config.LoadAPIServer(*configPath) - if err != nil { - fatalf("load config: %v", err) - } - - logger, _ := zap.NewDevelopment() - ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) - defer cancel() - - // KeyResolver returns our keypair whenever the SDK asks for the sender's signer. - keyResolver := func(partyID string) (token.Signer, error) { - if partyID != fromParty { - return nil, fmt.Errorf("no key for party %s (only %s available)", partyID, fromParty) - } - return kp, nil - } - - cantonClient, err := canton.New(ctx, cfg.Canton, - canton.WithLogger(logger), - canton.WithKeyResolver(keyResolver), - ) - if err != nil { - fatalf("canton client: %v", err) - } - defer func() { _ = cantonClient.Close() }() - - // Diagnostic: list current holdings so we can confirm the sender has at least `amount`. - holdings, err := cantonClient.Token.GetHoldingsByParty(ctx, fromParty, *symbol) - if err != nil { - fatalf("get holdings: %v", err) - } - fmt.Printf("\n>>> sender holdings for %s: %d contract(s)\n", *symbol, len(holdings)) - for i, h := range holdings { - fmt.Printf(" #%d amount=%s cid=%s admin=%s\n", i+1, h.Amount, h.ContractID, h.InstrumentAdmin) - } - if len(holdings) == 0 { - fatalf("no %s holdings visible for party %s — cannot send", *symbol, fromParty) - } - - if *dryRun { - fmt.Println("\n[dry-run] would call TransferByPartyID — skipping submission") - return - } - - idempotencyKey := fmt.Sprintf("send-usdcx-%s", uuid.NewString()) - fmt.Printf("\n>>> submitting TransferFactory_Transfer (commandId=%s)…\n", idempotencyKey) - t0 := time.Now() - if err := cantonClient.Token.TransferByPartyID(ctx, idempotencyKey, fromParty, *toParty, *amount, *symbol); err != nil { - fatalf("TransferByPartyID: %v", err) - } - fmt.Printf(">>> SUCCESS in %s\n", time.Since(t0).Round(time.Millisecond)) -} - -// loadCredentials reads the key=value text file produced by allocate-standalone-party.go. -func loadCredentials(path string) (map[string]string, error) { - f, err := os.Open(path) - if err != nil { - return nil, err - } - defer f.Close() - out := map[string]string{} - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "[") { - continue - } - idx := strings.Index(line, "=") - if idx < 0 { - continue - } - k := strings.TrimSpace(line[:idx]) - v := strings.TrimSpace(line[idx+1:]) - out[k] = v - } - return out, scanner.Err() -} - -func fatalf(format string, args ...any) { - fmt.Fprintf(os.Stderr, "FATAL: "+format+"\n", args...) - os.Exit(1) -} diff --git a/scripts/testing/withdraw-via-interface.go b/scripts/testing/withdraw-via-interface.go deleted file mode 100644 index 68f3645..0000000 --- a/scripts/testing/withdraw-via-interface.go +++ /dev/null @@ -1,561 +0,0 @@ -//go:build ignore - -// withdraw-via-interface.go — Withdraw a USDCx TransferOffer the sender (us) -// previously created, by exercising the Splice CIP-56 TransferInstruction -// interface choice TransferInstruction_Withdraw. -// -// Mirrors accept-via-interface.go, but: -// - the registrar endpoint is .../choice-contexts/withdraw -// - the Daml choice is TransferInstruction_Withdraw -// - the actor (ActAs) is the sender, reclaiming their locked input holding -// -// Use this for stale offers whose executeBefore has passed and the receiver -// never accepted. -// -// Usage: -// go run scripts/testing/withdraw-via-interface.go \ -// -config config.api-server.devnet-test.yaml \ -// -creds ./party-credentials.txt \ -// [-cid ] # if omitted, withdraws every visible offer - -package main - -import ( - "bufio" - "bytes" - "context" - "encoding/base64" - "encoding/hex" - "encoding/json" - "flag" - "fmt" - "io" - "net/http" - "os" - "sort" - "strings" - "time" - - canton "github.com/chainsafe/canton-middleware/pkg/cantonsdk/client" - lapiv2 "github.com/chainsafe/canton-middleware/pkg/cantonsdk/lapi/v2" - interactivev2 "github.com/chainsafe/canton-middleware/pkg/cantonsdk/lapi/v2/interactive" - "github.com/chainsafe/canton-middleware/pkg/cantonsdk/values" - "github.com/chainsafe/canton-middleware/pkg/config" - "github.com/chainsafe/canton-middleware/pkg/keys" - - "github.com/google/uuid" - "go.uber.org/zap" -) - -const ( - // Concrete template (for ACS lookup of pending offers) - transferOfferPackageID = "#utility-registry-app-v0" - transferOfferModule = "Utility.Registry.App.V0.Model.Transfer" - transferOfferEntity = "TransferOffer" - - // Splice TransferInstruction interface (for the Withdraw choice) - transferInstrPackageID = "#splice-api-token-transfer-instruction-v1" - transferInstrModule = "Splice.Api.Token.TransferInstructionV1" - transferInstrEntity = "TransferInstruction" - withdrawChoice = "TransferInstruction_Withdraw" -) - -var ( - configPath = flag.String("config", "config.api-server.devnet-test.yaml", "Config") - credsPath = flag.String("creds", "./party-credentials.txt", "Credentials file") - registryHost = flag.String("registry-host", "https://api.utilities.digitalasset-dev.com", "Registrar API base URL") - registrar = flag.String("registrar", "decentralized-usdc-interchain-rep::1220d420ba8f168d63157f610e6593dca072bbd79ff90a830efc345ed4348a816de7", "Registrar party ID") - onlyCID = flag.String("cid", "", "Optional: withdraw only this specific TransferOffer CID. If empty, withdraws every visible offer.") - dryRun = flag.Bool("dry-run", false, "Print the registry response and command without submitting") - debug = flag.Bool("debug", true, "Print registry response JSON") -) - -func main() { - flag.Parse() - - creds, err := loadCredentials(*credsPath) - if err != nil { - fatalf("load credentials: %v", err) - } - holder := creds["party_id"] - privBytes, err := hex.DecodeString(creds["private_key_hex"]) - if err != nil { - fatalf("decode private key: %v", err) - } - kp, err := keys.CantonKeyPairFromPrivateKey(privBytes) - if err != nil { - fatalf("reconstruct keypair: %v", err) - } - - cfg, err := config.LoadAPIServer(*configPath) - if err != nil { - fatalf("load config: %v", err) - } - logger, _ := zap.NewDevelopment() - ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) - defer cancel() - - cantonClient, err := canton.New(ctx, cfg.Canton, canton.WithLogger(logger)) - if err != nil { - fatalf("canton client: %v", err) - } - defer func() { _ = cantonClient.Close() }() - - offers, err := findOffers(ctx, cantonClient, holder) - if err != nil { - fatalf("find offers: %v", err) - } - if *onlyCID != "" { - filtered := offers[:0] - for _, c := range offers { - if c == *onlyCID { - filtered = append(filtered, c) - } - } - offers = filtered - if len(offers) == 0 { - fatalf("CID %s not found among visible TransferOffers for %s", *onlyCID, holder) - } - } - fmt.Printf(">>> Found %d TransferOffer(s) to withdraw for %s\n", len(offers), holder) - if len(offers) == 0 { - return - } - - httpClient := &http.Client{Timeout: 30 * time.Second} - - for i, cid := range offers { - fmt.Printf("\n--- Offer #%d ---\n CID: %s\n", i+1, cid) - - regResp, raw, err := fetchWithdrawContext(ctx, httpClient, *registryHost, *registrar, cid) - if err != nil { - fmt.Printf(" ERROR fetching choice-context: %v\n", err) - continue - } - if *debug { - fmt.Printf(" --- registry response ---\n%s\n -------------------------\n", indentJSON(raw)) - } - - ctxValue, err := encodeChoiceContextRecord(regResp.ChoiceContextData.Values) - if err != nil { - fmt.Printf(" ERROR encoding choice context: %v\n", err) - continue - } - extraArgs := buildExtraArgsValue(ctxValue) - - disclosed, err := buildDisclosedContracts(regResp.DisclosedContracts, cfg.Canton.DomainID) - if err != nil { - fmt.Printf(" ERROR building disclosed contracts: %v\n", err) - continue - } - fmt.Printf(" Disclosed contracts: %d\n", len(disclosed)) - - if *dryRun { - fmt.Println(" [dry-run] would exercise TransferInstruction_Withdraw") - continue - } - - if err := withdrawViaInterface(ctx, cantonClient, cfg, holder, kp, cid, extraArgs, disclosed); err != nil { - fmt.Printf(" ERROR: %v\n", err) - continue - } - fmt.Println(" WITHDRAWN") - } -} - -func findOffers(ctx context.Context, c *canton.Client, partyID string) ([]string, error) { - end, err := c.Ledger.GetLedgerEnd(ctx) - if err != nil { - return nil, err - } - if end == 0 { - return nil, nil - } - tid := &lapiv2.Identifier{ - PackageId: transferOfferPackageID, ModuleName: transferOfferModule, EntityName: transferOfferEntity, - } - events, err := c.Ledger.GetActiveContractsByTemplate(ctx, end, []string{partyID}, tid) - if err != nil { - return nil, err - } - var out []string - for _, ce := range events { - out = append(out, ce.ContractId) - } - return out, nil -} - -// ---------- Registry ---------- - -type acceptContextResponse struct { - ChoiceContextData struct { - Values map[string]json.RawMessage `json:"values"` - } `json:"choiceContextData"` - DisclosedContracts []registryDisclosedContract `json:"disclosedContracts"` -} - -type registryDisclosedContract struct { - ContractID string `json:"contractId"` - CreatedEventBlob string `json:"createdEventBlob"` - TemplateID json.RawMessage `json:"templateId"` - SynchronizerID string `json:"synchronizerId"` -} - -func fetchWithdrawContext(ctx context.Context, hc *http.Client, host, registrarParty, instructionCID string) (*acceptContextResponse, []byte, error) { - url := fmt.Sprintf( - "%s/api/token-standard/v0/registrars/%s/registry/transfer-instruction/v1/%s/choice-contexts/withdraw", - strings.TrimRight(host, "/"), registrarParty, instructionCID, - ) - body := []byte(`{"meta":{},"excludeDebugFields":false}`) - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) - if err != nil { - return nil, nil, err - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - - resp, err := hc.Do(req) - if err != nil { - return nil, nil, fmt.Errorf("registry POST %s: %w", url, err) - } - defer resp.Body.Close() - - raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) - if resp.StatusCode != http.StatusOK { - return nil, raw, fmt.Errorf("registry returned %d: %s", resp.StatusCode, string(raw)) - } - - var out acceptContextResponse - if err := json.Unmarshal(raw, &out); err != nil { - return nil, raw, fmt.Errorf("parse registry response: %w", err) - } - return &out, raw, nil -} - -// ---------- AnyValue encoding ---------- - -// anyValueJSON is the {"tag": "...", "value": ...} envelope used by Daml-LF JSON -// for the AnyValue ADT. -type anyValueJSON struct { - Tag string `json:"tag"` - Value json.RawMessage `json:"value"` -} - -func encodeAnyValue(raw json.RawMessage) (*lapiv2.Value, error) { - var av anyValueJSON - if err := json.Unmarshal(raw, &av); err != nil { - return nil, fmt.Errorf("parse AnyValue: %w", err) - } - var inner *lapiv2.Value - switch av.Tag { - case "AV_ContractId": - var s string - if err := json.Unmarshal(av.Value, &s); err != nil { - return nil, err - } - inner = values.ContractIDValue(s) - case "AV_Text": - var s string - if err := json.Unmarshal(av.Value, &s); err != nil { - return nil, err - } - inner = values.TextValue(s) - case "AV_Party": - var s string - if err := json.Unmarshal(av.Value, &s); err != nil { - return nil, err - } - inner = values.PartyValue(s) - case "AV_Bool": - var b bool - if err := json.Unmarshal(av.Value, &b); err != nil { - return nil, err - } - inner = &lapiv2.Value{Sum: &lapiv2.Value_Bool{Bool: b}} - case "AV_Int": - var n json.Number - if err := json.Unmarshal(av.Value, &n); err != nil { - return nil, err - } - i, err := n.Int64() - if err != nil { - return nil, fmt.Errorf("AV_Int: %w", err) - } - inner = &lapiv2.Value{Sum: &lapiv2.Value_Int64{Int64: i}} - case "AV_Decimal": - var s string - if err := json.Unmarshal(av.Value, &s); err != nil { - return nil, err - } - inner = values.NumericValue(s) - case "AV_List": - var items []json.RawMessage - if err := json.Unmarshal(av.Value, &items); err != nil { - return nil, err - } - elems := make([]*lapiv2.Value, 0, len(items)) - for _, it := range items { - v, err := encodeAnyValue(it) - if err != nil { - return nil, err - } - elems = append(elems, v) - } - inner = values.ListValue(elems) - case "AV_Map": - // Map is encoded as a list of (Text, AnyValue) tuples - var items []struct { - Key string `json:"_1"` - Value json.RawMessage `json:"_2"` - } - if err := json.Unmarshal(av.Value, &items); err != nil { - return nil, err - } - elems := make([]*lapiv2.Value, 0, len(items)) - for _, it := range items { - v, err := encodeAnyValue(it.Value) - if err != nil { - return nil, err - } - elems = append(elems, &lapiv2.Value{ - Sum: &lapiv2.Value_Record{ - Record: &lapiv2.Record{ - Fields: []*lapiv2.RecordField{ - {Label: "_1", Value: values.TextValue(it.Key)}, - {Label: "_2", Value: v}, - }, - }, - }, - }) - } - inner = values.ListValue(elems) - default: - return nil, fmt.Errorf("unsupported AnyValue tag: %s (raw=%s)", av.Tag, string(raw)) - } - return &lapiv2.Value{ - Sum: &lapiv2.Value_Variant{ - Variant: &lapiv2.Variant{ - Constructor: av.Tag, - Value: inner, - }, - }, - }, nil -} - -// encodeChoiceContextRecord builds the Splice ChoiceContext record: -// -// ChoiceContext { values: TextMap AnyValue } -func encodeChoiceContextRecord(vals map[string]json.RawMessage) (*lapiv2.Value, error) { - keys := make([]string, 0, len(vals)) - for k := range vals { - keys = append(keys, k) - } - sort.Strings(keys) - - entries := make([]*lapiv2.TextMap_Entry, 0, len(vals)) - for _, k := range keys { - v, err := encodeAnyValue(vals[k]) - if err != nil { - return nil, fmt.Errorf("encode key %q: %w", k, err) - } - entries = append(entries, &lapiv2.TextMap_Entry{Key: k, Value: v}) - } - return &lapiv2.Value{ - Sum: &lapiv2.Value_Record{ - Record: &lapiv2.Record{ - Fields: []*lapiv2.RecordField{ - { - Label: "values", - Value: &lapiv2.Value{ - Sum: &lapiv2.Value_TextMap{ - TextMap: &lapiv2.TextMap{Entries: entries}, - }, - }, - }, - }, - }, - }, - }, nil -} - -func buildExtraArgsValue(ctxValue *lapiv2.Value) *lapiv2.Value { - return &lapiv2.Value{ - Sum: &lapiv2.Value_Record{ - Record: &lapiv2.Record{ - Fields: []*lapiv2.RecordField{ - {Label: "context", Value: ctxValue}, - {Label: "meta", Value: values.EmptyMetadata()}, - }, - }, - }, - } -} - -// ---------- DisclosedContracts ---------- - -// templateID may be a string ":Module:Entity" or a {packageId, moduleName, entityName} object. -func parseTemplateID(raw json.RawMessage) (*lapiv2.Identifier, error) { - if len(raw) == 0 { - return nil, nil - } - var s string - if err := json.Unmarshal(raw, &s); err == nil && s != "" { - parts := strings.SplitN(s, ":", 3) - if len(parts) != 3 { - return nil, fmt.Errorf("templateId %q not in pkg:module:entity form", s) - } - return &lapiv2.Identifier{PackageId: parts[0], ModuleName: parts[1], EntityName: parts[2]}, nil - } - var obj struct { - PackageID string `json:"packageId"` - ModuleName string `json:"moduleName"` - EntityName string `json:"entityName"` - } - if err := json.Unmarshal(raw, &obj); err != nil { - return nil, fmt.Errorf("parse templateId: %w", err) - } - return &lapiv2.Identifier{PackageId: obj.PackageID, ModuleName: obj.ModuleName, EntityName: obj.EntityName}, nil -} - -func buildDisclosedContracts(rcs []registryDisclosedContract, fallbackDomain string) ([]*lapiv2.DisclosedContract, error) { - out := make([]*lapiv2.DisclosedContract, 0, len(rcs)) - for _, c := range rcs { - blob, err := base64.StdEncoding.DecodeString(c.CreatedEventBlob) - if err != nil { - return nil, fmt.Errorf("decode blob for %s: %w", c.ContractID, err) - } - tid, err := parseTemplateID(c.TemplateID) - if err != nil { - return nil, err - } - domain := c.SynchronizerID - if domain == "" { - domain = fallbackDomain - } - out = append(out, &lapiv2.DisclosedContract{ - TemplateId: tid, - ContractId: c.ContractID, - CreatedEventBlob: blob, - SynchronizerId: domain, - }) - } - return out, nil -} - -// ---------- Submit ---------- - -func withdrawViaInterface( - ctx context.Context, - c *canton.Client, - cfg *config.APIServer, - holder string, - kp *keys.CantonKeyPair, - contractID string, - extraArgs *lapiv2.Value, - disclosed []*lapiv2.DisclosedContract, -) error { - cmd := &lapiv2.Command{ - Command: &lapiv2.Command_Exercise{ - Exercise: &lapiv2.ExerciseCommand{ - TemplateId: &lapiv2.Identifier{ - PackageId: transferInstrPackageID, - ModuleName: transferInstrModule, - EntityName: transferInstrEntity, - }, - ContractId: contractID, - Choice: withdrawChoice, - ChoiceArgument: &lapiv2.Value{ - Sum: &lapiv2.Value_Record{ - Record: &lapiv2.Record{ - Fields: []*lapiv2.RecordField{ - {Label: "extraArgs", Value: extraArgs}, - }, - }, - }, - }, - }, - }, - } - - authCtx := c.Ledger.AuthContext(ctx) - prepResp, err := c.Ledger.Interactive().PrepareSubmission(authCtx, &interactivev2.PrepareSubmissionRequest{ - UserId: cfg.Canton.Token.UserID, - CommandId: uuid.NewString(), - Commands: []*lapiv2.Command{cmd}, - ActAs: []string{holder}, - SynchronizerId: cfg.Canton.DomainID, - DisclosedContracts: disclosed, - }) - if err != nil { - return fmt.Errorf("PrepareSubmission: %w", err) - } - - derSig, err := kp.SignDER(prepResp.PreparedTransactionHash) - if err != nil { - return fmt.Errorf("sign: %w", err) - } - fingerprint, _ := kp.Fingerprint() - - if _, err := c.Ledger.Interactive().ExecuteSubmissionAndWait(authCtx, &interactivev2.ExecuteSubmissionAndWaitRequest{ - PreparedTransaction: prepResp.PreparedTransaction, - PartySignatures: &interactivev2.PartySignatures{ - Signatures: []*interactivev2.SinglePartySignatures{ - { - Party: holder, - Signatures: []*lapiv2.Signature{ - { - Format: lapiv2.SignatureFormat_SIGNATURE_FORMAT_DER, - Signature: derSig, - SignedBy: fingerprint, - SigningAlgorithmSpec: lapiv2.SigningAlgorithmSpec_SIGNING_ALGORITHM_SPEC_EC_DSA_SHA_256, - }, - }, - }, - }, - }, - SubmissionId: uuid.NewString(), - UserId: cfg.Canton.Token.UserID, - HashingSchemeVersion: prepResp.HashingSchemeVersion, - }); err != nil { - return fmt.Errorf("ExecuteSubmission: %w", err) - } - return nil -} - -// ---------- Helpers ---------- - -func indentJSON(raw []byte) string { - var buf bytes.Buffer - if err := json.Indent(&buf, raw, " ", " "); err != nil { - return string(raw) - } - return buf.String() -} - -func loadCredentials(path string) (map[string]string, error) { - f, err := os.Open(path) - if err != nil { - return nil, err - } - defer f.Close() - out := map[string]string{} - s := bufio.NewScanner(f) - for s.Scan() { - line := strings.TrimSpace(s.Text()) - if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "[") { - continue - } - idx := strings.Index(line, "=") - if idx < 0 { - continue - } - out[strings.TrimSpace(line[:idx])] = strings.TrimSpace(line[idx+1:]) - } - return out, s.Err() -} - -func fatalf(format string, args ...any) { - fmt.Fprintf(os.Stderr, "FATAL: "+format+"\n", args...) - os.Exit(1) -}