From 1918b14454e579561d1c8641c8453dce1e39836c Mon Sep 17 00:00:00 2001 From: Ales Verbic Date: Thu, 3 Oct 2024 07:34:20 -0400 Subject: [PATCH] feat: add support for Kupo Signed-off-by: Ales Verbic --- go.mod | 17 ++- go.sum | 43 ++++++-- input/chainsync/chainsync.go | 88 ++++++++++++++- input/chainsync/options.go | 6 + input/chainsync/plugin.go | 9 ++ input/chainsync/transactionOutput.go | 129 ++++++++++++++++++++++ input/chainsync/transactionOutput_test.go | 67 +++++++++++ input/chainsync/tx.go | 13 ++- 8 files changed, 348 insertions(+), 24 deletions(-) create mode 100644 input/chainsync/transactionOutput.go create mode 100644 input/chainsync/transactionOutput_test.go diff --git a/go.mod b/go.mod index ced94e6..73e5675 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,9 @@ go 1.21 toolchain go1.21.6 require ( - github.com/blinklabs-io/gouroboros v0.96.0 + github.com/SundaeSwap-finance/kugo v1.0.5 + github.com/SundaeSwap-finance/ogmigo/v6 v6.0.0-20231128043329-e8ced51013a1 + github.com/blinklabs-io/gouroboros v0.98.0 github.com/gen2brain/beeep v0.0.0-20230602101333-f384c29b62dd github.com/gin-gonic/gin v1.10.0 github.com/kelseyhightower/envconfig v1.4.0 @@ -13,6 +15,7 @@ require ( github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.16.3 + github.com/utxorpc/go-codegen v0.10.0 go.uber.org/automaxprocs v1.6.0 golang.org/x/oauth2 v0.23.0 gopkg.in/yaml.v2 v2.4.0 @@ -25,6 +28,8 @@ require ( cloud.google.com/go/compute/metadata v0.3.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect + github.com/aws/aws-sdk-go v1.48.7 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect @@ -43,7 +48,9 @@ require ( github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect github.com/jinzhu/copier v0.4.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect @@ -59,13 +66,13 @@ require ( github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - github.com/utxorpc/go-codegen v0.9.0 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.27.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index f891317..c0b0ff7 100644 --- a/go.sum +++ b/go.sum @@ -4,10 +4,18 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/blinklabs-io/gouroboros v0.96.0 h1:UYF2fwOo9MT6uHrpwHz+1krVib6SZ9sDkZ/C5KQxwdA= -github.com/blinklabs-io/gouroboros v0.96.0/go.mod h1:lfvV4sV5tNz/qkaLiR85pKpKqPlHfAa5wFhWGbgsXZ0= -github.com/blinklabs-io/ouroboros-mock v0.3.3 h1:c6jN9qcLzNQSVh3zjPE61gF33UkkRRIiNqSGBkZ10cY= -github.com/blinklabs-io/ouroboros-mock v0.3.3/go.mod h1:UXkR/8qA5w/WtkzffOIdXudgOndN99DEorgRwy4ynN8= +github.com/SundaeSwap-finance/kugo v1.0.5 h1:GWUbHkAIIMh1SmGMCw5r0rpOvriRsEoLpp5ofufWRvs= +github.com/SundaeSwap-finance/kugo v1.0.5/go.mod h1:jkNGTmwLRdUPKVzkOOQjxqkpPTDw5gJ2hkJi3zUF9tA= +github.com/SundaeSwap-finance/ogmigo/v6 v6.0.0-20231128043329-e8ced51013a1 h1:Lfw4vCNhm5Ik5wdbPsCK8k4gphhCB2/jtLxY5s/EifA= +github.com/SundaeSwap-finance/ogmigo/v6 v6.0.0-20231128043329-e8ced51013a1/go.mod h1:CsDGcgbkKoz6S4h0RJ30go7oXG+KhGE2KLhBpRFnEqA= +github.com/aws/aws-sdk-go v1.48.7 h1:gDcOhmkohlNk20j0uWpko5cLBbwSkB+xpkshQO45F7Y= +github.com/aws/aws-sdk-go v1.48.7/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/blinklabs-io/gouroboros v0.98.0 h1:Nngi16Y/lJiihLx/H7AMeDX1mqkqfka3xi1Lv6CC1zM= +github.com/blinklabs-io/gouroboros v0.98.0/go.mod h1:gU9pBcL1h584sVqYF8H7JJcB2x0n1HdWcmWP11VYxPE= +github.com/blinklabs-io/ouroboros-mock v0.3.4 h1:codPfiI5vLeD6YdhKL5VwYSzy2N3Dsgx6xjcLsqFaJQ= +github.com/blinklabs-io/ouroboros-mock v0.3.4/go.mod h1:e/wgG1ZYVenroN2XEMXy7DgEfdmP7KXVRHIQKuh8E/0= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= @@ -65,8 +73,14 @@ github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -100,6 +114,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE= +github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= @@ -132,12 +148,14 @@ github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk= github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o= +github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= +github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/utxorpc/go-codegen v0.9.0 h1:sV5vK/QiQs3WvKynRVo+U+5n2tmKU7yMz5y3rEX90Hg= -github.com/utxorpc/go-codegen v0.9.0/go.mod h1:+npvJc9wftIf8JMtWaRXxwjX0YlOCpNp1OlZVioNEO0= +github.com/utxorpc/go-codegen v0.10.0 h1:EVRNc136CThXbbGeYV/oIy6NKfcZGLH2QSRmLTPE6TQ= +github.com/utxorpc/go-codegen v0.10.0/go.mod h1:+npvJc9wftIf8JMtWaRXxwjX0YlOCpNp1OlZVioNEO0= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -150,8 +168,8 @@ golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= @@ -174,8 +192,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -183,8 +201,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -199,6 +217,7 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/input/chainsync/chainsync.go b/input/chainsync/chainsync.go index acf7a54..374d6a7 100644 --- a/input/chainsync/chainsync.go +++ b/input/chainsync/chainsync.go @@ -15,10 +15,17 @@ package chainsync import ( + "context" "encoding/hex" "fmt" + "log/slog" + "net/http" + "strings" "time" + "github.com/SundaeSwap-finance/kugo" + + "github.com/SundaeSwap-finance/ogmigo/v6/ouroboros/chainsync" "github.com/blinklabs-io/adder/event" "github.com/blinklabs-io/adder/plugin" @@ -59,6 +66,7 @@ type ChainSync struct { cursorCache []ocommon.Point dialAddress string dialFamily string + kupoUrl string } type ChainSyncStatus struct { @@ -318,7 +326,12 @@ func (c *ChainSync) handleRollForward( blockEvt := event.New("chainsync.block", time.Now(), NewBlockHeaderContext(v), NewBlockEvent(block, c.includeCbor)) c.eventChan <- blockEvt for t, transaction := range block.Transactions() { - txEvt := event.New("chainsync.transaction", time.Now(), NewTransactionContext(block, transaction, uint32(t), c.networkMagic), NewTransactionEvent(block, transaction, c.includeCbor)) + resolvedInputs, err := resolveTransactionInputs(transaction, c) + if err != nil { + return err + } + txEvt := event.New("chainsync.transaction", time.Now(), NewTransactionContext(block, transaction, uint32(t), c.networkMagic), + NewTransactionEvent(block, transaction, c.includeCbor, resolvedInputs)) c.eventChan <- txEvt } c.updateStatus(v.SlotNumber(), v.BlockNumber(), v.Hash(), tip.Point.Slot, hex.EncodeToString(tip.Point.Hash)) @@ -339,6 +352,10 @@ func (c *ChainSync) handleBlockFetchBlock( ) c.eventChan <- blockEvt for t, transaction := range block.Transactions() { + resolvedInputs, err := resolveTransactionInputs(transaction, c) + if err != nil { + return err + } txEvt := event.New( "chainsync.transaction", time.Now(), @@ -348,7 +365,7 @@ func (c *ChainSync) handleBlockFetchBlock( uint32(t), c.networkMagic, ), - NewTransactionEvent(block, transaction, c.includeCbor), + NewTransactionEvent(block, transaction, c.includeCbor, resolvedInputs), ) c.eventChan <- txEvt } @@ -403,3 +420,70 @@ func (c *ChainSync) updateStatus( c.statusUpdateFunc(*(c.status)) } } + +func getKupoClient(c *ChainSync) (*kugo.Client, error) { + k := kugo.New(kugo.WithEndpoint(c.kupoUrl)) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + healthUrl := strings.TrimRight(c.kupoUrl, "/") + "/v1/health" + req, err := http.NewRequestWithContext(ctx, "GET", healthUrl, nil) + if err != nil { + return nil, fmt.Errorf("failed to create health check request: %w", err) + } + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to perform health check: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("health check failed with status code: %d", resp.StatusCode) + } + + return k, nil +} + +// resolveTransactionInputs resolves the transaction inputs by using the +// Kupo client and fetching the corresponding transaction outputs. +func resolveTransactionInputs(transaction ledger.Transaction, c *ChainSync) ([]ledger.TransactionOutput, error) { + var resolvedInputs []ledger.TransactionOutput + + // Use Kupo client to resolve inputs if available + if c.kupoUrl != "" { + k, err := getKupoClient(c) + if err != nil { + return nil, fmt.Errorf("failed to get Kupo client: %w", err) + } + + for _, input := range transaction.Inputs() { + // Extract transaction ID and index from the input + txId := input.Id().String() + txIndex := int(input.Index()) + matches, err := k.Matches(context.Background(), + kugo.TxOut(chainsync.NewTxID(txId, txIndex)), + ) + if err != nil { + return nil, fmt.Errorf("Error fetching matches for input TxId: %s, Index: %d. Error: %w\n", txId, txIndex, err) + } + + if len(matches) == 0 { + slog.Info("No matches found for input TxId: %s, Index: %d, could be due to Kupo not in sync\n", txId, txIndex) + } else { + slog.Debug(fmt.Sprintf("Found matches %d for input TxId: %s, Index: %d\n", len(matches), txId, txIndex)) + for _, match := range matches { + slog.Debug(fmt.Sprintf("Match: %#v\n", match)) + transactionOutput, err := NewResolvedTransactionOutput(match) + if err != nil { + return nil, err + } + resolvedInputs = append(resolvedInputs, transactionOutput) + } + } + } + } + return resolvedInputs, nil +} diff --git a/input/chainsync/options.go b/input/chainsync/options.go index d382d3b..13e4ae7 100644 --- a/input/chainsync/options.go +++ b/input/chainsync/options.go @@ -107,3 +107,9 @@ func WithBulkMode(bulkMode bool) ChainSyncOptionFunc { c.bulkMode = bulkMode } } + +func WithKupoUrl(kupoUrl string) ChainSyncOptionFunc { + return func(c *ChainSync) { + c.kupoUrl = kupoUrl + } +} diff --git a/input/chainsync/plugin.go b/input/chainsync/plugin.go index 0dec06b..91d57b0 100644 --- a/input/chainsync/plugin.go +++ b/input/chainsync/plugin.go @@ -36,6 +36,7 @@ var cmdlineOptions struct { intersectPoint string includeCbor bool autoReconnect bool + kupoUrl string } func init() { @@ -118,6 +119,13 @@ func init() { DefaultValue: true, Dest: &(cmdlineOptions.autoReconnect), }, + { + Name: "kupo-url", + Type: plugin.PluginOptionTypeString, + Description: "kupo-url address", + DefaultValue: "", + Dest: &(cmdlineOptions.kupoUrl), + }, }, }, ) @@ -136,6 +144,7 @@ func NewFromCmdlineOptions() plugin.Plugin { WithBulkMode(cmdlineOptions.bulkMode), WithIncludeCbor(cmdlineOptions.includeCbor), WithAutoReconnect(cmdlineOptions.autoReconnect), + WithKupoUrl(cmdlineOptions.kupoUrl), } if cmdlineOptions.intersectPoint != "" { intersectPoints := []ocommon.Point{} diff --git a/input/chainsync/transactionOutput.go b/input/chainsync/transactionOutput.go new file mode 100644 index 0000000..66889c7 --- /dev/null +++ b/input/chainsync/transactionOutput.go @@ -0,0 +1,129 @@ +// Copyright 2024 Blink Labs Software +// +// 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. + +package chainsync + +import ( + "encoding/hex" + "fmt" + "log/slog" + + "github.com/SundaeSwap-finance/kugo" + "github.com/blinklabs-io/gouroboros/cbor" + "github.com/blinklabs-io/gouroboros/ledger" + "github.com/blinklabs-io/gouroboros/ledger/common" + utxorpc "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" +) + +// ResolvedTransactionOutput represents a concrete implementation of the TransactionOutput interface +type ResolvedTransactionOutput struct { + AddressField common.Address `json:"address"` + AmountField uint64 `json:"amount"` + AssetsField *common.MultiAsset[uint64] `json:"assets"` +} + +func ExtractAssetDetailsFromMatch(match kugo.Match) (common.MultiAsset[uint64], uint64, error) { + // Initialize the map that will store the assets + assetsMap := map[common.Blake2b224]map[cbor.ByteString]uint64{} + totalLovelace := uint64(0) + + // Iterate over all policies (asset types) in the Value map + for policyId, assets := range match.Value { + // Decode policyId if not ADA + policyIdBytes, err := hex.DecodeString(policyId) + if err != nil { + slog.Debug(fmt.Sprintf("PolicyId %s is not a valid hex string\n", policyId)) + policyIdBytes = []byte(policyId) + } + policyBlake := common.NewBlake2b224(policyIdBytes) + + // Prepare the map for this policy's assets + policyAssets := make(map[cbor.ByteString]uint64) + + // Iterate over all assets within this policyId + for assetName, amount := range assets { + // Check if this is the ADA (lovelace) asset + if policyId == "ada" && assetName == "lovelace" { + totalLovelace = amount.Uint64() + fmt.Printf("Found ADA (lovelace): %d\n", totalLovelace) + continue // Skip adding "lovelace" to assetsMap, as it is handled separately + } + + byteStringAssetName := cbor.NewByteString([]byte(assetName)) + assetAmount := amount.Uint64() + policyAssets[byteStringAssetName] = assetAmount + slog.Debug("Get policyId, assetName, assetAmount from match.Value") + slog.Debug(fmt.Sprintf("policyId: %s, assetName: %s, amount: %d\n", policyId, assetName, assetAmount)) + } + + // Only add non-empty policyAssets to the assetsMap + if len(policyAssets) > 0 { + assetsMap[policyBlake] = policyAssets + } + } + + assets := common.NewMultiAsset(assetsMap) + return assets, totalLovelace, nil +} + +func NewResolvedTransactionOutput(match kugo.Match) (ledger.TransactionOutput, error) { + addr, err := common.NewAddress(match.Address) + if err != nil { + return nil, fmt.Errorf("failed to create address from match.Address: %w", err) + } + + assets, amount, err := ExtractAssetDetailsFromMatch(match) + if err != nil { + return nil, fmt.Errorf("failed to extract asset details from match: %w", err) + } + + slog.Info(fmt.Sprintf("ResolvedTransactionOutput: address: %s, amount: %d, assets: %#v\n", addr, amount, assets)) + return &ResolvedTransactionOutput{ + AddressField: addr, + AmountField: amount, + AssetsField: &assets, + }, nil +} + +func (txOut ResolvedTransactionOutput) Address() common.Address { + return txOut.AddressField +} + +func (txOut ResolvedTransactionOutput) Amount() uint64 { + return txOut.AmountField +} + +func (txOut ResolvedTransactionOutput) Assets() *common.MultiAsset[uint64] { + return txOut.AssetsField +} + +func (txOut ResolvedTransactionOutput) Datum() *cbor.LazyValue { + // Placeholder for Datum serialization + return nil +} + +func (txOut ResolvedTransactionOutput) DatumHash() *common.Blake2b256 { + // Placeholder for DatumHash serialization + return nil +} + +func (txOut ResolvedTransactionOutput) Cbor() []byte { + // Placeholder for CBOR serialization + return []byte{} +} + +func (txOut ResolvedTransactionOutput) Utxorpc() *utxorpc.TxOutput { + // Placeholder for UTXO RPC representation + return &utxorpc.TxOutput{} +} diff --git a/input/chainsync/transactionOutput_test.go b/input/chainsync/transactionOutput_test.go new file mode 100644 index 0000000..a80273d --- /dev/null +++ b/input/chainsync/transactionOutput_test.go @@ -0,0 +1,67 @@ +// Copyright 2023 Blink Labs Software +// +// 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. + +package chainsync + +import ( + "encoding/json" + "testing" + + "github.com/blinklabs-io/gouroboros/cbor" + "github.com/blinklabs-io/gouroboros/ledger/common" + "github.com/stretchr/testify/assert" +) + +func TestResolvedTransactionOutput_MarshalJSON(t *testing.T) { + // Create the address, handle the error properly + addr, err := common.NewAddress("addr_test1wq5yehcpw4e3r32rltrww40e6ezdckr9v9l0ehptsxeynlg630pts") + if err != nil { + t.Fatalf("Failed to create address: %v", err) + } + + // Create assets for the resolved output + assets := common.NewMultiAsset(map[common.Blake2b224]map[cbor.ByteString]uint64{ + common.NewBlake2b224([]byte("policy1")): {cbor.NewByteString([]byte("TokenA")): 100}, + }) + + // Create the resolved transaction output + resolvedOutput := ResolvedTransactionOutput{ + AddressField: addr, + AmountField: 2000000, + AssetsField: &assets, + } + + // Marshal the resolved transaction output to JSON + jsonOutput, err := json.Marshal(resolvedOutput) + if err != nil { + t.Fatalf("Failed to marshal ResolvedTransactionOutput: %v", err) + } + + // Expected JSON string + expectedJSON := `{ + "address":"addr_test1wq5yehcpw4e3r32rltrww40e6ezdckr9v9l0ehptsxeynlg630pts", + "amount":2000000, + "assets":[ + { + "name":"TokenA", + "nameHex":"546f6b656e41", + "policyId":"706f6c69637931000000000000000000000000000000000000000000", + "fingerprint":"asset174ghjk04g2dpjv8zuw6s99rm09wmfvmgtfl84n", + "amount":100 + } + ] + }` + + assert.JSONEq(t, expectedJSON, string(jsonOutput)) +} diff --git a/input/chainsync/tx.go b/input/chainsync/tx.go index a36cd37..3812544 100644 --- a/input/chainsync/tx.go +++ b/input/chainsync/tx.go @@ -38,6 +38,7 @@ type TransactionEvent struct { Metadata *cbor.LazyValue `json:"metadata,omitempty"` Fee uint64 `json:"fee"` TTL uint64 `json:"ttl,omitempty"` + ResolvedInputs []ledger.TransactionOutput `json:"resolvedInputs,omitempty"` } func NewTransactionContext( @@ -60,13 +61,15 @@ func NewTransactionEvent( block ledger.Block, tx ledger.Transaction, includeCbor bool, + resolvedInputs []ledger.TransactionOutput, ) TransactionEvent { evt := TransactionEvent{ - Transaction: tx, - BlockHash: block.Hash(), - Inputs: tx.Inputs(), - Outputs: tx.Outputs(), - Fee: tx.Fee(), + Transaction: tx, + BlockHash: block.Hash(), + Inputs: tx.Inputs(), + Outputs: tx.Outputs(), + Fee: tx.Fee(), + ResolvedInputs: resolvedInputs, } if includeCbor { evt.TransactionCbor = tx.Cbor()