Skip to content

feat: write synthetic EVM Transfer log for non-custodial transfers#253

Draft
sadiq1971 wants to merge 1 commit into
mainfrom
sadiq/fix/non-custodial-evm-logs
Draft

feat: write synthetic EVM Transfer log for non-custodial transfers#253
sadiq1971 wants to merge 1 commit into
mainfrom
sadiq/fix/non-custodial-evm-logs

Conversation

@sadiq1971
Copy link
Copy Markdown
Member

After a successful /api/v2/transfer/execute, the TransferService now opens a synthetic EVM block and persists an ERC-20 Transfer log entry so that eth_getLogs / the Activity page reflects non-custodial (snap-signed) transfers alongside custodial ones.

Key changes:

  • CachedTransfer wraps PreparedTransfer with EVM metadata (from/to EVM addr, contract address, amount bytes, calldata) stored at Prepare time so Execute has everything needed to build the log.
  • TransferService accepts an optional EvmLogStore (nil when EthRPC is disabled); on Execute success it writes block + tx + Transfer log.
  • server.go wires the evmStore into TransferService when EthRPC is enabled; drops the now-unused tokenSymbols() helper.
  • Tests updated: external package (transfer_test) eliminates the import cycle caused by the mock importing the transfer package.

After a successful /api/v2/transfer/execute, write a synthetic EVM block
containing a Transfer log so eth_getLogs / the Activity page shows
non-custodial (snap-signed) transfers alongside custodial ones.

EVM metadata (from/to EVM addr, contract address, amount) is captured
into a sync.Map at Prepare time and consumed at Execute time.
evmStore is nil when EthRPC is disabled so the path is zero-cost then.
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 4, 2026

Codecov Report

❌ Patch coverage is 0% with 107 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@232aa94). Learn more about missing BASE report.

Files with missing lines Patch % Lines
pkg/transfer/service.go 0.00% 98 Missing and 2 partials ⚠️
pkg/app/api/server.go 0.00% 7 Missing ⚠️

❌ Your patch status has failed because the patch coverage (0.00%) is below the target coverage (50.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #253   +/-   ##
=======================================
  Coverage        ?   31.96%           
=======================================
  Files           ?      127           
  Lines           ?     9073           
  Branches        ?        0           
=======================================
  Hits            ?     2900           
  Misses          ?     5921           
  Partials        ?      252           
Flag Coverage Δ
unittests 31.96% <0.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
pkg/app/api/server.go 0.00% <0.00%> (ø)
pkg/transfer/service.go 24.56% <0.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements synthetic EVM log generation for non-custodial transfers to ensure they appear in activity logs and external indexers. It introduces a CachedTransfer struct to store EVM-specific metadata during the preparation phase and adds logic to the Execute method to persist these logs via a new EvmLogStore interface. Review feedback suggests improving data integrity by failing the preparation phase if amount parsing fails, addressing potential race conditions in nonce management, and adding validation to prevent negative amounts from being processed.

Comment thread pkg/transfer/service.go Outdated
Comment on lines +146 to +149
amount, parseErr := parseTokenAmount(req.Amount, entry.decimals)
if parseErr != nil {
amount = new(big.Int) // fall back to zero; log will show 0 amount
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Falling back to a zero amount when parsing fails is risky as it results in misleading synthetic logs for successful transfers. Since PrepareTransfer has already succeeded, any parsing failure here indicates an inconsistency between the Canton amount format and this local parser. It is better to return an error and fail the Prepare call to ensure data integrity between the Canton and EVM layers.

Suggested change
amount, parseErr := parseTokenAmount(req.Amount, entry.decimals)
if parseErr != nil {
amount = new(big.Int) // fall back to zero; log will show 0 amount
}
amount, err := parseTokenAmount(req.Amount, entry.decimals)
if err != nil {
return nil, apperrors.BadRequestError(err, "invalid amount format for EVM log")
}

Comment thread pkg/transfer/service.go Outdated
// EVM transaction record) for a completed non-custodial transfer. Errors here
// are non-fatal: the Canton transfer already succeeded.
func (s *TransferService) writeEvmTransferLog(ctx context.Context, cached *CachedTransfer) error {
nonce, err := s.evmStore.GetEvmTransactionCount(ctx, cached.SenderEVMAddr)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Retrieving the nonce via GetEvmTransactionCount without a lock or a sequence generator in the store is prone to race conditions if multiple transfers for the same sender are executed concurrently. This could lead to duplicate nonces in the synthetic EVM history, causing conflicts when the activity page or external indexers process these logs. Consider using a more robust nonce management strategy if high concurrency is expected.

Comment thread pkg/transfer/service.go
Comment on lines +291 to +308
func parseTokenAmount(amountStr string, decimals int) (*big.Int, error) {
parts := strings.SplitN(strings.TrimSpace(amountStr), ".", 2)
whole := strings.ReplaceAll(parts[0], ",", "")
frac := ""
if len(parts) == 2 {
frac = parts[1]
}
if len(frac) > decimals {
return nil, fmt.Errorf("too many decimal places in %q", amountStr)
}
frac += strings.Repeat("0", decimals-len(frac))
raw := whole + frac
result := new(big.Int)
if _, ok := result.SetString(raw, 10); !ok {
return nil, fmt.Errorf("invalid amount %q", amountStr)
}
return result, nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The parseTokenAmount function does not explicitly check for negative amounts. While the Canton layer likely validates this, big.Int.Bytes() returns the absolute value, meaning a negative amount string would be silently logged as a positive value in the EVM layer. You should validate that the amount is non-negative before processing.

func parseTokenAmount(amountStr string, decimals int) (*big.Int, error) {
	amountStr = strings.TrimSpace(amountStr)
	if strings.HasPrefix(amountStr, "-") {
		return nil, fmt.Errorf("negative amount not allowed: %q", amountStr)
	}
	parts := strings.SplitN(amountStr, ".", 2)
	whole := strings.ReplaceAll(parts[0], ",", "")
	frac := ""
	if len(parts) == 2 {
		frac = parts[1]
	}
	if len(frac) > decimals {
		return nil, fmt.Errorf("too many decimal places in %q", amountStr)
	}
	frac += strings.Repeat("0", decimals-len(frac))
	raw := whole + frac
	result := new(big.Int)
	if _, ok := result.SetString(raw, 10); !ok {
		return nil, fmt.Errorf("invalid amount %q", amountStr)
	}
	return result, nil
}

@sadiq1971 sadiq1971 force-pushed the sadiq/fix/non-custodial-evm-logs branch from f6635d4 to a28a1e0 Compare May 4, 2026 10:56
@sadiq1971 sadiq1971 marked this pull request as draft May 4, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants