-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(screener): use chainalysis (#2744)
Co-authored-by: Trajan0x <[email protected]>
- Loading branch information
1 parent
ac748d3
commit 08e2b19
Showing
31 changed files
with
518 additions
and
1,143 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
package chainalysis | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"github.com/TwiN/gocache/v2" | ||
"github.com/valyala/fastjson" | ||
"net/http" | ||
"slices" | ||
"strings" | ||
"time" | ||
|
||
"github.com/go-resty/resty/v2" | ||
"github.com/synapsecns/sanguine/core/retry" | ||
) | ||
|
||
const ( | ||
// EntityEndpoint is the endpoint for the entity API. | ||
EntityEndpoint = "/api/risk/v2/entities" | ||
) | ||
|
||
// Client is the interface for the Chainalysis API client. It makes requests to the Chainalysis API. | ||
type Client interface { | ||
ScreenAddress(ctx context.Context, address string) (bool, error) | ||
} | ||
|
||
// clientImpl is the implementation of the Chainalysis API client. | ||
type clientImpl struct { | ||
client *resty.Client | ||
apiKey string | ||
url string | ||
riskLevels []string | ||
registrationCache *gocache.Cache | ||
} | ||
|
||
const ( | ||
maxCacheSizeGB = 3 | ||
bytesInGB = 1024 * 1024 * 1024 | ||
chainalysisRequestTimeout = 30 * time.Second | ||
) | ||
|
||
// NewClient creates a new Chainalysis API client. | ||
func NewClient(riskLevels []string, apiKey, url string) Client { | ||
client := resty.New(). | ||
SetBaseURL(url). | ||
SetHeader("Content-Type", "application/json"). | ||
SetHeader("Token", apiKey). | ||
SetTimeout(chainalysisRequestTimeout) | ||
|
||
// max cache size 3gb | ||
// TODO: make this configurable. | ||
registrationCache := gocache.NewCache().WithEvictionPolicy(gocache.LeastRecentlyUsed).WithMaxMemoryUsage(maxCacheSizeGB * bytesInGB) | ||
|
||
return &clientImpl{ | ||
client: client, | ||
apiKey: apiKey, | ||
url: url, | ||
riskLevels: riskLevels, | ||
registrationCache: registrationCache, | ||
} | ||
} | ||
|
||
// ScreenAddress screens an address from the Chainalysis API. | ||
func (c *clientImpl) ScreenAddress(parentCtx context.Context, address string) (bool, error) { | ||
// make sure to cancel the context when we're done. | ||
// this ensures if we didn't need pessimistic register, we don't wait on it. | ||
ctx, cancel := context.WithCancel(parentCtx) | ||
defer cancel() | ||
|
||
address = strings.ToLower(address) | ||
|
||
// we don't even wait on pessimistic register since if the address is already registered, but not in the in-memory cache | ||
// this will just get canceled. | ||
go func() { | ||
// Register the address in the cache. | ||
if err := c.pessimisticRegister(ctx, address); err != nil && !errors.Is(err, context.Canceled) { | ||
fmt.Printf("could not register address: %v\n", err) | ||
} | ||
}() | ||
|
||
return c.checkBlacklist(ctx, address) | ||
} | ||
|
||
// pessimisticRegister registers an address if its not in memory cache. This happens regardless it was registered before. | ||
func (c *clientImpl) pessimisticRegister(ctx context.Context, address string) error { | ||
if _, isPresent := c.registrationCache.Get(address); !isPresent { | ||
if err := c.registerAddress(ctx, address); err != nil { | ||
return fmt.Errorf("could not register address: %w", err) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (c *clientImpl) checkBlacklist(ctx context.Context, address string) (bool, error) { | ||
var resp *resty.Response | ||
// Retry until the user is registered. | ||
err := retry.WithBackoff(ctx, | ||
func(ctx context.Context) (err error) { | ||
resp, err = c.client.R(). | ||
SetContext(ctx). | ||
SetPathParam("address", address). | ||
Get(EntityEndpoint + "/" + address) | ||
if err != nil { | ||
return fmt.Errorf("could not get response: %w", err) | ||
} | ||
|
||
if resp.StatusCode() != http.StatusOK { | ||
return fmt.Errorf("could not get response: %s", resp.Status()) | ||
} | ||
return nil | ||
}, retry.WithMax(time.Second)) | ||
if err != nil { | ||
return false, fmt.Errorf("could not get response: %w", err) | ||
} | ||
|
||
// address has been found, let's screen it. | ||
c.registrationCache.Set(address, struct{}{}) | ||
|
||
risk := fastjson.GetString(resp.Body(), "risk") | ||
return slices.Contains(c.riskLevels, risk), nil | ||
} | ||
|
||
// registerAddress registers an address in the case that we try and screen for a nonexistent address. | ||
func (c *clientImpl) registerAddress(ctx context.Context, address string) error { | ||
payload := map[string]interface{}{ | ||
"address": address, | ||
} | ||
res, err := c.client.R().SetContext(ctx).SetBody(payload).Post(EntityEndpoint) | ||
if err != nil { | ||
return fmt.Errorf("could not register address: %w", err) | ||
} | ||
if res.IsError() { | ||
return fmt.Errorf("could not register address: %s", res.Status()) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
var _ Client = &clientImpl{} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
// Package chainalysis contains the implementation of the Chainalysis API client. | ||
// this implementation is incomplete, but it is a good starting point. | ||
package chainalysis |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.