From e8a9716325f6f428a82ec732996759eb0d927451 Mon Sep 17 00:00:00 2001 From: Dmytro Steblyna <80773046+dimast-x@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:59:44 +0000 Subject: [PATCH 01/13] YNU-814: App Registry (#597) * **New Features** * App registry: register apps with owner signature and per-app approval setting * APIs to submit app versions and to list apps (filters: app_id, owner_wallet) with pagination * Optional owner signature path for creating app sessions when required * **Database** * App registry table added; app session records include version, status, and timestamps * **Tests & Docs** * Expanded tests and API docs for app registry and owner-sign flows --- .../api/app_session_v1/create_app_session.go | 47 +++ .../app_session_v1/create_app_session_test.go | 261 ++++++++++++++ clearnode/api/app_session_v1/interface.go | 3 + clearnode/api/app_session_v1/testing.go | 8 + clearnode/api/apps_v1/get_apps.go | 71 ++++ clearnode/api/apps_v1/get_apps_test.go | 143 ++++++++ clearnode/api/apps_v1/handler.go | 16 + clearnode/api/apps_v1/interface.go | 15 + clearnode/api/apps_v1/submit_app_version.go | 95 ++++++ .../api/apps_v1/submit_app_version_test.go | 235 +++++++++++++ clearnode/api/apps_v1/testing.go | 26 ++ clearnode/api/rpc_router.go | 8 +- .../20251222000000_initial_schema.sql | 17 +- clearnode/main.go | 2 +- clearnode/runtime.go | 1 + clearnode/store/database/app.go | 108 ++++++ clearnode/store/database/app_session.go | 46 +-- .../store/database/app_session_key_state.go | 2 +- clearnode/store/database/app_session_test.go | 2 +- clearnode/store/database/app_test.go | 317 ++++++++++++++++++ clearnode/store/database/database.go | 2 +- clearnode/store/database/interface.go | 11 + clearnode/store/database/testing.go | 4 +- clearnode/store/database/utils.go | 2 +- docs/api.yaml | 107 +++++- pkg/app/app_v1.go | 59 ++++ pkg/rpc/api.go | 40 ++- pkg/rpc/client.go | 22 ++ pkg/rpc/client_test.go | 135 ++++++++ pkg/rpc/methods.go | 5 + pkg/rpc/types.go | 27 ++ sdk/go/README.md | 24 ++ sdk/go/app_registry.go | 170 ++++++++++ sdk/ts/README.md | 24 ++ sdk/ts/src/app/packing.ts | 33 +- sdk/ts/src/client.ts | 80 ++++- sdk/ts/src/rpc/api.ts | 31 ++ sdk/ts/src/rpc/client.ts | 18 + sdk/ts/src/rpc/methods.ts | 5 + sdk/ts/src/rpc/types.ts | 30 ++ 40 files changed, 2215 insertions(+), 37 deletions(-) create mode 100644 clearnode/api/apps_v1/get_apps.go create mode 100644 clearnode/api/apps_v1/get_apps_test.go create mode 100644 clearnode/api/apps_v1/handler.go create mode 100644 clearnode/api/apps_v1/interface.go create mode 100644 clearnode/api/apps_v1/submit_app_version.go create mode 100644 clearnode/api/apps_v1/submit_app_version_test.go create mode 100644 clearnode/api/apps_v1/testing.go create mode 100644 clearnode/store/database/app.go create mode 100644 clearnode/store/database/app_test.go create mode 100644 pkg/app/app_v1.go create mode 100644 sdk/go/app_registry.go diff --git a/clearnode/api/app_session_v1/create_app_session.go b/clearnode/api/app_session_v1/create_app_session.go index d2e6043c8..ad3db3cfc 100644 --- a/clearnode/api/app_session_v1/create_app_session.go +++ b/clearnode/api/app_session_v1/create_app_session.go @@ -7,6 +7,7 @@ import ( "github.com/erc7824/nitrolite/pkg/app" "github.com/erc7824/nitrolite/pkg/log" "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/ethereum/go-ethereum/common/hexutil" ) // CreateAppSession creates a new application session between participants. @@ -35,6 +36,11 @@ func (h *Handler) CreateAppSession(c *rpc.Context) { return } + if reqPayload.Definition.Application == "" { + c.Fail(nil, "application id is required") + return + } + appDef, err := unmapAppDefinitionV1(reqPayload.Definition) if err != nil { c.Fail(err, "invalid app definition") @@ -101,6 +107,47 @@ func (h *Handler) CreateAppSession(c *rpc.Context) { } err = h.useStoreInTx(func(tx Store) error { + registeredApp, err := tx.GetApp(appDef.Application) + if err != nil { + return rpc.Errorf("failed to look up application: %v", err) + } + + // App must be registered regardless of CreationApprovalNotRequired flag. + if registeredApp == nil { + return rpc.Errorf("application %s is not registered", appDef.Application) + } + + if !registeredApp.App.CreationApprovalNotRequired { + if reqPayload.OwnerSig == "" { + return rpc.Errorf("owner_sig is required for this application") + } + + sigBytes, err := hexutil.Decode(reqPayload.OwnerSig) + if err != nil { + return rpc.Errorf("failed to decode signature: %v", err) + } + if len(sigBytes) == 0 { + return rpc.Errorf("empty owner_sig after decode") + } + + sigType := app.AppSessionSignerTypeV1(sigBytes[0]) + appSessionSignerValidator := app.NewAppSessionKeySigValidatorV1( + func(sessionKeyAddr string) (string, error) { + return tx.GetAppSessionKeyOwner(sessionKeyAddr, appSessionID) + }, + ) + recoveredOwnerWallet, err := appSessionSignerValidator.Recover(packedRequest, sigBytes) + if err != nil { + h.metrics.IncAppSessionUpdateSigValidation(appSessionID, sigType, false) + return rpc.Errorf("failed to recover user wallet: %v", err) + } + h.metrics.IncAppSessionUpdateSigValidation(appSessionID, sigType, true) + + if !strings.EqualFold(recoveredOwnerWallet, registeredApp.App.OwnerWallet) { + return rpc.Errorf("invalid owner signature: signer %s is not the app owner", recoveredOwnerWallet) + } + } + // Create app session with 0 allocations appSession := app.AppSessionV1{ SessionID: appSessionID, diff --git a/clearnode/api/app_session_v1/create_app_session_test.go b/clearnode/api/app_session_v1/create_app_session_test.go index 552644b47..40ede0537 100644 --- a/clearnode/api/app_session_v1/create_app_session_test.go +++ b/clearnode/api/app_session_v1/create_app_session_test.go @@ -76,6 +76,9 @@ func TestCreateAppSession_Success(t *testing.T) { } // Mock expectations + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", CreationApprovalNotRequired: true}, + }, nil).Once() mockStore.On("CreateAppSession", mock.MatchedBy(func(session any) bool { return true // Accept any app session for now })).Return(nil).Once() @@ -186,6 +189,9 @@ func TestCreateAppSession_QuorumWithMultipleSignatures(t *testing.T) { } // Mock expectations + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", CreationApprovalNotRequired: true}, + }, nil).Once() mockStore.On("CreateAppSession", mock.Anything).Return(nil).Once() // Create RPC context @@ -461,6 +467,9 @@ func TestCreateAppSession_SignatureFromNonParticipant(t *testing.T) { QuorumSigs: []string{sig}, } + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", CreationApprovalNotRequired: true}, + }, nil).Once() mockStore.On("CreateAppSession", mock.Anything).Return(nil).Once() // Create RPC context @@ -553,6 +562,9 @@ func TestCreateAppSession_QuorumNotMet(t *testing.T) { }, } + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", CreationApprovalNotRequired: true}, + }, nil).Once() mockStore.On("CreateAppSession", mock.Anything).Return(nil).Once() // Create RPC context @@ -641,6 +653,9 @@ func TestCreateAppSession_DuplicateSignatures(t *testing.T) { }, } + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", CreationApprovalNotRequired: true}, + }, nil).Once() mockStore.On("CreateAppSession", mock.Anything).Return(nil).Once() // Create RPC context @@ -707,6 +722,9 @@ func TestCreateAppSession_InvalidSignatureHex(t *testing.T) { QuorumSigs: []string{"not-valid-hex"}, // Invalid hex string } + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", CreationApprovalNotRequired: true}, + }, nil).Once() mockStore.On("CreateAppSession", mock.Anything).Return(nil).Once() // Create RPC context @@ -773,6 +791,9 @@ func TestCreateAppSession_SignatureRecoveryFailure(t *testing.T) { QuorumSigs: []string{"0xa100000000"}, } + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", CreationApprovalNotRequired: true}, + }, nil).Once() mockStore.On("CreateAppSession", mock.Anything).Return(nil).Once() // Create RPC context @@ -796,3 +817,243 @@ func TestCreateAppSession_SignatureRecoveryFailure(t *testing.T) { // Verify mocks were called mockStore.AssertExpectations(t) } + +func TestCreateAppSession_AppNotRegistered(t *testing.T) { + mockStore := new(MockStore) + + storeTxProvider := func(fn StoreTxHandler) error { + return fn(mockStore) + } + + mockSigner := NewMockSigner() + mockAssetStore := new(MockAssetStore) + mockStatePacker := new(MockStatePacker) + + handler := NewHandler( + storeTxProvider, + mockAssetStore, + mockSigner, + core.NewStateAdvancerV1(mockAssetStore), + mockStatePacker, + "0xnode", + metrics.NewNoopRuntimeMetricExporter(), + 32, 1024, 256, 16, + ) + + wallet1 := NewTestAppSessionWallet(t) + participant1 := wallet1.Address + + appDef := app.AppDefinitionV1{ + Application: "unregistered-app", + Participants: []app.AppParticipantV1{ + {WalletAddress: participant1, SignatureWeight: 1}, + }, + Quorum: 1, + Nonce: 12345, + } + sig1 := wallet1.SignCreateRequest(t, appDef, "") + + reqPayload := rpc.AppSessionsV1CreateAppSessionRequest{ + Definition: rpc.AppDefinitionV1{ + Application: "unregistered-app", + Participants: []rpc.AppParticipantV1{ + {WalletAddress: participant1, SignatureWeight: 1}, + }, + Quorum: 1, + Nonce: "12345", + }, + QuorumSigs: []string{sig1}, + } + + // GetApp returns nil (not found) + mockStore.On("GetApp", "unregistered-app").Return(nil, nil).Once() + + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, string(rpc.AppSessionsV1CreateAppSessionMethod), payload), + } + + handler.CreateAppSession(ctx) + + assert.NotNil(t, ctx.Response) + err = ctx.Response.Error() + require.Error(t, err) + assert.Contains(t, err.Error(), "not registered") + + mockStore.AssertExpectations(t) +} + +func TestCreateAppSession_OwnerSigRequired(t *testing.T) { + mockStore := new(MockStore) + + storeTxProvider := func(fn StoreTxHandler) error { + return fn(mockStore) + } + + mockSigner := NewMockSigner() + mockAssetStore := new(MockAssetStore) + mockStatePacker := new(MockStatePacker) + + handler := NewHandler( + storeTxProvider, + mockAssetStore, + mockSigner, + core.NewStateAdvancerV1(mockAssetStore), + mockStatePacker, + "0xnode", + metrics.NewNoopRuntimeMetricExporter(), + 32, 1024, 256, 16, + ) + + wallet1 := NewTestAppSessionWallet(t) + participant1 := wallet1.Address + + appDef := app.AppDefinitionV1{ + Application: "restricted-app", + Participants: []app.AppParticipantV1{ + {WalletAddress: participant1, SignatureWeight: 1}, + }, + Quorum: 1, + Nonce: 12345, + } + sig1 := wallet1.SignCreateRequest(t, appDef, "") + + reqPayload := rpc.AppSessionsV1CreateAppSessionRequest{ + Definition: rpc.AppDefinitionV1{ + Application: "restricted-app", + Participants: []rpc.AppParticipantV1{ + {WalletAddress: participant1, SignatureWeight: 1}, + }, + Quorum: 1, + Nonce: "12345", + }, + QuorumSigs: []string{sig1}, + // No OwnerSig provided + } + + // App requires approval (CreationApprovalNotRequired = false) + mockStore.On("GetApp", "restricted-app").Return(&app.AppInfoV1{ + App: app.AppV1{ + ID: "restricted-app", + OwnerWallet: "0xowneraddr", + CreationApprovalNotRequired: false, + }, + }, nil).Once() + + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, string(rpc.AppSessionsV1CreateAppSessionMethod), payload), + } + + handler.CreateAppSession(ctx) + + assert.NotNil(t, ctx.Response) + err = ctx.Response.Error() + require.Error(t, err) + assert.Contains(t, err.Error(), "owner_sig is required") + + mockStore.AssertExpectations(t) +} + +func TestCreateAppSession_OwnerSigSuccess(t *testing.T) { + mockStore := new(MockStore) + + storeTxProvider := func(fn StoreTxHandler) error { + return fn(mockStore) + } + + mockSigner := NewMockSigner() + mockAssetStore := new(MockAssetStore) + mockStatePacker := new(MockStatePacker) + + handler := NewHandler( + storeTxProvider, + mockAssetStore, + mockSigner, + core.NewStateAdvancerV1(mockAssetStore), + mockStatePacker, + "0xnode", + metrics.NewNoopRuntimeMetricExporter(), + 32, 1024, 256, 16, + ) + + // Create participant and owner wallets + wallet1 := NewTestAppSessionWallet(t) + ownerWallet := NewTestAppSessionWallet(t) + participant1 := wallet1.Address + + appDef := app.AppDefinitionV1{ + Application: "restricted-app", + Participants: []app.AppParticipantV1{ + {WalletAddress: participant1, SignatureWeight: 1}, + }, + Quorum: 1, + Nonce: 12345, + } + sessionData := `{"game": "poker"}` + + // Participant signs for quorum + sig1 := wallet1.SignCreateRequest(t, appDef, sessionData) + // Owner signs for approval + ownerSig := ownerWallet.SignCreateRequest(t, appDef, sessionData) + + reqPayload := rpc.AppSessionsV1CreateAppSessionRequest{ + Definition: rpc.AppDefinitionV1{ + Application: "restricted-app", + Participants: []rpc.AppParticipantV1{ + {WalletAddress: participant1, SignatureWeight: 1}, + }, + Quorum: 1, + Nonce: "12345", + }, + QuorumSigs: []string{sig1}, + SessionData: sessionData, + OwnerSig: ownerSig, + } + + // App requires approval — owner wallet matches + mockStore.On("GetApp", "restricted-app").Return(&app.AppInfoV1{ + App: app.AppV1{ + ID: "restricted-app", + OwnerWallet: ownerWallet.Address, + CreationApprovalNotRequired: false, + }, + }, nil).Once() + mockStore.On("CreateAppSession", mock.MatchedBy(func(session any) bool { + return true + })).Return(nil).Once() + + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, string(rpc.AppSessionsV1CreateAppSessionMethod), payload), + } + + handler.CreateAppSession(ctx) + + assert.NotNil(t, ctx.Response) + + if respErr := ctx.Response.Error(); respErr != nil { + t.Fatalf("Unexpected error response: %v", respErr) + } + + assert.Equal(t, rpc.MsgTypeResp, ctx.Response.Type) + + var resp rpc.AppSessionsV1CreateAppSessionResponse + err = ctx.Response.Payload.Translate(&resp) + require.NoError(t, err) + + assert.NotEmpty(t, resp.AppSessionID) + assert.Equal(t, "1", resp.Version) + assert.Equal(t, app.AppSessionStatusOpen.String(), resp.Status) + + mockStore.AssertExpectations(t) +} diff --git a/clearnode/api/app_session_v1/interface.go b/clearnode/api/app_session_v1/interface.go index 056a820b0..f83d988d7 100644 --- a/clearnode/api/app_session_v1/interface.go +++ b/clearnode/api/app_session_v1/interface.go @@ -8,6 +8,9 @@ import ( // Store defines the persistence layer interface for app session management. type Store interface { + // App registry operations + GetApp(appID string) (*app.AppInfoV1, error) + // App session operations CreateAppSession(session app.AppSessionV1) error GetAppSession(sessionID string) (*app.AppSessionV1, error) diff --git a/clearnode/api/app_session_v1/testing.go b/clearnode/api/app_session_v1/testing.go index dcae3b3ff..faf9bcfff 100644 --- a/clearnode/api/app_session_v1/testing.go +++ b/clearnode/api/app_session_v1/testing.go @@ -128,6 +128,14 @@ func (m *MockStore) GetAppSessionKeyOwner(sessionKey, appSessionId string) (stri return args.String(0), args.Error(1) } +func (m *MockStore) GetApp(appID string) (*app.AppInfoV1, error) { + args := m.Called(appID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*app.AppInfoV1), args.Error(1) +} + func (m *MockStore) ValidateChannelSessionKeyForAsset(wallet, sessionKey, asset, metadataHash string) (bool, error) { args := m.Called(wallet, sessionKey, asset, metadataHash) return args.Bool(0), args.Error(1) diff --git a/clearnode/api/apps_v1/get_apps.go b/clearnode/api/apps_v1/get_apps.go new file mode 100644 index 000000000..95d36e631 --- /dev/null +++ b/clearnode/api/apps_v1/get_apps.go @@ -0,0 +1,71 @@ +package apps_v1 + +import ( + "strconv" + + "github.com/erc7824/nitrolite/pkg/app" + "github.com/erc7824/nitrolite/pkg/core" + "github.com/erc7824/nitrolite/pkg/rpc" +) + +// GetApps retrieves registered applications with optional filtering. +func (h *Handler) GetApps(c *rpc.Context) { + var req rpc.AppsV1GetAppsRequest + if err := c.Request.Payload.Translate(&req); err != nil { + c.Fail(err, "failed to parse parameters") + return + } + + var paginationParams core.PaginationParams + if req.Pagination != nil { + paginationParams.Offset = req.Pagination.Offset + paginationParams.Limit = req.Pagination.Limit + paginationParams.Sort = req.Pagination.Sort + } + + apps, metadata, err := h.store.GetApps(req.AppID, req.OwnerWallet, &paginationParams) + if err != nil { + c.Fail(err, "failed to retrieve apps") + return + } + + response := rpc.AppsV1GetAppsResponse{ + Apps: make([]rpc.AppInfoV1, len(apps)), + Metadata: mapPaginationMetadataV1(metadata), + } + + for i, a := range apps { + response.Apps[i] = mapAppInfoV1(a) + } + + payload, err := rpc.NewPayload(response) + if err != nil { + c.Fail(err, "failed to create response") + return + } + + c.Succeed(c.Request.Method, payload) +} + +func mapAppInfoV1(info app.AppInfoV1) rpc.AppInfoV1 { + return rpc.AppInfoV1{ + AppV1: rpc.AppV1{ + ID: info.App.ID, + OwnerWallet: info.App.OwnerWallet, + Metadata: info.App.Metadata, + Version: strconv.FormatUint(info.App.Version, 10), + CreationApprovalNotRequired: info.App.CreationApprovalNotRequired, + }, + CreatedAt: strconv.FormatInt(info.CreatedAt.Unix(), 10), + UpdatedAt: strconv.FormatInt(info.UpdatedAt.Unix(), 10), + } +} + +func mapPaginationMetadataV1(meta core.PaginationMetadata) rpc.PaginationMetadataV1 { + return rpc.PaginationMetadataV1{ + Page: meta.Page, + PerPage: meta.PerPage, + TotalCount: meta.TotalCount, + PageCount: meta.PageCount, + } +} diff --git a/clearnode/api/apps_v1/get_apps_test.go b/clearnode/api/apps_v1/get_apps_test.go new file mode 100644 index 000000000..cf4c567f5 --- /dev/null +++ b/clearnode/api/apps_v1/get_apps_test.go @@ -0,0 +1,143 @@ +package apps_v1 + +import ( + "context" + "testing" + + "github.com/erc7824/nitrolite/pkg/app" + "github.com/erc7824/nitrolite/pkg/core" + "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetApps_Success(t *testing.T) { + mockStore := &MockStore{ + getAppsFn: func(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) { + return []app.AppInfoV1{ + {App: app.AppV1{ID: "app-1", OwnerWallet: "0x1111", Metadata: "0x00", Version: 1}}, + {App: app.AppV1{ID: "app-2", OwnerWallet: "0x2222", Metadata: "0x00", Version: 1}}, + }, core.PaginationMetadata{TotalCount: 2, Page: 1, PerPage: 50}, nil + }, + } + + handler := NewHandler(mockStore, 4096) + + reqPayload := rpc.AppsV1GetAppsRequest{} + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, string(rpc.AppsV1GetAppsMethod), payload), + } + + handler.GetApps(ctx) + + require.NotNil(t, ctx.Response) + require.NoError(t, ctx.Response.Error()) + + var resp rpc.AppsV1GetAppsResponse + require.NoError(t, ctx.Response.Payload.Translate(&resp)) + assert.Len(t, resp.Apps, 2) + assert.Equal(t, "app-1", resp.Apps[0].ID) + assert.Equal(t, "app-2", resp.Apps[1].ID) + assert.Equal(t, uint32(2), resp.Metadata.TotalCount) +} + +func TestGetApps_EmptyResults(t *testing.T) { + mockStore := &MockStore{ + getAppsFn: func(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) { + return []app.AppInfoV1{}, core.PaginationMetadata{TotalCount: 0}, nil + }, + } + + handler := NewHandler(mockStore, 4096) + + reqPayload := rpc.AppsV1GetAppsRequest{} + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, string(rpc.AppsV1GetAppsMethod), payload), + } + + handler.GetApps(ctx) + + require.NotNil(t, ctx.Response) + require.NoError(t, ctx.Response.Error()) + + var resp rpc.AppsV1GetAppsResponse + require.NoError(t, ctx.Response.Payload.Translate(&resp)) + assert.Empty(t, resp.Apps) + assert.Equal(t, uint32(0), resp.Metadata.TotalCount) +} + +func TestGetApps_FilterByAppID(t *testing.T) { + mockStore := &MockStore{ + getAppsFn: func(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) { + require.NotNil(t, appID) + assert.Equal(t, "app-1", *appID) + return []app.AppInfoV1{ + {App: app.AppV1{ID: "app-1", OwnerWallet: "0x1111", Version: 1}}, + }, core.PaginationMetadata{TotalCount: 1, Page: 1, PerPage: 50}, nil + }, + } + + handler := NewHandler(mockStore, 4096) + + aid := "app-1" + reqPayload := rpc.AppsV1GetAppsRequest{AppID: &aid} + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, string(rpc.AppsV1GetAppsMethod), payload), + } + + handler.GetApps(ctx) + + require.NotNil(t, ctx.Response) + require.NoError(t, ctx.Response.Error()) + + var resp rpc.AppsV1GetAppsResponse + require.NoError(t, ctx.Response.Payload.Translate(&resp)) + assert.Len(t, resp.Apps, 1) + assert.Equal(t, "app-1", resp.Apps[0].ID) +} + +func TestGetApps_FilterByOwnerWallet(t *testing.T) { + mockStore := &MockStore{ + getAppsFn: func(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) { + require.NotNil(t, ownerWallet) + assert.Equal(t, "0x1111", *ownerWallet) + return []app.AppInfoV1{ + {App: app.AppV1{ID: "app-1", OwnerWallet: "0x1111", Version: 1}}, + }, core.PaginationMetadata{TotalCount: 1, Page: 1, PerPage: 50}, nil + }, + } + + handler := NewHandler(mockStore, 4096) + + owner := "0x1111" + reqPayload := rpc.AppsV1GetAppsRequest{OwnerWallet: &owner} + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, string(rpc.AppsV1GetAppsMethod), payload), + } + + handler.GetApps(ctx) + + require.NotNil(t, ctx.Response) + require.NoError(t, ctx.Response.Error()) + + var resp rpc.AppsV1GetAppsResponse + require.NoError(t, ctx.Response.Payload.Translate(&resp)) + assert.Len(t, resp.Apps, 1) + assert.Equal(t, "0x1111", resp.Apps[0].OwnerWallet) +} diff --git a/clearnode/api/apps_v1/handler.go b/clearnode/api/apps_v1/handler.go new file mode 100644 index 000000000..16cc5804e --- /dev/null +++ b/clearnode/api/apps_v1/handler.go @@ -0,0 +1,16 @@ +package apps_v1 + +// Handler manages app registry operations and provides RPC endpoints. +type Handler struct { + store Store + + maxAppMetadataLen int +} + +// NewHandler creates a new Handler instance with the provided dependencies. +func NewHandler(store Store, maxAppMetadataLen int) *Handler { + return &Handler{ + store: store, + maxAppMetadataLen: maxAppMetadataLen, + } +} diff --git a/clearnode/api/apps_v1/interface.go b/clearnode/api/apps_v1/interface.go new file mode 100644 index 000000000..94dfdbd73 --- /dev/null +++ b/clearnode/api/apps_v1/interface.go @@ -0,0 +1,15 @@ +package apps_v1 + +import ( + "github.com/erc7824/nitrolite/pkg/app" + "github.com/erc7824/nitrolite/pkg/core" +) + +// Store defines the persistence layer interface for app registry operations. +type Store interface { + // CreateApp registers a new application. Returns an error if the app ID already exists. + CreateApp(entry app.AppV1) error + + // GetApps retrieves applications with optional filtering. + GetApps(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) +} diff --git a/clearnode/api/apps_v1/submit_app_version.go b/clearnode/api/apps_v1/submit_app_version.go new file mode 100644 index 000000000..c1b78984e --- /dev/null +++ b/clearnode/api/apps_v1/submit_app_version.go @@ -0,0 +1,95 @@ +package apps_v1 + +import ( + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/erc7824/nitrolite/pkg/app" + "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/erc7824/nitrolite/pkg/sign" +) + +// SubmitAppVersion updates an entry in the app registry. +func (h *Handler) SubmitAppVersion(c *rpc.Context) { + var req rpc.AppsV1SubmitAppVersionRequest + if err := c.Request.Payload.Translate(&req); err != nil { + c.Fail(err, "failed to parse parameters") + return + } + + if !app.AppIDV1Regex.MatchString(req.App.ID) { + c.Fail(rpc.Errorf("invalid app ID: should match regex %s", app.AppIDV1Regex.String()), "") + return + } + if req.App.OwnerWallet == "" { + c.Fail(nil, "owner_wallet is required") + return + } + if req.OwnerSig == "" { + c.Fail(nil, "owner_sig is required") + return + } + if len(req.App.Metadata) > h.maxAppMetadataLen { + c.Fail(rpc.Errorf("metadata exceeds maximum length of %d characters", h.maxAppMetadataLen), "") + return + } + + version, err := strconv.ParseUint(req.App.Version, 10, 64) + if err != nil { + c.Fail(err, "invalid version") + return + } + + // Only creation (version 1) is supported for now + if version != 1 { + c.Fail(nil, "only version 1 (creation) is currently supported") + return + } + + appEntry := app.AppV1{ + ID: strings.ToLower(req.App.ID), + OwnerWallet: strings.ToLower(req.App.OwnerWallet), + Metadata: req.App.Metadata, + Version: version, + CreationApprovalNotRequired: req.App.CreationApprovalNotRequired, + } + + packedApp, err := app.PackAppV1(appEntry) + if err != nil { + c.Fail(rpc.Errorf("failed to pack app data: %v", err), "") + return + } + + sigBytes, err := hexutil.Decode(req.OwnerSig) + if err != nil { + c.Fail(rpc.Errorf("failed to decode owner signature: %v", err), "") + return + } + + sigValidator, err := sign.NewSigValidator(sign.TypeEthereumMsg) + if err != nil { + c.Fail(rpc.Errorf("failed to create signature validator: %v", err), "") + return + } + + if err := sigValidator.Verify(appEntry.OwnerWallet, packedApp, sigBytes); err != nil { + c.Fail(rpc.Errorf("invalid owner signature: %v", err), "") + return + } + + if err := h.store.CreateApp(appEntry); err != nil { + c.Fail(err, "failed to create app") + return + } + + resp := rpc.AppsV1SubmitAppVersionResponse{} + payload, err := rpc.NewPayload(resp) + if err != nil { + c.Fail(err, "failed to create response") + return + } + + c.Succeed(c.Request.Method, payload) +} diff --git a/clearnode/api/apps_v1/submit_app_version_test.go b/clearnode/api/apps_v1/submit_app_version_test.go new file mode 100644 index 000000000..1ade9743e --- /dev/null +++ b/clearnode/api/apps_v1/submit_app_version_test.go @@ -0,0 +1,235 @@ +package apps_v1 + +import ( + "context" + "strings" + "testing" + + "github.com/erc7824/nitrolite/pkg/app" + "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/erc7824/nitrolite/pkg/sign" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// testOwnerWallet generates a real ECDSA key pair, signs the packed app data, and returns +// the wallet address (lowercase hex), the hex-encoded signature, and the signer. +func testOwnerWallet(t *testing.T, appEntry app.AppV1) (address string, sig string) { + t.Helper() + + key, err := crypto.GenerateKey() + require.NoError(t, err) + + addr := strings.ToLower(crypto.PubkeyToAddress(key.PublicKey).Hex()) + privKeyHex := hexutil.Encode(crypto.FromECDSA(key)) + + signer, err := sign.NewEthereumMsgSigner(privKeyHex) + require.NoError(t, err) + + // Update the entry with the real address for packing + appEntry.OwnerWallet = addr + packed, err := app.PackAppV1(appEntry) + require.NoError(t, err) + + sigBytes, err := signer.Sign(packed) + require.NoError(t, err) + + return addr, hexutil.Encode(sigBytes) +} + +func newHandlerWithDefaults(store Store) *Handler { + return NewHandler(store, 4096) +} + +func TestSubmitAppVersion_Success(t *testing.T) { + appEntry := app.AppV1{ + ID: "test-app", + Metadata: "0x0000000000000000000000000000000000000000000000000000000000000000", + Version: 1, + } + + addr, sig := testOwnerWallet(t, appEntry) + + mockStore := &MockStore{ + createAppFn: func(entry app.AppV1) error { + assert.Equal(t, "test-app", entry.ID) + assert.Equal(t, addr, entry.OwnerWallet) + return nil + }, + } + + handler := newHandlerWithDefaults(mockStore) + + reqPayload := rpc.AppsV1SubmitAppVersionRequest{ + App: rpc.AppV1{ + ID: "test-app", + OwnerWallet: addr, + Metadata: "0x0000000000000000000000000000000000000000000000000000000000000000", + Version: "1", + }, + OwnerSig: sig, + } + + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, string(rpc.AppsV1SubmitAppVersionMethod), payload), + } + + handler.SubmitAppVersion(ctx) + + require.NotNil(t, ctx.Response) + require.NoError(t, ctx.Response.Error()) +} + +func TestSubmitAppVersion_MissingOwnerWallet(t *testing.T) { + mockStore := &MockStore{} + handler := newHandlerWithDefaults(mockStore) + + reqPayload := rpc.AppsV1SubmitAppVersionRequest{ + App: rpc.AppV1{ + ID: "test-app", + Metadata: "0x00", + Version: "1", + }, + OwnerSig: "0xdeadbeef", + } + + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, string(rpc.AppsV1SubmitAppVersionMethod), payload), + } + + handler.SubmitAppVersion(ctx) + + require.NotNil(t, ctx.Response) + err = ctx.Response.Error() + require.Error(t, err) + assert.Contains(t, err.Error(), "owner_wallet") +} + +func TestSubmitAppVersion_MissingOwnerSig(t *testing.T) { + mockStore := &MockStore{} + handler := newHandlerWithDefaults(mockStore) + + reqPayload := rpc.AppsV1SubmitAppVersionRequest{ + App: rpc.AppV1{ + ID: "test-app", + OwnerWallet: "0x1111111111111111111111111111111111111111", + Metadata: "0x00", + Version: "1", + }, + } + + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, string(rpc.AppsV1SubmitAppVersionMethod), payload), + } + + handler.SubmitAppVersion(ctx) + + require.NotNil(t, ctx.Response) + err = ctx.Response.Error() + require.Error(t, err) + assert.Contains(t, err.Error(), "owner_sig") +} + +func TestSubmitAppVersion_InvalidAppID(t *testing.T) { + mockStore := &MockStore{} + handler := newHandlerWithDefaults(mockStore) + + reqPayload := rpc.AppsV1SubmitAppVersionRequest{ + App: rpc.AppV1{ + ID: "INVALID_APP!!", // doesn't match regex + OwnerWallet: "0x1111111111111111111111111111111111111111", + Metadata: "0x00", + Version: "1", + }, + OwnerSig: "0xdeadbeef", + } + + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, string(rpc.AppsV1SubmitAppVersionMethod), payload), + } + + handler.SubmitAppVersion(ctx) + + require.NotNil(t, ctx.Response) + err = ctx.Response.Error() + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid app ID") +} + +func TestSubmitAppVersion_InvalidVersion(t *testing.T) { + mockStore := &MockStore{} + handler := newHandlerWithDefaults(mockStore) + + reqPayload := rpc.AppsV1SubmitAppVersionRequest{ + App: rpc.AppV1{ + ID: "test-app", + OwnerWallet: "0x1111111111111111111111111111111111111111", + Metadata: "0x00", + Version: "2", // Only version 1 is supported + }, + OwnerSig: "0xdeadbeef", + } + + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, string(rpc.AppsV1SubmitAppVersionMethod), payload), + } + + handler.SubmitAppVersion(ctx) + + require.NotNil(t, ctx.Response) + err = ctx.Response.Error() + require.Error(t, err) + assert.Contains(t, err.Error(), "version 1") +} + +func TestSubmitAppVersion_InvalidSignature(t *testing.T) { + mockStore := &MockStore{} + handler := newHandlerWithDefaults(mockStore) + + reqPayload := rpc.AppsV1SubmitAppVersionRequest{ + App: rpc.AppV1{ + ID: "test-app", + OwnerWallet: "0x1111111111111111111111111111111111111111", + Metadata: "0x0000000000000000000000000000000000000000000000000000000000000000", + Version: "1", + }, + OwnerSig: "0x" + strings.Repeat("ab", 65), // valid hex, wrong signature + } + + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, string(rpc.AppsV1SubmitAppVersionMethod), payload), + } + + handler.SubmitAppVersion(ctx) + + require.NotNil(t, ctx.Response) + err = ctx.Response.Error() + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid owner signature") +} diff --git a/clearnode/api/apps_v1/testing.go b/clearnode/api/apps_v1/testing.go new file mode 100644 index 000000000..6a3c5a5a0 --- /dev/null +++ b/clearnode/api/apps_v1/testing.go @@ -0,0 +1,26 @@ +package apps_v1 + +import ( + "github.com/erc7824/nitrolite/pkg/app" + "github.com/erc7824/nitrolite/pkg/core" +) + +// MockStore implements the Store interface for testing. +type MockStore struct { + createAppFn func(entry app.AppV1) error + getAppsFn func(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) +} + +func (m *MockStore) CreateApp(entry app.AppV1) error { + if m.createAppFn != nil { + return m.createAppFn(entry) + } + return nil +} + +func (m *MockStore) GetApps(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) { + if m.getAppsFn != nil { + return m.getAppsFn(appID, ownerWallet, pagination) + } + return nil, core.PaginationMetadata{}, nil +} diff --git a/clearnode/api/rpc_router.go b/clearnode/api/rpc_router.go index 220b55fea..1a03dff6e 100644 --- a/clearnode/api/rpc_router.go +++ b/clearnode/api/rpc_router.go @@ -4,6 +4,7 @@ import ( "time" "github.com/erc7824/nitrolite/clearnode/api/app_session_v1" + "github.com/erc7824/nitrolite/clearnode/api/apps_v1" "github.com/erc7824/nitrolite/clearnode/api/channel_v1" "github.com/erc7824/nitrolite/clearnode/api/node_v1" "github.com/erc7824/nitrolite/clearnode/api/user_v1" @@ -31,7 +32,7 @@ func NewRPCRouter( memoryStore memory.MemoryStore, runtimeMetrics metrics.RuntimeMetricExporter, logger log.Logger, - maxParticipants, maxSessionDataLen, maxRebalanceSignedUpdates, maxSessionKeyIDs int, + maxParticipants, maxSessionDataLen, maxAppMetadataLen, maxRebalanceSignedUpdates, maxSessionKeyIDs int, ) *RPCRouter { r := &RPCRouter{ Node: node, @@ -75,6 +76,7 @@ func NewRPCRouter( channelV1Handler := channel_v1.NewHandler(useChannelV1StoreInTx, memoryStore, nodeChannelSigner, stateAdvancer, statePacker, nodeAddress, minChallenge, runtimeMetrics, maxSessionKeyIDs) appSessionV1Handler := app_session_v1.NewHandler(useAppSessionV1StoreInTx, memoryStore, signer, stateAdvancer, statePacker, nodeAddress, runtimeMetrics, maxParticipants, maxSessionDataLen, maxSessionKeyIDs, maxRebalanceSignedUpdates) + appsV1Handler := apps_v1.NewHandler(dbStore, maxAppMetadataLen) nodeV1Handler := node_v1.NewHandler(memoryStore, nodeAddress, nodeVersion) userV1Handler := user_v1.NewHandler(dbStore) @@ -105,6 +107,10 @@ func NewRPCRouter( nodeV1Group.Handle(rpc.NodeV1GetAssetsMethod.String(), nodeV1Handler.GetAssets) nodeV1Group.Handle(rpc.NodeV1GetConfigMethod.String(), nodeV1Handler.GetConfig) + appsV1Group := r.Node.NewGroup(rpc.AppsV1Group.String()) + appsV1Group.Handle(rpc.AppsV1GetAppsMethod.String(), appsV1Handler.GetApps) + appsV1Group.Handle(rpc.AppsV1SubmitAppVersionMethod.String(), appsV1Handler.SubmitAppVersion) + userV1Group := r.Node.NewGroup(rpc.UserV1Group.String()) userV1Group.Handle(rpc.UserV1GetBalancesMethod.String(), userV1Handler.GetBalances) userV1Group.Handle(rpc.UserV1GetTransactionsMethod.String(), userV1Handler.GetTransactions) diff --git a/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql b/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql index f84923a28..5fb7e073f 100644 --- a/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql +++ b/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql @@ -93,10 +93,23 @@ CREATE INDEX idx_transactions_from_to_type ON transactions(from_account, to_acco CREATE INDEX idx_transactions_from_comp ON transactions(from_account, asset_symbol, created_at DESC); CREATE INDEX idx_transactions_to_comp ON transactions(to_account, asset_symbol, created_at DESC); +-- Application registry +CREATE TABLE apps_v1 ( + id VARCHAR(66) PRIMARY KEY, + owner_wallet CHAR(42) NOT NULL, + metadata TEXT NOT NULL, + version NUMERIC(20,0) NOT NULL DEFAULT 1, + creation_approval_not_required BOOLEAN NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_apps_v1_owner_wallet ON apps_v1(owner_wallet); + -- App Sessions table: Application sessions CREATE TABLE app_sessions_v1 ( id CHAR(66) PRIMARY KEY, - application VARCHAR NOT NULL, + application_id VARCHAR NOT NULL, nonce NUMERIC(20,0) NOT NULL, session_data TEXT NOT NULL, quorum SMALLINT NOT NULL DEFAULT 100, @@ -275,6 +288,8 @@ DROP TABLE IF EXISTS app_session_participants_v1; DROP INDEX IF EXISTS idx_app_sessions_v1_status; DROP INDEX IF EXISTS idx_app_sessions_v1_application; DROP TABLE IF EXISTS app_sessions_v1; +DROP INDEX IF EXISTS idx_apps_v1_owner_wallet; +DROP TABLE IF EXISTS apps_v1; DROP INDEX IF EXISTS idx_transactions_to_comp; DROP INDEX IF EXISTS idx_transactions_from_comp; DROP INDEX IF EXISTS idx_transactions_from_to_type; diff --git a/clearnode/main.go b/clearnode/main.go index d34fdad40..84adf725a 100644 --- a/clearnode/main.go +++ b/clearnode/main.go @@ -33,7 +33,7 @@ func main() { vl := bb.ValidationLimits api.NewRPCRouter(bb.NodeVersion, bb.ChannelMinChallengeDuration, bb.RpcNode, bb.StateSigner, bb.DbStore, bb.MemoryStore, bb.RuntimeMetrics, bb.Logger, - vl.MaxParticipants, vl.MaxSessionDataLen, vl.MaxSignedUpdates, vl.MaxSessionKeyIDs) + vl.MaxParticipants, vl.MaxSessionDataLen, vl.MaxAppMetadataLen, vl.MaxSignedUpdates, vl.MaxSessionKeyIDs) rpcListenAddr := ":7824" rpcListenEndpoint := "/ws" diff --git a/clearnode/runtime.go b/clearnode/runtime.go index d58ec8189..fdff48027 100644 --- a/clearnode/runtime.go +++ b/clearnode/runtime.go @@ -72,6 +72,7 @@ type Config struct { type ValidationLimits struct { MaxParticipants int `yaml:"max_participants" env:"CLEARNODE_MAX_PARTICIPANTS" env-default:"32"` MaxSessionDataLen int `yaml:"max_session_data_len" env:"CLEARNODE_MAX_SESSION_DATA_LEN" env-default:"1024"` + MaxAppMetadataLen int `yaml:"max_app_metadata_len" env:"CLEARNODE_MAX_APP_METADATA_LEN" env-default:"1024"` MaxSessionKeyIDs int `yaml:"max_session_key_ids" env:"CLEARNODE_MAX_SESSION_KEY_IDS" env-default:"256"` MaxSignedUpdates int `yaml:"max_signed_updates" env:"CLEARNODE_MAX_SIGNED_UPDATES" env-default:"0"` } diff --git a/clearnode/store/database/app.go b/clearnode/store/database/app.go new file mode 100644 index 000000000..cd0eed5ea --- /dev/null +++ b/clearnode/store/database/app.go @@ -0,0 +1,108 @@ +package database + +import ( + "fmt" + "strings" + "time" + + "github.com/erc7824/nitrolite/pkg/app" + "github.com/erc7824/nitrolite/pkg/core" + "gorm.io/gorm" +) + +// AppV1 represents an application registry entry in the database. +type AppV1 struct { + ID string `gorm:"primaryKey"` + OwnerWallet string `gorm:"column:owner_wallet;not null"` + Metadata string `gorm:"column:metadata;type:text;not null"` + Version uint64 `gorm:"column:version;default:1"` + CreationApprovalNotRequired bool `gorm:"column:creation_approval_not_required"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (AppV1) TableName() string { + return "apps_v1" +} + +// CreateApp registers a new application. Returns an error if the app ID already exists. +func (s *DBStore) CreateApp(entry app.AppV1) error { + dbApp := AppV1{ + ID: strings.ToLower(entry.ID), + OwnerWallet: strings.ToLower(entry.OwnerWallet), + Metadata: entry.Metadata, + Version: entry.Version, + CreationApprovalNotRequired: entry.CreationApprovalNotRequired, + } + + if err := s.db.Create(&dbApp).Error; err != nil { + return fmt.Errorf("failed to create app: %w", err) + } + + return nil +} + +// GetApp retrieves a single application by ID. Returns nil if not found. +func (s *DBStore) GetApp(appID string) (*app.AppInfoV1, error) { + var dbApp AppV1 + err := s.db.Where("id = ?", strings.ToLower(appID)).First(&dbApp).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, fmt.Errorf("failed to get app: %w", err) + } + + result := databaseAppToCore(&dbApp) + return &result, nil +} + +// GetApps retrieves applications with optional filtering by app ID, owner wallet, and pagination. +func (s *DBStore) GetApps(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) { + query := s.db.Model(&AppV1{}) + + if appID != nil && *appID != "" { + query = query.Where("id = ?", strings.ToLower(*appID)) + } + + if ownerWallet != nil && *ownerWallet != "" { + query = query.Where("owner_wallet = ?", strings.ToLower(*ownerWallet)) + } + + var totalCount int64 + if err := query.Count(&totalCount).Error; err != nil { + return nil, core.PaginationMetadata{}, fmt.Errorf("failed to count apps: %w", err) + } + + offset, limit := pagination.GetOffsetAndLimit(DefaultLimit, MaxLimit) + + query = query.Order("created_at DESC").Offset(int(offset)).Limit(int(limit)) + + var dbApps []AppV1 + if err := query.Find(&dbApps).Error; err != nil { + return nil, core.PaginationMetadata{}, fmt.Errorf("failed to get apps: %w", err) + } + + apps := make([]app.AppInfoV1, len(dbApps)) + for i, dbApp := range dbApps { + apps[i] = databaseAppToCore(&dbApp) + } + + metadata := calculatePaginationMetadata(totalCount, offset, limit) + + return apps, metadata, nil +} + +func databaseAppToCore(dbApp *AppV1) app.AppInfoV1 { + return app.AppInfoV1{ + App: app.AppV1{ + ID: dbApp.ID, + OwnerWallet: dbApp.OwnerWallet, + Metadata: dbApp.Metadata, + Version: dbApp.Version, + CreationApprovalNotRequired: dbApp.CreationApprovalNotRequired, + }, + CreatedAt: dbApp.CreatedAt, + UpdatedAt: dbApp.UpdatedAt, + } +} diff --git a/clearnode/store/database/app_session.go b/clearnode/store/database/app_session.go index ef93afd9f..d82d6d7c5 100644 --- a/clearnode/store/database/app_session.go +++ b/clearnode/store/database/app_session.go @@ -12,16 +12,16 @@ import ( // AppSessionV1 represents a virtual payment application session between participants type AppSessionV1 struct { - ID string `gorm:"primaryKey"` - Application string `gorm:"column:application;not null"` - Nonce uint64 `gorm:"column:nonce;not null"` - Participants []AppParticipantV1 `gorm:"foreignKey:AppSessionID;references:ID"` - SessionData string `gorm:"column:session_data;type:text;not null"` - Quorum uint8 `gorm:"column:quorum;default:100"` - Version uint64 `gorm:"column:version;default:1"` - Status app.AppSessionStatus `gorm:"column:status;not null"` - CreatedAt time.Time - UpdatedAt time.Time + ID string `gorm:"primaryKey"` + ApplicationID string `gorm:"column:application_id;not null"` + Nonce uint64 `gorm:"column:nonce;not null"` + Participants []AppParticipantV1 `gorm:"foreignKey:AppSessionID;references:ID"` + SessionData string `gorm:"column:session_data;type:text;not null"` + Quorum uint8 `gorm:"column:quorum;default:100"` + Version uint64 `gorm:"column:version;default:1"` + Status app.AppSessionStatus `gorm:"column:status;not null"` + CreatedAt time.Time + UpdatedAt time.Time } func (AppSessionV1) TableName() string { @@ -51,16 +51,16 @@ func (s *DBStore) CreateAppSession(session app.AppSessionV1) error { } dbSession := AppSessionV1{ - ID: strings.ToLower(session.SessionID), - Application: session.Application, - Nonce: session.Nonce, - Participants: participants, - SessionData: session.SessionData, - Quorum: session.Quorum, - Version: session.Version, - Status: session.Status, - CreatedAt: session.CreatedAt, - UpdatedAt: session.UpdatedAt, + ID: strings.ToLower(session.SessionID), + ApplicationID: session.Application, + Nonce: session.Nonce, + Participants: participants, + SessionData: session.SessionData, + Quorum: session.Quorum, + Version: session.Version, + Status: session.Status, + CreatedAt: session.CreatedAt, + UpdatedAt: session.UpdatedAt, } if err := s.db.Create(&dbSession).Error; err != nil { @@ -135,7 +135,7 @@ func (s *DBStore) GetAppSessions(appSessionID *string, participant *string, stat // AppSessionCount holds the result of a COUNT() GROUP BY query on app sessions. type AppSessionCount struct { - Application string `gorm:"column:application"` + Application string `gorm:"column:application_id"` Status app.AppSessionStatus `gorm:"column:status"` Count uint64 `gorm:"column:count"` } @@ -144,8 +144,8 @@ type AppSessionCount struct { func (s *DBStore) CountAppSessionsByStatus() ([]AppSessionCount, error) { var results []AppSessionCount err := s.db.Model(&AppSessionV1{}). - Select("application, status, COUNT(id) as count"). - Group("application, status"). + Select("application_id, status, COUNT(id) as count"). + Group("application_id, status"). Find(&results).Error if err != nil { return nil, fmt.Errorf("failed to count app sessions: %w", err) diff --git a/clearnode/store/database/app_session_key_state.go b/clearnode/store/database/app_session_key_state.go index 2733f2fc9..aa5e3fbae 100644 --- a/clearnode/store/database/app_session_key_state.go +++ b/clearnode/store/database/app_session_key_state.go @@ -194,7 +194,7 @@ func (s *DBStore) GetAppSessionKeyOwner(sessionKey, appSessionId string) (string appSessionId = strings.ToLower(appSessionId) // Subquery to get the application ID from the app session - appSubQuery := s.db.Model(&AppSessionV1{}).Select("application").Where("id = ?", appSessionId) + appSubQuery := s.db.Model(&AppSessionV1{}).Select("application_id").Where("id = ?", appSessionId) maxVersionSubQ := s.db.Model(&AppSessionKeyStateV1{}). Select("MAX(version)"). diff --git a/clearnode/store/database/app_session_test.go b/clearnode/store/database/app_session_test.go index c0b9437d7..71948e093 100644 --- a/clearnode/store/database/app_session_test.go +++ b/clearnode/store/database/app_session_test.go @@ -54,7 +54,7 @@ func TestDBStore_CreateAppSession(t *testing.T) { require.NoError(t, err) assert.Equal(t, "session123", dbSession.ID) - assert.Equal(t, "poker", dbSession.Application) + assert.Equal(t, "poker", dbSession.ApplicationID) assert.Equal(t, uint64(1), dbSession.Nonce) assert.Equal(t, `{"state": "active"}`, dbSession.SessionData) assert.Equal(t, uint8(100), dbSession.Quorum) diff --git a/clearnode/store/database/app_test.go b/clearnode/store/database/app_test.go new file mode 100644 index 000000000..e392bc1d6 --- /dev/null +++ b/clearnode/store/database/app_test.go @@ -0,0 +1,317 @@ +package database + +import ( + "testing" + "time" + + "github.com/erc7824/nitrolite/pkg/app" + "github.com/erc7824/nitrolite/pkg/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAppV1_TableName(t *testing.T) { + a := AppV1{} + assert.Equal(t, "apps_v1", a.TableName()) +} + +func TestDBStore_CreateApp(t *testing.T) { + t.Run("Success", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + + store := NewDBStore(db) + + entry := app.AppV1{ + ID: "test-app", + OwnerWallet: "0x1111111111111111111111111111111111111111", + Metadata: "0xabcdef", + Version: 1, + CreationApprovalNotRequired: true, + } + + err := store.CreateApp(entry) + require.NoError(t, err) + + // Verify app was created + var dbApp AppV1 + err = db.Where("id = ?", "test-app").First(&dbApp).Error + require.NoError(t, err) + + assert.Equal(t, "test-app", dbApp.ID) + assert.Equal(t, "0x1111111111111111111111111111111111111111", dbApp.OwnerWallet) + assert.Equal(t, "0xabcdef", dbApp.Metadata) + assert.Equal(t, uint64(1), dbApp.Version) + assert.True(t, dbApp.CreationApprovalNotRequired) + }) + + t.Run("Duplicate ID error", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + + store := NewDBStore(db) + + entry := app.AppV1{ + ID: "test-app", + OwnerWallet: "0x1111111111111111111111111111111111111111", + Metadata: "0xabcdef", + Version: 1, + } + + err := store.CreateApp(entry) + require.NoError(t, err) + + // Try to create again with same ID + err = store.CreateApp(entry) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create app") + }) + + t.Run("Stores lowercase ID and wallet", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + + store := NewDBStore(db) + + entry := app.AppV1{ + ID: "My-App", + OwnerWallet: "0xABCD1234567890ABCDEF1234567890ABCDEF1234", + Metadata: "0x00", + Version: 1, + } + + err := store.CreateApp(entry) + require.NoError(t, err) + + var dbApp AppV1 + err = db.Where("id = ?", "my-app").First(&dbApp).Error + require.NoError(t, err) + + assert.Equal(t, "my-app", dbApp.ID) + assert.Equal(t, "0xabcd1234567890abcdef1234567890abcdef1234", dbApp.OwnerWallet) + }) +} + +func TestDBStore_GetApp(t *testing.T) { + t.Run("Found", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + + store := NewDBStore(db) + + entry := app.AppV1{ + ID: "test-app", + OwnerWallet: "0x1111111111111111111111111111111111111111", + Metadata: "0xabcdef", + Version: 1, + CreationApprovalNotRequired: true, + } + require.NoError(t, store.CreateApp(entry)) + + result, err := store.GetApp("test-app") + require.NoError(t, err) + require.NotNil(t, result) + + assert.Equal(t, "test-app", result.App.ID) + assert.Equal(t, "0x1111111111111111111111111111111111111111", result.App.OwnerWallet) + assert.Equal(t, "0xabcdef", result.App.Metadata) + assert.Equal(t, uint64(1), result.App.Version) + assert.True(t, result.App.CreationApprovalNotRequired) + }) + + t.Run("Not found returns nil", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + + store := NewDBStore(db) + + result, err := store.GetApp("nonexistent") + require.NoError(t, err) + assert.Nil(t, result) + }) + + t.Run("Case-insensitive lookup", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + + store := NewDBStore(db) + + entry := app.AppV1{ + ID: "test-app", + OwnerWallet: "0x1111111111111111111111111111111111111111", + Metadata: "0x00", + Version: 1, + } + require.NoError(t, store.CreateApp(entry)) + + // Look up with different casing + result, err := store.GetApp("Test-App") + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, "test-app", result.App.ID) + }) +} + +func TestDBStore_GetApps(t *testing.T) { + t.Run("No filter", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + + store := NewDBStore(db) + + require.NoError(t, store.CreateApp(app.AppV1{ + ID: "app-1", OwnerWallet: "0x1111111111111111111111111111111111111111", + Metadata: "0x01", Version: 1, + })) + require.NoError(t, store.CreateApp(app.AppV1{ + ID: "app-2", OwnerWallet: "0x2222222222222222222222222222222222222222", + Metadata: "0x02", Version: 1, + })) + + // Small delay to ensure different created_at times + time.Sleep(10 * time.Millisecond) + + pagination := &core.PaginationParams{} + apps, metadata, err := store.GetApps(nil, nil, pagination) + require.NoError(t, err) + + assert.Len(t, apps, 2) + assert.Equal(t, uint32(2), metadata.TotalCount) + }) + + t.Run("Filter by appID", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + + store := NewDBStore(db) + + require.NoError(t, store.CreateApp(app.AppV1{ + ID: "app-1", OwnerWallet: "0x1111111111111111111111111111111111111111", + Metadata: "0x01", Version: 1, + })) + require.NoError(t, store.CreateApp(app.AppV1{ + ID: "app-2", OwnerWallet: "0x2222222222222222222222222222222222222222", + Metadata: "0x02", Version: 1, + })) + + appID := "app-1" + pagination := &core.PaginationParams{} + apps, metadata, err := store.GetApps(&appID, nil, pagination) + require.NoError(t, err) + + assert.Len(t, apps, 1) + assert.Equal(t, uint32(1), metadata.TotalCount) + assert.Equal(t, "app-1", apps[0].App.ID) + }) + + t.Run("Filter by ownerWallet", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + + store := NewDBStore(db) + + owner := "0x1111111111111111111111111111111111111111" + require.NoError(t, store.CreateApp(app.AppV1{ + ID: "app-1", OwnerWallet: owner, Metadata: "0x01", Version: 1, + })) + require.NoError(t, store.CreateApp(app.AppV1{ + ID: "app-2", OwnerWallet: "0x2222222222222222222222222222222222222222", + Metadata: "0x02", Version: 1, + })) + require.NoError(t, store.CreateApp(app.AppV1{ + ID: "app-3", OwnerWallet: owner, Metadata: "0x03", Version: 1, + })) + + pagination := &core.PaginationParams{} + apps, metadata, err := store.GetApps(nil, &owner, pagination) + require.NoError(t, err) + + assert.Len(t, apps, 2) + assert.Equal(t, uint32(2), metadata.TotalCount) + for _, a := range apps { + assert.Equal(t, owner, a.App.OwnerWallet) + } + }) + + t.Run("Combined filters", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + + store := NewDBStore(db) + + owner := "0x1111111111111111111111111111111111111111" + require.NoError(t, store.CreateApp(app.AppV1{ + ID: "app-1", OwnerWallet: owner, Metadata: "0x01", Version: 1, + })) + require.NoError(t, store.CreateApp(app.AppV1{ + ID: "app-2", OwnerWallet: owner, Metadata: "0x02", Version: 1, + })) + require.NoError(t, store.CreateApp(app.AppV1{ + ID: "app-3", OwnerWallet: "0x2222222222222222222222222222222222222222", + Metadata: "0x03", Version: 1, + })) + + appID := "app-1" + pagination := &core.PaginationParams{} + apps, metadata, err := store.GetApps(&appID, &owner, pagination) + require.NoError(t, err) + + assert.Len(t, apps, 1) + assert.Equal(t, uint32(1), metadata.TotalCount) + assert.Equal(t, "app-1", apps[0].App.ID) + assert.Equal(t, owner, apps[0].App.OwnerWallet) + }) + + t.Run("Pagination", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + + store := NewDBStore(db) + + for i := 1; i <= 3; i++ { + require.NoError(t, store.CreateApp(app.AppV1{ + ID: "app-" + string(rune(i+'0')), + OwnerWallet: "0x1111111111111111111111111111111111111111", + Metadata: "0x00", + Version: 1, + })) + time.Sleep(10 * time.Millisecond) + } + + limit := uint32(2) + offset := uint32(0) + pagination := &core.PaginationParams{Limit: &limit, Offset: &offset} + + apps, metadata, err := store.GetApps(nil, nil, pagination) + require.NoError(t, err) + + assert.Len(t, apps, 2) + assert.Equal(t, uint32(3), metadata.TotalCount) + assert.Equal(t, uint32(1), metadata.Page) + assert.Equal(t, uint32(2), metadata.PerPage) + + // Second page + offset = 2 + pagination.Offset = &offset + apps, metadata, err = store.GetApps(nil, nil, pagination) + require.NoError(t, err) + + assert.Len(t, apps, 1) + assert.Equal(t, uint32(2), metadata.Page) + }) + + t.Run("Empty results", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + + store := NewDBStore(db) + + appID := "nonexistent" + pagination := &core.PaginationParams{} + apps, metadata, err := store.GetApps(&appID, nil, pagination) + require.NoError(t, err) + + assert.Empty(t, apps) + assert.Equal(t, uint32(0), metadata.TotalCount) + }) +} diff --git a/clearnode/store/database/database.go b/clearnode/store/database/database.go index 4ba74d4d6..a29e31ea0 100644 --- a/clearnode/store/database/database.go +++ b/clearnode/store/database/database.go @@ -213,7 +213,7 @@ func migratePostgres(cnf DatabaseConfig, embedMigrations embed.FS) error { } func migrateSqlite(db *gorm.DB) error { - if err := db.AutoMigrate(&AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &UserBalance{}); err != nil { + if err := db.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &UserBalance{}); err != nil { return err } return nil diff --git a/clearnode/store/database/interface.go b/clearnode/store/database/interface.go index d6b5fc7b8..62dfd16da 100644 --- a/clearnode/store/database/interface.go +++ b/clearnode/store/database/interface.go @@ -113,6 +113,17 @@ type DatabaseStore interface { // GetStateByID retrieves a state by its deterministic ID. GetStateByID(stateID string) (*core.State, error) + // --- App Registry Operations --- + + // CreateApp registers a new application. Returns an error if the app ID already exists. + CreateApp(entry app.AppV1) error + + // GetApp retrieves a single application by ID. Returns nil if not found. + GetApp(appID string) (*app.AppInfoV1, error) + + // GetApps retrieves applications with optional filtering by app ID, owner wallet, and pagination. + GetApps(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) + // --- App Session Operations --- // CreateAppSession initializes a new application session. diff --git a/clearnode/store/database/testing.go b/clearnode/store/database/testing.go index ffc7cd669..ca9d9f8c3 100644 --- a/clearnode/store/database/testing.go +++ b/clearnode/store/database/testing.go @@ -54,7 +54,7 @@ func setupTestSqlite(t testing.TB) *gorm.DB { t.Fatalf("Failed to open SQLite database: %v", err) } - err = database.AutoMigrate(&AppLedgerEntryV1{}, &AppSessionV1{}, &AppParticipantV1{}, &BlockchainAction{}, &Channel{}, &ContractEvent{}, &State{}, &Transaction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{}) + err = database.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &AppSessionV1{}, &AppParticipantV1{}, &BlockchainAction{}, &Channel{}, &ContractEvent{}, &State{}, &Transaction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{}) if err != nil { t.Fatalf("Failed to run migrations: %v", err) } @@ -99,7 +99,7 @@ func setupTestPostgres(ctx context.Context, t testing.TB) (*gorm.DB, testcontain t.Fatalf("Failed to open PostgreSQL database: %v", err) } - err = database.AutoMigrate(&AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &Transaction{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{}) + err = database.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &Transaction{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{}) if err != nil { t.Fatalf("Failed to run migrations: %v", err) } diff --git a/clearnode/store/database/utils.go b/clearnode/store/database/utils.go index 696de008b..ea62d7b37 100644 --- a/clearnode/store/database/utils.go +++ b/clearnode/store/database/utils.go @@ -39,7 +39,7 @@ func databaseAppSessionToCore(dbSession *AppSessionV1) *app.AppSessionV1 { return &app.AppSessionV1{ SessionID: dbSession.ID, - Application: dbSession.Application, + Application: dbSession.ApplicationID, Participants: participants, Quorum: dbSession.Quorum, Nonce: dbSession.Nonce, diff --git a/docs/api.yaml b/docs/api.yaml index 886019203..b25670dfa 100644 --- a/docs/api.yaml +++ b/docs/api.yaml @@ -415,6 +415,50 @@ types: type: string description: User's signature over the session key metadata to authorize the registration/update of the session key + - app: + description: Application definition + fields: + - name: id + type: string + description: Application identifier + - name: owner_wallet + type: string + description: Owner's wallet address + - name: metadata + type: string + description: Application metadata (bytes32 hash) + - name: version + type: string + description: Current version of the application + - name: creation_approval_not_required + type: boolean + description: Whether app sessions can be created without owner approval + + - app_info: + description: Full application info + fields: + - name: id + type: string + description: Application identifier + - name: owner_wallet + type: string + description: Owner's wallet address + - name: metadata + type: string + description: Application metadata (bytes32 hash) + - name: version + type: string + description: Current version of the application + - name: creation_approval_not_required + type: boolean + description: Whether app sessions can be created without owner approval + - name: created_at + type: string + description: Creation timestamp (unix seconds) + - name: updated_at + type: string + description: Last update timestamp (unix seconds) + - pagination_params: description: Pagination request parameters fields: @@ -760,7 +804,7 @@ api: - message: invalid_parameters description: The request parameters are invalid - name: create_app_session - description: Create a new application session between participants + description: Create a new application session between participants. The application must be registered in the app registry. If the application requires creation approval (creation_approval_not_required is false), an owner signature is required. request: - field_name: definition type: app_definition @@ -773,6 +817,9 @@ api: description: Participant signatures for the app session creation items: type: string + - field_name: owner_sig + type: string + description: Owner signature for app session creation, required when the application's creation_approval_not_required is false optional: true response: - field_name: app_session_id @@ -787,6 +834,12 @@ api: errors: - message: invalid_definition description: The application definition is invalid + - message: application_not_registered + description: The application is not registered in the app registry + - message: owner_sig_required + description: Owner signature is required for this application (creation_approval_not_required is false) + - message: invalid_owner_signature + description: The owner signature is invalid or does not match the registered app owner - message: insufficient_balance description: Participant has insufficient balance for allocations - name: submit_session_key_state @@ -819,6 +872,58 @@ api: - message: account_not_found description: The specified account was not found + - name: apps + description: Operations related to application registry management + versions: + - version: v1 + methods: + - name: get_apps + description: Retrieve registered applications with optional filtering by app ID and owner wallet + request: + - field_name: app_id + type: string + description: Filter by application ID + optional: true + - field_name: owner_wallet + type: string + description: Filter by owner wallet address + optional: true + - field_name: pagination + type: pagination_params + description: Pagination parameters (offset, limit, sort) + optional: true + response: + - field_name: apps + type: array + items: + type: app_info + description: List of registered applications + - field_name: metadata + type: pagination_metadata + description: Pagination information + errors: + - message: invalid_parameters + description: The request parameters are invalid + - name: submit_app_version + description: Register a new application in the app registry. Currently only version 1 (creation) is supported. The owner must sign the packed app data to prove ownership. + request: + - field_name: app + type: app + description: Application definition including ID, owner wallet, metadata, version, and creation approval flag + - field_name: owner_sig + type: string + description: Owner's EIP-191 signature over the packed application data + response: [] + errors: + - message: invalid_app_id + description: The application ID does not match the required format + - message: invalid_version + description: Only version 1 (creation) is currently supported + - message: invalid_signature + description: The owner signature is invalid + - message: app_already_exists + description: An application with this ID already exists + - name: session_keys description: Operations related to session key management versions: diff --git a/pkg/app/app_v1.go b/pkg/app/app_v1.go new file mode 100644 index 000000000..08ce87afd --- /dev/null +++ b/pkg/app/app_v1.go @@ -0,0 +1,59 @@ +package app + +import ( + "fmt" + "regexp" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +var AppIDV1Regex = regexp.MustCompile(`^[a-z0-9][-a-z0-9]{0,65}$`) + +// AppV1 represents an application registry entry. +type AppV1 struct { + ID string + OwnerWallet string + Metadata string + Version uint64 + CreationApprovalNotRequired bool +} + +// AppInfoV1 represents full application info including timestamps. +type AppInfoV1 struct { + App AppV1 + CreatedAt time.Time + UpdatedAt time.Time +} + +// PackAppV1 packs the AppV1 for signing using ABI encoding. +func PackAppV1(app AppV1) ([]byte, error) { + if !common.IsHexAddress(app.OwnerWallet) { + return nil, fmt.Errorf("invalid owner wallet address: %s", app.OwnerWallet) + } + + args := abi.Arguments{ + {Type: abi.Type{T: abi.StringTy}}, // id + {Type: abi.Type{T: abi.AddressTy}}, // ownerWallet + {Type: abi.Type{T: abi.FixedBytesTy, Size: 32}}, // metadata (bytes32) + {Type: abi.Type{T: abi.UintTy, Size: 64}}, // version + {Type: abi.Type{T: abi.BoolTy}}, // creationApprovalNotRequired + } + + appMetadataHash := crypto.Keccak256Hash([]byte(app.Metadata)) + + packed, err := args.Pack( + app.ID, + common.HexToAddress(app.OwnerWallet), + appMetadataHash, + app.Version, + app.CreationApprovalNotRequired, + ) + if err != nil { + return nil, fmt.Errorf("failed to pack app: %w", err) + } + + return crypto.Keccak256(packed), nil +} diff --git a/pkg/rpc/api.go b/pkg/rpc/api.go index d22fd2b20..d4d310336 100644 --- a/pkg/rpc/api.go +++ b/pkg/rpc/api.go @@ -247,8 +247,10 @@ type AppSessionsV1CreateAppSessionRequest struct { Definition AppDefinitionV1 `json:"definition"` // SessionData is the optional JSON stringified session data SessionData string `json:"session_data"` - - QuorumSigs []string `json:"quorum_sigs,omitempty"` // Participant signatures for the app session creation + // QuorumSigs is the list of participant signatures for the app session creation + QuorumSigs []string `json:"quorum_sigs,omitempty"` + // OwnerSig is the optional owner signature for app session creation if approval required by the app registry + OwnerSig string `json:"owner_sig,omitempty"` } // AppSessionsV1CreateAppSessionResponse returns the created application session information. @@ -284,6 +286,40 @@ type AppSessionsV1GetLastKeyStatesResponse struct { States []AppSessionKeyStateV1 `json:"states"` } +// ============================================================================ +// Apps Group - V1 API +// ============================================================================ + +// AppsV1GetAppsRequest retrieves registered applications with optional filtering. +type AppsV1GetAppsRequest struct { + // AppID filters by application ID + AppID *string `json:"app_id,omitempty"` + // OwnerWallet filters by owner wallet address + OwnerWallet *string `json:"owner_wallet,omitempty"` + // Pagination contains pagination parameters (offset, limit, sort) + Pagination *PaginationParamsV1 `json:"pagination,omitempty"` +} + +// AppsV1GetAppsResponse returns the list of registered applications. +type AppsV1GetAppsResponse struct { + // Apps is the list of registered applications + Apps []AppInfoV1 `json:"apps"` + // Metadata contains pagination information + Metadata PaginationMetadataV1 `json:"metadata"` +} + +// AppsV1SubmitAppVersionRequest submits a new application version (currently only creation is supported). +type AppsV1SubmitAppVersionRequest struct { + // App contains the application definition + App AppV1 `json:"app"` + // OwnerSig is the owner's signature over the packed app data + OwnerSig string `json:"owner_sig"` +} + +// AppsV1SubmitAppVersionResponse returns the result of the application version submission. +type AppsV1SubmitAppVersionResponse struct { +} + // ============================================================================ // User Group - V1 API // ============================================================================ diff --git a/pkg/rpc/client.go b/pkg/rpc/client.go index 5a26261a8..b3be86ecf 100644 --- a/pkg/rpc/client.go +++ b/pkg/rpc/client.go @@ -217,6 +217,28 @@ func (c *Client) AppSessionsV1GetLastKeyStates(ctx context.Context, req AppSessi return resp, nil } +// ============================================================================ +// Apps Group - V1 API Methods +// ============================================================================ + +// AppsV1GetApps retrieves registered applications with optional filtering. +func (c *Client) AppsV1GetApps(ctx context.Context, req AppsV1GetAppsRequest) (AppsV1GetAppsResponse, error) { + var resp AppsV1GetAppsResponse + if err := c.call(ctx, AppsV1GetAppsMethod, req, &resp); err != nil { + return resp, err + } + return resp, nil +} + +// AppsV1SubmitAppVersion submits a new application version (currently only creation is supported). +func (c *Client) AppsV1SubmitAppVersion(ctx context.Context, req AppsV1SubmitAppVersionRequest) (AppsV1SubmitAppVersionResponse, error) { + var resp AppsV1SubmitAppVersionResponse + if err := c.call(ctx, AppsV1SubmitAppVersionMethod, req, &resp); err != nil { + return resp, err + } + return resp, nil +} + // ============================================================================ // User Group - V1 API Methods // ============================================================================ diff --git a/pkg/rpc/client_test.go b/pkg/rpc/client_test.go index 3bbc44266..ef02f6826 100644 --- a/pkg/rpc/client_test.go +++ b/pkg/rpc/client_test.go @@ -509,6 +509,133 @@ func TestClientV1_AppSessionsV1GetLastKeyStates(t *testing.T) { assert.Equal(t, "0xsession_key_1", res.States[0].SessionKey) } +// ============================================================================ +// Apps Group Tests +// ============================================================================ + +func TestClientV1_AppsV1GetApps(t *testing.T) { + t.Parallel() + + client, dialer := setupClient() + + response := rpc.AppsV1GetAppsResponse{ + Apps: []rpc.AppInfoV1{ + { + AppV1: rpc.AppV1{ + ID: "my-app", + OwnerWallet: testWalletV1, + Metadata: `{"name": "My App"}`, + Version: "1", + CreationApprovalNotRequired: true, + }, + CreatedAt: "1700000000", + UpdatedAt: "1700000001", + }, + { + AppV1: rpc.AppV1{ + ID: "another-app", + OwnerWallet: testWallet2V1, + Metadata: `{"name": "Another App"}`, + Version: "1", + CreationApprovalNotRequired: false, + }, + CreatedAt: "1700000100", + UpdatedAt: "1700000200", + }, + }, + Metadata: rpc.PaginationMetadataV1{ + Page: 1, + PerPage: 10, + TotalCount: 2, + PageCount: 1, + }, + } + + registerSimpleHandlerV1(dialer, rpc.AppsV1GetAppsMethod.String(), response) + + resp, err := client.AppsV1GetApps(testCtxV1, rpc.AppsV1GetAppsRequest{}) + require.NoError(t, err) + assert.Len(t, resp.Apps, 2) + assert.Equal(t, "my-app", resp.Apps[0].ID) + assert.Equal(t, testWalletV1, resp.Apps[0].OwnerWallet) + assert.Equal(t, `{"name": "My App"}`, resp.Apps[0].Metadata) + assert.Equal(t, "1", resp.Apps[0].Version) + assert.True(t, resp.Apps[0].CreationApprovalNotRequired) + assert.Equal(t, "1700000000", resp.Apps[0].CreatedAt) + assert.Equal(t, "1700000001", resp.Apps[0].UpdatedAt) + assert.Equal(t, uint32(2), resp.Metadata.TotalCount) +} + +func TestClientV1_AppsV1GetApps_WithFilters(t *testing.T) { + t.Parallel() + + client, dialer := setupClient() + + appID := "my-app" + ownerWallet := testWalletV1 + + response := rpc.AppsV1GetAppsResponse{ + Apps: []rpc.AppInfoV1{ + { + AppV1: rpc.AppV1{ + ID: appID, + OwnerWallet: ownerWallet, + Metadata: `{}`, + Version: "1", + }, + CreatedAt: "1700000000", + UpdatedAt: "1700000000", + }, + }, + Metadata: rpc.PaginationMetadataV1{ + Page: 1, + PerPage: 10, + TotalCount: 1, + PageCount: 1, + }, + } + + registerSimpleHandlerV1(dialer, rpc.AppsV1GetAppsMethod.String(), response) + + resp, err := client.AppsV1GetApps(testCtxV1, rpc.AppsV1GetAppsRequest{ + AppID: &appID, + OwnerWallet: &ownerWallet, + Pagination: &rpc.PaginationParamsV1{ + Offset: ptrUint32(0), + Limit: ptrUint32(10), + }, + }) + require.NoError(t, err) + assert.Len(t, resp.Apps, 1) + assert.Equal(t, appID, resp.Apps[0].ID) +} + +func TestClientV1_AppsV1SubmitAppVersion(t *testing.T) { + t.Parallel() + + client, dialer := setupClient() + + response := rpc.AppsV1SubmitAppVersionResponse{} + + registerSimpleHandlerV1(dialer, rpc.AppsV1SubmitAppVersionMethod.String(), response) + + _, err := client.AppsV1SubmitAppVersion(testCtxV1, rpc.AppsV1SubmitAppVersionRequest{ + App: rpc.AppV1{ + ID: "my-app", + OwnerWallet: testWalletV1, + Metadata: `{"name": "My App"}`, + Version: "1", + CreationApprovalNotRequired: false, + }, + OwnerSig: "0xsig123", + }) + require.NoError(t, err) +} + +// ============================================================================ +// User Group Tests +// ============================================================================ + func TestClientV1_UserV1GetBalances(t *testing.T) { t.Parallel() @@ -587,3 +714,11 @@ func TestClientV1_ErrorHandling(t *testing.T) { _, err = client.NodeV1GetAssets(testCtxV1, rpc.NodeV1GetAssetsRequest{}) assert.Contains(t, err.Error(), "internal server error") } + +// ============================================================================ +// Test Helpers +// ============================================================================ + +func ptrUint32(v uint32) *uint32 { + return &v +} diff --git a/pkg/rpc/methods.go b/pkg/rpc/methods.go index 9b3cf44c9..4d116a599 100644 --- a/pkg/rpc/methods.go +++ b/pkg/rpc/methods.go @@ -29,6 +29,11 @@ const ( AppSessionsV1SubmitSessionKeyStateMethod Method = "app_sessions.v1.submit_session_key_state" AppSessionsV1GetLastKeyStatesMethod Method = "app_sessions.v1.get_last_key_states" + // Apps Group - V1 Methods + AppsV1Group Group = "apps.v1" + AppsV1GetAppsMethod Method = "apps.v1.get_apps" + AppsV1SubmitAppVersionMethod Method = "apps.v1.submit_app_version" + // User Group - V1 Methods UserV1Group Group = "user.v1" UserV1GetBalancesMethod Method = "user.v1.get_balances" diff --git a/pkg/rpc/types.go b/pkg/rpc/types.go index 87b914994..5f61f7764 100644 --- a/pkg/rpc/types.go +++ b/pkg/rpc/types.go @@ -220,6 +220,33 @@ type AppSessionKeyStateV1 struct { UserSig string `json:"user_sig"` } +// ============================================================================ +// App Registry Types +// ============================================================================ + +// AppV1 represents a registered application definition (without timestamps). +type AppV1 struct { + // ID is the application identifier + ID string `json:"id"` + // OwnerWallet is the owner's wallet address + OwnerWallet string `json:"owner_wallet"` + // Metadata is the application metadata + Metadata string `json:"metadata"` + // Version is the current version + Version string `json:"version"` + // CreationApprovalNotRequired indicates if sessions can be created without approval + CreationApprovalNotRequired bool `json:"creation_approval_not_required"` +} + +// AppInfoV1 represents full application info including timestamps. +type AppInfoV1 struct { + AppV1 + // CreatedAt is the creation timestamp (unix seconds) + CreatedAt string `json:"created_at"` + // UpdatedAt is the last update timestamp (unix seconds) + UpdatedAt string `json:"updated_at"` +} + // ============================================================================ // Asset and Blockchain Types // ============================================================================ diff --git a/sdk/go/README.md b/sdk/go/README.md index 2a229b2eb..620c8c423 100644 --- a/sdk/go/README.md +++ b/sdk/go/README.md @@ -47,6 +47,12 @@ client.GetEscrowChannel(ctx, escrowChannelID) // Escrow channel info client.GetLatestState(ctx, wallet, asset, onlySigned) // Latest state ``` +### App Registry +```go +client.GetApps(ctx, opts) // List registered apps +client.RegisterApp(ctx, appID, metadata, approvalNotRequired) // Register new app +``` + ### App Sessions ```go client.GetAppSessions(ctx, opts) // List sessions @@ -137,6 +143,7 @@ sdk/go/ ├── node.go # Node information methods ├── user.go # User query methods ├── channel.go # Channel and state management +├── app_registry.go # App registry methods ├── app_session.go # App session methods ├── asset_cache.go # Asset lookup and caching ├── config.go # Configuration options @@ -351,6 +358,19 @@ state, err := client.GetLatestState(ctx, wallet, asset, onlySigned) **Note:** State submission and channel creation are handled internally by state operations (Deposit, Withdraw, Transfer). On-chain settlement is handled by Checkpoint. +### App Registry + +```go +// List registered applications with optional filtering +apps, meta, err := client.GetApps(ctx, &sdk.GetAppsOptions{ + AppID: &appID, + OwnerWallet: &wallet, +}) + +// Register a new application +err := client.RegisterApp(ctx, "my-app", `{"name": "My App"}`, false) +``` + ### App Sessions (Low-Level) ```go @@ -586,6 +606,10 @@ core.ChannelSessionKeyStateV1 // Channel session key state // Fields: UserAddress, SessionKey, Version (uint64), Assets []string, // ExpiresAt (time.Time), UserSig string +// App registry types +app.AppV1 // Application definition +app.AppInfoV1 // Application info with timestamps + // App session types app.AppSessionInfoV1 // Session info app.AppDefinitionV1 // Session definition diff --git a/sdk/go/app_registry.go b/sdk/go/app_registry.go new file mode 100644 index 000000000..be38dc51c --- /dev/null +++ b/sdk/go/app_registry.go @@ -0,0 +1,170 @@ +package sdk + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/erc7824/nitrolite/pkg/app" + "github.com/erc7824/nitrolite/pkg/core" + "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/erc7824/nitrolite/pkg/sign" +) + +// ============================================================================ +// App Registry Methods +// ============================================================================ + +// GetAppsOptions contains optional filters for GetApps. +type GetAppsOptions struct { + // AppID filters by application ID + AppID *string + + // OwnerWallet filters by owner wallet address + OwnerWallet *string + + // Pagination parameters + Pagination *core.PaginationParams +} + +// GetApps retrieves registered applications with optional filtering. +// +// Parameters: +// - opts: Optional filters (pass nil for no filters) +// +// Returns: +// - Slice of AppInfoV1 with application information +// - core.PaginationMetadata with pagination information +// - Error if the request fails +// +// Example: +// +// apps, meta, err := client.GetApps(ctx, nil) +// for _, a := range apps { +// fmt.Printf("App %s owned by %s\n", a.App.ID, a.App.OwnerWallet) +// } +func (c *Client) GetApps(ctx context.Context, opts *GetAppsOptions) ([]app.AppInfoV1, core.PaginationMetadata, error) { + req := rpc.AppsV1GetAppsRequest{} + if opts != nil { + req.AppID = opts.AppID + req.OwnerWallet = opts.OwnerWallet + req.Pagination = transformPaginationParams(opts.Pagination) + } + resp, err := c.rpcClient.AppsV1GetApps(ctx, req) + if err != nil { + return nil, core.PaginationMetadata{}, fmt.Errorf("failed to get apps: %w", err) + } + + apps, err := transformApps(resp.Apps) + if err != nil { + return nil, core.PaginationMetadata{}, fmt.Errorf("failed to transform apps: %w", err) + } + + return apps, transformPaginationMetadata(resp.Metadata), nil +} + +// RegisterApp registers a new application in the app registry. +// Currently only version 1 (creation) is supported. +// +// The method builds the app definition from the provided parameters, +// using the client's signer address as the owner wallet and version 1. +// It then packs and signs the definition automatically. +// +// Session key signers are not allowed to perform this action; the main +// wallet signer must be used. +// +// Parameters: +// - appID: The application identifier +// - metadata: The application metadata +// - creationApprovalNotRequired: Whether sessions can be created without owner approval +// +// Returns: +// - Error if the request fails +// +// Example: +// +// err := client.RegisterApp(ctx, "my-app", `{"name": "My App"}`, false) +func (c *Client) RegisterApp(ctx context.Context, appID string, metadata string, creationApprovalNotRequired bool) error { + appDef := app.AppV1{ + ID: appID, + OwnerWallet: c.GetUserAddress(), + Metadata: metadata, + Version: 1, + CreationApprovalNotRequired: creationApprovalNotRequired, + } + + packed, err := app.PackAppV1(appDef) + if err != nil { + return fmt.Errorf("failed to pack app: %w", err) + } + + ethMsgSigner, err := sign.NewEthereumMsgSignerFromRaw(c.rawSigner) + if err != nil { + return fmt.Errorf("failed to create Ethereum message signer: %w", err) + } + + sig, err := ethMsgSigner.Sign(packed) + if err != nil { + return fmt.Errorf("failed to sign app data: %w", err) + } + + req := rpc.AppsV1SubmitAppVersionRequest{ + App: transformAppToRPC(appDef), + OwnerSig: sig.String(), + } + _, err = c.rpcClient.AppsV1SubmitAppVersion(ctx, req) + if err != nil { + return fmt.Errorf("failed to register app: %w", err) + } + return nil +} + +// ============================================================================ +// App Registry Transformations +// ============================================================================ + +// transformApps converts RPC AppInfoV1 slice to app.AppInfoV1 slice. +func transformApps(apps []rpc.AppInfoV1) ([]app.AppInfoV1, error) { + result := make([]app.AppInfoV1, 0, len(apps)) + for _, a := range apps { + version, err := strconv.ParseUint(a.Version, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse app version: %w", err) + } + + createdAtSec, err := strconv.ParseInt(a.CreatedAt, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse created_at: %w", err) + } + + updatedAtSec, err := strconv.ParseInt(a.UpdatedAt, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse updated_at: %w", err) + } + + result = append(result, app.AppInfoV1{ + App: app.AppV1{ + ID: a.ID, + OwnerWallet: a.OwnerWallet, + Metadata: a.Metadata, + Version: version, + CreationApprovalNotRequired: a.CreationApprovalNotRequired, + }, + CreatedAt: time.Unix(createdAtSec, 0), + UpdatedAt: time.Unix(updatedAtSec, 0), + }) + } + return result, nil +} + +// transformAppToRPC converts app.AppV1 to rpc.AppV1. +func transformAppToRPC(a app.AppV1) rpc.AppV1 { + return rpc.AppV1{ + ID: a.ID, + OwnerWallet: a.OwnerWallet, + Metadata: a.Metadata, + Version: strconv.FormatUint(a.Version, 10), + CreationApprovalNotRequired: a.CreationApprovalNotRequired, + } +} diff --git a/sdk/ts/README.md b/sdk/ts/README.md index 95bef03b5..485c4e60f 100644 --- a/sdk/ts/README.md +++ b/sdk/ts/README.md @@ -52,6 +52,12 @@ client.getEscrowChannel(escrowChannelId) // Escrow channel info client.getLatestState(wallet, asset, onlySigned) // Latest state ``` +### App Registry +```typescript +client.getApps(opts) // List registered apps +client.registerApp(appID, metadata, approvalNotRequired) // Register new app +``` + ### App Sessions ```typescript client.getAppSessions(opts) // List sessions @@ -391,6 +397,21 @@ const state = await client.getLatestState(wallet, asset, onlySigned); **Note:** State submission and channel creation are handled internally by state operations (`deposit()`, `withdraw()`, `transfer()`). On-chain settlement is handled by `checkpoint()`. +### App Registry + +```typescript +// List registered applications with optional filtering +const { apps, metadata } = await client.getApps({ + appId: 'my-app', + ownerWallet: '0x1234...', + page: 1, + pageSize: 10, +}); + +// Register a new application +await client.registerApp('my-app', '{"name": "My App"}', false); +``` + ### App Sessions (Low-Level) ```typescript @@ -854,6 +875,9 @@ import type { ChannelSessionKeyStateV1, PaginationMetadata, } from '@yellow-org/sdk'; + +// App Registry types (from rpc/types) +import type { AppV1, AppInfoV1 } from '@yellow-org/sdk'; ``` ### BigInt for Chain IDs diff --git a/sdk/ts/src/app/packing.ts b/sdk/ts/src/app/packing.ts index cccff4d70..c66686273 100644 --- a/sdk/ts/src/app/packing.ts +++ b/sdk/ts/src/app/packing.ts @@ -1,10 +1,11 @@ -import { Address, encodeAbiParameters, keccak256 } from 'viem'; +import { Address, encodeAbiParameters, keccak256, toHex } from 'viem'; import { AppDefinitionV1, AppStateUpdateV1, AppSessionKeyStateV1, AppSessionVersionV1, } from './types'; +import { AppV1 } from '../rpc/types'; /** * PackCreateAppSessionRequestV1 packs the Definition and SessionData for signing using ABI encoding. @@ -176,6 +177,36 @@ export function generateRebalanceTransactionIDV1( return keccak256(packed); } +/** + * PackAppV1 packs the AppV1 for signing using ABI encoding. + * Matches Go SDK's PackAppV1. + * + * @param app - The application definition to pack + * @returns Keccak256 hash of the ABI-encoded app data + */ +export function packAppV1(app: AppV1): `0x${string}` { + const metadataHash = keccak256(toHex(new TextEncoder().encode(app.metadata))); + + const packed = encodeAbiParameters( + [ + { type: 'string' }, // id + { type: 'address' }, // ownerWallet + { type: 'bytes32' }, // metadata (hashed) + { type: 'uint64' }, // version + { type: 'bool' }, // creationApprovalNotRequired + ], + [ + app.id, + app.owner_wallet as Address, + metadataHash, + BigInt(app.version), + app.creation_approval_not_required, + ] + ); + + return keccak256(packed); +} + /** * PackAppSessionKeyStateV1 packs the app session key state for signing using ABI encoding. * Matches Go SDK's PackAppSessionKeyStateV1. diff --git a/sdk/ts/src/client.ts b/sdk/ts/src/client.ts index 70676ba66..078834ca9 100644 --- a/sdk/ts/src/client.ts +++ b/sdk/ts/src/client.ts @@ -10,7 +10,7 @@ import Decimal from 'decimal.js'; import * as core from './core'; import * as app from './app'; import * as API from './rpc/api'; -import { StateV1, ChannelDefinitionV1, ChannelSessionKeyStateV1 } from './rpc/types'; +import { StateV1, ChannelDefinitionV1, ChannelSessionKeyStateV1, AppV1, AppInfoV1 } from './rpc/types'; import { RPCClient } from './rpc/client'; import { WebsocketDialer } from './rpc/dialer'; import { ClientAssetStore } from './asset_store'; @@ -1405,6 +1405,84 @@ export class Client { return resp.batch_id; } + // ============================================================================ + // App Registry Methods + // ============================================================================ + + /** + * GetApps retrieves registered applications with optional filtering. + * + * @param options - Optional filters (appId, ownerWallet, pagination) + * @returns Array of registered apps and pagination metadata + * + * @example + * ```typescript + * const { apps, metadata } = await client.getApps({ ownerWallet: '0x1234...' }); + * for (const app of apps) { + * console.log(`${app.id}: owned by ${app.owner_wallet}`); + * } + * ``` + */ + async getApps(options?: { + appId?: string; + ownerWallet?: string; + page?: number; + pageSize?: number; + }): Promise<{ apps: AppInfoV1[]; metadata: core.PaginationMetadata }> { + const req: API.AppsV1GetAppsRequest = { + app_id: options?.appId, + owner_wallet: options?.ownerWallet, + pagination: options?.page && options?.pageSize ? { + offset: (options.page - 1) * options.pageSize, + limit: options.pageSize, + } : undefined, + }; + const resp = await this.rpcClient.appsV1GetApps(req); + return { + apps: resp.apps, + metadata: transformPaginationMetadata(resp.metadata), + }; + } + + /** + * RegisterApp registers a new application in the app registry. + * Currently only version 1 (creation) is supported. + * + * The method builds the app definition from the provided parameters, + * using the client's signer address as the owner wallet and version 1. + * It then packs and signs the definition automatically. + * + * Session key signers are not allowed to perform this action; the main + * wallet signer must be used. + * + * @param appID - The application identifier + * @param metadata - The application metadata + * @param creationApprovalNotRequired - Whether sessions can be created without owner approval + * + * @example + * ```typescript + * await client.registerApp('my-app', '{"name": "My App"}', false); + * ``` + */ + async registerApp(appID: string, metadata: string, creationApprovalNotRequired: boolean): Promise { + const appDef: AppV1 = { + id: appID, + owner_wallet: this.txSigner.getAddress(), + metadata, + version: '1', + creation_approval_not_required: creationApprovalNotRequired, + }; + + const packed = app.packAppV1(appDef); + const ownerSig = await this.stateSigner.signMessage(packed); + + const req: API.AppsV1SubmitAppVersionRequest = { + app: appDef, + owner_sig: ownerSig, + }; + await this.rpcClient.appsV1SubmitAppVersion(req); + } + // ============================================================================ // Channel Session Key Methods // ============================================================================ diff --git a/sdk/ts/src/rpc/api.ts b/sdk/ts/src/rpc/api.ts index 736a5039c..662e8481d 100644 --- a/sdk/ts/src/rpc/api.ts +++ b/sdk/ts/src/rpc/api.ts @@ -16,6 +16,8 @@ import { PaginationMetadataV1, AssetV1, BlockchainInfoV1, + AppV1, + AppInfoV1, } from './types'; import { AppDefinitionV1, @@ -285,6 +287,35 @@ export interface AppSessionsV1GetLastKeyStatesResponse { states: AppSessionKeyStateV1[]; } +// ============================================================================ +// Apps Group - V1 API +// ============================================================================ + +export interface AppsV1GetAppsRequest { + /** Application ID filter */ + app_id?: string; + /** Owner wallet address filter */ + owner_wallet?: string; + /** Pagination parameters */ + pagination?: PaginationParamsV1; +} + +export interface AppsV1GetAppsResponse { + /** List of registered applications */ + apps: AppInfoV1[]; + /** Pagination information */ + metadata: PaginationMetadataV1; +} + +export interface AppsV1SubmitAppVersionRequest { + /** Application definition */ + app: AppV1; + /** Owner's signature over the packed app data */ + owner_sig: string; +} + +export interface AppsV1SubmitAppVersionResponse {} + // ============================================================================ // User Group - V1 API // ============================================================================ diff --git a/sdk/ts/src/rpc/client.ts b/sdk/ts/src/rpc/client.ts index f8ae922dc..6de5dc2cf 100644 --- a/sdk/ts/src/rpc/client.ts +++ b/sdk/ts/src/rpc/client.ts @@ -198,6 +198,24 @@ export class RPCClient { return this.call(Methods.AppSessionsV1GetLastKeyStatesMethod, req, signal); } + // ============================================================================ + // Apps Group - V1 API Methods + // ============================================================================ + + async appsV1GetApps( + req: API.AppsV1GetAppsRequest, + signal?: AbortSignal + ): Promise { + return this.call(Methods.AppsV1GetAppsMethod, req, signal); + } + + async appsV1SubmitAppVersion( + req: API.AppsV1SubmitAppVersionRequest, + signal?: AbortSignal + ): Promise { + return this.call(Methods.AppsV1SubmitAppVersionMethod, req, signal); + } + // ============================================================================ // User Group - V1 API Methods // ============================================================================ diff --git a/sdk/ts/src/rpc/methods.ts b/sdk/ts/src/rpc/methods.ts index 2e4cf657b..e7a33ee34 100644 --- a/sdk/ts/src/rpc/methods.ts +++ b/sdk/ts/src/rpc/methods.ts @@ -40,6 +40,11 @@ export const AppSessionsV1CloseAppSessionMethod: Method = 'app_sessions.v1.close export const AppSessionsV1SubmitSessionKeyStateMethod: Method = 'app_sessions.v1.submit_session_key_state'; export const AppSessionsV1GetLastKeyStatesMethod: Method = 'app_sessions.v1.get_last_key_states'; +// Apps Group - V1 Methods +export const AppsV1Group: Group = 'apps.v1'; +export const AppsV1GetAppsMethod: Method = 'apps.v1.get_apps'; +export const AppsV1SubmitAppVersionMethod: Method = 'apps.v1.submit_app_version'; + // User Group - V1 Methods export const UserV1Group: Group = 'user.v1'; export const UserV1GetBalancesMethod: Method = 'user.v1.get_balances'; diff --git a/sdk/ts/src/rpc/types.ts b/sdk/ts/src/rpc/types.ts index 8fefff6d2..77d0d3e27 100644 --- a/sdk/ts/src/rpc/types.ts +++ b/sdk/ts/src/rpc/types.ts @@ -147,6 +147,36 @@ export interface ChannelSessionKeyStateV1 { user_sig: string; } +// ============================================================================ +// App Registry Types +// ============================================================================ + +/** + * AppV1 represents a registered application definition (without timestamps) + */ +export interface AppV1 { + /** Application identifier */ + id: string; + /** Owner's wallet address */ + owner_wallet: string; + /** Application metadata */ + metadata: string; + /** Current version */ + version: string; + /** Whether sessions can be created without owner approval */ + creation_approval_not_required: boolean; +} + +/** + * AppInfoV1 represents full application info including timestamps + */ +export interface AppInfoV1 extends AppV1 { + /** Creation timestamp (unix seconds) */ + created_at: string; + /** Last update timestamp (unix seconds) */ + updated_at: string; +} + // ============================================================================ // Asset and Blockchain Types // ============================================================================ From c47990724b8b6126a02b3edfc1155e5933897526 Mon Sep 17 00:00:00 2001 From: Sazonov Nikita <35502225+nksazonov@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:05:45 +0100 Subject: [PATCH 02/13] YNU-716: fix(ChannelHub): apply fixes to ensure home-chain unhappy paths, add tests for them (#599) --- contracts/foundry.toml | 4 +- contracts/src/ChannelEngine.sol | 36 +- contracts/src/ChannelHub.sol | 155 ++- .../src/interfaces/ISignatureValidator.sol | 3 + .../src/sigValidators/ECDSAValidator.sol | 3 + .../src/sigValidators/SessionKeyValidator.sol | 3 + contracts/test/ChannelHub_Base.t.sol | 40 +- .../test/ChannelHub_Challenge_Base.t.sol | 79 ++ .../test/ChannelHub_challengeHomeChain.t.sol | 1101 +++++++++++++++++ .../ChannelHub_challengeNonHomeChain.t.sol | 35 + .../ChannelHub_crosschain.lifecycle.t.sol | 20 +- .../ChannelHub_singlechain.lifecycle.t.sol | 24 +- 12 files changed, 1390 insertions(+), 113 deletions(-) create mode 100644 contracts/test/ChannelHub_Challenge_Base.t.sol create mode 100644 contracts/test/ChannelHub_challengeHomeChain.t.sol create mode 100644 contracts/test/ChannelHub_challengeNonHomeChain.t.sol diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 700f9701c..7dfca72f5 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -11,10 +11,10 @@ optimizer_runs = 1_000_000 # special compiler profile for ChannelHub to prevent code size overflow additional_compiler_profiles = [ - { name = "channelhub", optimizer_runs = 4_000 } + { name = "channelhub", optimizer_runs = 2_000 } ] # compile ChannelHub with lower optimizer runs to stay within size limits compilation_restrictions = [ - { paths = "src/ChannelHub.sol", optimizer_runs = 4_000 } + { paths = "src/ChannelHub.sol", optimizer_runs = 2_000 } ] diff --git a/contracts/src/ChannelEngine.sol b/contracts/src/ChannelEngine.sol index a2ee66179..7abef8b70 100644 --- a/contracts/src/ChannelEngine.sol +++ b/contracts/src/ChannelEngine.sol @@ -26,6 +26,7 @@ library ChannelEngine { error IncorrectPreviousStateIntent(); error IncorrectChannelStatus(); error IncorrectStateVersion(); + error ChallengeExpired(); error IncorrectUserAllocation(); error IncorrectNodeAllocation(); @@ -64,7 +65,6 @@ library ChannelEngine { ChannelStatus newStatus; uint64 newChallengeExpiry; bool updateLastState; - bool clearDispute; bool closeChannel; } @@ -124,6 +124,11 @@ library ChannelEngine { require(netFlowsSum >= 0, NegativeNetFlowSum()); require(allocsSum == netFlowsSum.toUint256(), IncorrectAllocationSum()); + + // If channel is DISPUTED, check that challenge hasn't expired + if (ctx.status == ChannelStatus.DISPUTED) { + require(block.timestamp <= ctx.challengeExpiry, ChallengeExpired()); + } } // ========== Internal: Phase 2 - Intent-Specific Calculation ========== @@ -183,7 +188,7 @@ library ChannelEngine { effects.userFundsDelta = userNfDelta; // Pull deposit from user effects.nodeFundsDelta = nodeNfDelta; // May lock more from node or release effects.newStatus = ChannelStatus.OPERATING; - effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED); + effects.newChallengeExpiry = 0; return effects; } @@ -205,7 +210,7 @@ library ChannelEngine { effects.userFundsDelta = userNfDelta; // Negative = push to user effects.nodeFundsDelta = nodeNfDelta; effects.newStatus = ChannelStatus.OPERATING; - effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED); + effects.newChallengeExpiry = 0; return effects; } @@ -228,7 +233,7 @@ library ChannelEngine { // Calculate effects effects.nodeFundsDelta = nodeNfDelta; // Only node balance adjustments effects.newStatus = ChannelStatus.OPERATING; - effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED); + effects.newChallengeExpiry = 0; return effects; } @@ -259,6 +264,7 @@ library ChannelEngine { effects.userFundsDelta = userNfDelta; effects.nodeFundsDelta = nodeNfDelta; effects.newStatus = ChannelStatus.CLOSED; + effects.newChallengeExpiry = 0; effects.closeChannel = true; return effects; @@ -294,7 +300,7 @@ library ChannelEngine { // Calculate effects effects.nodeFundsDelta = nodeNfDelta; // Only node balance adjustments effects.newStatus = ChannelStatus.OPERATING; - effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED); + effects.newChallengeExpiry = 0; return effects; } @@ -313,10 +319,6 @@ library ChannelEngine { || ctx.status == ChannelStatus.MIGRATING_IN, IncorrectChannelStatus() ); - require(candidate.homeLedger.nodeAllocation == 0, IncorrectNodeAllocation()); - // nothing changes from initiate escrow deposit state - require(userNfDelta == 0, IncorrectUserNetFlowDelta()); - require(nodeNfDelta == 0, IncorrectNodeNetFlowDelta()); // Check home - non-home state consistency require(ctx.prevState.intent == StateIntent.INITIATE_ESCROW_DEPOSIT, IncorrectPreviousStateIntent()); @@ -324,6 +326,11 @@ library ChannelEngine { require(candidate.nonHomeLedger.userAllocation == 0, IncorrectUserAllocation()); require(candidate.nonHomeLedger.nodeAllocation == 0, IncorrectNodeAllocation()); + require(candidate.homeLedger.nodeAllocation == 0, IncorrectNodeAllocation()); + // nothing changes from initiate escrow deposit state + require(userNfDelta == 0, IncorrectUserNetFlowDelta()); + require(nodeNfDelta == 0, IncorrectNodeNetFlowDelta()); + uint256 depositAmount = ctx.prevState.nonHomeLedger.userAllocation; require(candidate.nonHomeLedger.userNetFlow == depositAmount.toInt256(), IncorrectUserNetFlow()); require(candidate.nonHomeLedger.nodeNetFlow == -depositAmount.toInt256(), IncorrectNodeNetFlow()); @@ -339,7 +346,7 @@ library ChannelEngine { effects.userFundsDelta = 0; effects.nodeFundsDelta = 0; effects.newStatus = ChannelStatus.OPERATING; - effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED); + effects.newChallengeExpiry = 0; return effects; } @@ -372,7 +379,7 @@ library ChannelEngine { // Calculate effects - no immediate fund movement effects.nodeFundsDelta = nodeNfDelta; // Only node balance adjustments effects.newStatus = ChannelStatus.OPERATING; - effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED); + effects.newChallengeExpiry = 0; return effects; } @@ -404,7 +411,7 @@ library ChannelEngine { // Calculate effects effects.nodeFundsDelta = nodeNfDelta; // Only node balance adjustments effects.newStatus = ChannelStatus.OPERATING; - effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED); + effects.newChallengeExpiry = 0; return effects; } @@ -469,7 +476,8 @@ library ChannelEngine { // Calculate effects - may adjust node vault based on net flow delta effects.nodeFundsDelta = nodeNfDelta; - effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED); + effects.newStatus = ChannelStatus.OPERATING; + effects.newChallengeExpiry = 0; } else { revert IncorrectChannelStatus(); } @@ -523,7 +531,7 @@ library ChannelEngine { // Calculate effects - release all currently locked funds to node vault effects.nodeFundsDelta = nodeNfDelta; effects.newStatus = ChannelStatus.MIGRATED_OUT; - effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED); + effects.newChallengeExpiry = 0; effects.closeChannel = true; } else { revert IncorrectChannelStatus(); diff --git a/contracts/src/ChannelHub.sol b/contracts/src/ChannelHub.sol index 9621cff22..bdade7b6d 100644 --- a/contracts/src/ChannelHub.sol +++ b/contracts/src/ChannelHub.sol @@ -94,7 +94,8 @@ contract ChannelHub is IVault, ReentrancyGuard { error IncorrectStateIntent(); error IncorrectChannelStatus(); error ChallengerVersionTooLow(); - error OnlyNonHomeEscrowsCanBeChallenged(); + error NoChannelIdFound(); + error IncorrectChannelId(); struct ChannelMeta { ChannelStatus status; @@ -546,7 +547,6 @@ contract ChannelHub is IVault, ReentrancyGuard { // If version is higher, process the new state if (candidate.version > prevState.version) { - require(candidate.intent == StateIntent.OPERATE, IncorrectStateIntent()); _validateSignatures(channelId, candidate, user, node, def.approvedSignatureValidators); ChannelEngine.TransitionContext memory ctx = @@ -569,8 +569,6 @@ contract ChannelHub is IVault, ReentrancyGuard { } function closeChannel(bytes32 channelId, State calldata candidate) external payable { - require(candidate.intent == StateIntent.CLOSE, IncorrectStateIntent()); - ChannelMeta storage meta = _channels[channelId]; ChannelDefinition memory def = meta.definition; ChannelStatus status = meta.status; @@ -595,6 +593,7 @@ contract ChannelHub is IVault, ReentrancyGuard { } // Path 2: Cooperative closure with signed CLOSE state + require(candidate.intent == StateIntent.CLOSE, IncorrectStateIntent()); _validateSignatures(channelId, candidate, user, node, def.approvedSignatureValidators); ChannelEngine.TransitionContext memory ctx = @@ -639,9 +638,9 @@ contract ChannelHub is IVault, ReentrancyGuard { external { EscrowDepositMeta storage meta = _escrowDeposits[escrowId]; - require(!_isHomeChain(meta.channelId), OnlyNonHomeEscrowsCanBeChallenged()); - bytes32 channelId = meta.channelId; + require(channelId != bytes32(0), NoChannelIdFound()); + (ISignatureValidator validator, bytes calldata sigData) = _extractValidator(challengerSig, meta.node, meta.approvedSignatureValidators); _validateChallengerSignature(channelId, meta.initState, sigData, validator, meta.user, meta.node, challengerIdx); @@ -656,15 +655,25 @@ contract ChannelHub is IVault, ReentrancyGuard { emit EscrowDepositChallenged(escrowId, meta.initState, effects.newChallengeExpiry); } - function finalizeEscrowDeposit(bytes32 escrowId, State calldata candidate) external { + function finalizeEscrowDeposit(bytes32 channelId, bytes32 escrowId, State calldata candidate) external { + if (_isHomeChain(channelId)) { + // HOME CHAIN: Get user/node from channel definition + ChannelMeta storage channelMeta = _channels[channelId]; + _processHomeChainEscrowFinalize( + channelId, candidate, channelMeta.definition.user, channelMeta.definition.node + ); + emit EscrowDepositFinalizedOnHome(escrowId, channelId, candidate); + return; + } + + // NON-HOME CHAIN: Use escrow metadata EscrowDepositMeta storage meta = _escrowDeposits[escrowId]; + require(meta.channelId == channelId, IncorrectChannelId()); // Validate consistency address user = meta.user; address node = meta.node; - - bool isHomeChain = _isHomeChain(meta.channelId); EscrowStatus status = meta.status; - if (!isHomeChain && status == EscrowStatus.DISPUTED && block.timestamp > meta.challengeExpireAt) { + if (status == EscrowStatus.DISPUTED && block.timestamp > meta.challengeExpireAt) { // NON-HOME CHAIN: Unilateral finalization after challenge timeout meta.status = EscrowStatus.FINALIZED; uint256 lockedAmount = meta.lockedAmount; @@ -673,31 +682,24 @@ contract ChannelHub is IVault, ReentrancyGuard { _pushFunds(node, meta.initState.nonHomeLedger.token, lockedAmount); - emit EscrowDepositFinalized(escrowId, meta.channelId, candidate); + emit EscrowDepositFinalized(escrowId, channelId, candidate); return; } require(candidate.intent == StateIntent.FINALIZE_ESCROW_DEPOSIT, IncorrectStateIntent()); - if (_isHomeChain(meta.channelId)) { - _processHomeChainEscrowFinalize(meta.channelId, candidate, user, node); - emit EscrowDepositFinalizedOnHome(escrowId, meta.channelId, candidate); - return; - } else { - // NON-HOME CHAIN: Update via EscrowDepositEngine - _validateSignatures(meta.channelId, candidate, user, node, meta.approvedSignatureValidators); + // NON-HOME CHAIN: Update via EscrowDepositEngine + _validateSignatures(channelId, candidate, user, node, meta.approvedSignatureValidators); - EscrowDepositEngine.TransitionContext memory ctx = - _buildEscrowDepositContext(escrowId, _nodeBalances[node][candidate.nonHomeLedger.token]); - EscrowDepositEngine.TransitionEffects memory effects = - EscrowDepositEngine.validateTransition(ctx, candidate); + EscrowDepositEngine.TransitionContext memory ctx = + _buildEscrowDepositContext(escrowId, _nodeBalances[node][candidate.nonHomeLedger.token]); + EscrowDepositEngine.TransitionEffects memory effects = EscrowDepositEngine.validateTransition(ctx, candidate); - _applyEscrowDepositEffects( - escrowId, meta.channelId, candidate, effects, user, node, meta.approvedSignatureValidators - ); + _applyEscrowDepositEffects( + escrowId, channelId, candidate, effects, user, node, meta.approvedSignatureValidators + ); - emit EscrowDepositFinalized(escrowId, meta.channelId, candidate); - } + emit EscrowDepositFinalized(escrowId, channelId, candidate); } function initiateEscrowWithdrawal(ChannelDefinition calldata def, State calldata candidate) external { @@ -709,6 +711,7 @@ contract ChannelHub is IVault, ReentrancyGuard { bytes32 escrowId = Utils.getEscrowId(channelId, candidate.version); if (_isHomeChain(channelId)) { + // HOME CHAIN: Process through channel state, no escrow metadata _processHomeChainEscrowInitiate(channelId, candidate); emit EscrowWithdrawalInitiatedOnHome(escrowId, channelId, candidate); } else { @@ -729,13 +732,13 @@ contract ChannelHub is IVault, ReentrancyGuard { external { EscrowWithdrawalMeta storage meta = _escrowWithdrawals[escrowId]; - require(!_isHomeChain(meta.channelId), OnlyNonHomeEscrowsCanBeChallenged()); + bytes32 channelId = meta.channelId; + require(channelId != bytes32(0), NoChannelIdFound()); EscrowWithdrawalEngine.TransitionContext memory ctx = _buildEscrowWithdrawalContext(escrowId, meta.node); EscrowWithdrawalEngine.TransitionEffects memory effects = EscrowWithdrawalEngine.validateChallenge(ctx); // Validate challenger signature - bytes32 channelId = meta.channelId; address user = meta.user; address node = meta.node; (ISignatureValidator validator, bytes calldata sigData) = @@ -749,16 +752,25 @@ contract ChannelHub is IVault, ReentrancyGuard { emit EscrowWithdrawalChallenged(escrowId, meta.initState, effects.newChallengeExpiry); } - function finalizeEscrowWithdrawal(bytes32 escrowId, State calldata candidate) external { + function finalizeEscrowWithdrawal(bytes32 channelId, bytes32 escrowId, State calldata candidate) external { + if (_isHomeChain(channelId)) { + // HOME CHAIN: Get user/node from channel definition + ChannelMeta storage channelMeta = _channels[channelId]; + _processHomeChainEscrowFinalize( + channelId, candidate, channelMeta.definition.user, channelMeta.definition.node + ); + emit EscrowWithdrawalFinalizedOnHome(escrowId, channelId, candidate); + return; + } + + // NON-HOME CHAIN: Use escrow metadata EscrowWithdrawalMeta storage meta = _escrowWithdrawals[escrowId]; - bytes32 channelId = meta.channelId; + require(meta.channelId == channelId, IncorrectChannelId()); // Validate consistency address user = meta.user; address node = meta.node; - - bool isHomeChain = _isHomeChain(channelId); EscrowStatus status = meta.status; - if (!isHomeChain && status == EscrowStatus.DISPUTED && block.timestamp > meta.challengeExpireAt) { + if (status == EscrowStatus.DISPUTED && block.timestamp > meta.challengeExpireAt) { // NON-HOME CHAIN: Unilateral finalization after challenge timeout meta.status = EscrowStatus.FINALIZED; uint256 lockedAmount = meta.lockedAmount; @@ -767,29 +779,24 @@ contract ChannelHub is IVault, ReentrancyGuard { _pushFunds(node, meta.initState.nonHomeLedger.token, lockedAmount); - emit EscrowWithdrawalFinalized(escrowId, meta.channelId, candidate); + emit EscrowWithdrawalFinalized(escrowId, channelId, candidate); return; } require(candidate.intent == StateIntent.FINALIZE_ESCROW_WITHDRAWAL, IncorrectStateIntent()); - if (_isHomeChain(channelId)) { - _processHomeChainEscrowFinalize(channelId, candidate, user, node); - emit EscrowWithdrawalFinalizedOnHome(escrowId, channelId, candidate); - } else { - // Non-Home chain: Update via EscrowWithdrawalEngine - _validateSignatures(channelId, candidate, user, node, meta.approvedSignatureValidators); + // NON-HOME CHAIN: Update via EscrowWithdrawalEngine + _validateSignatures(channelId, candidate, user, node, meta.approvedSignatureValidators); - EscrowWithdrawalEngine.TransitionContext memory ctx = _buildEscrowWithdrawalContext(escrowId, node); - EscrowWithdrawalEngine.TransitionEffects memory effects = - EscrowWithdrawalEngine.validateTransition(ctx, candidate); + EscrowWithdrawalEngine.TransitionContext memory ctx = _buildEscrowWithdrawalContext(escrowId, node); + EscrowWithdrawalEngine.TransitionEffects memory effects = + EscrowWithdrawalEngine.validateTransition(ctx, candidate); - _applyEscrowWithdrawalEffects( - escrowId, channelId, candidate, effects, user, node, meta.approvedSignatureValidators - ); + _applyEscrowWithdrawalEffects( + escrowId, channelId, candidate, effects, user, node, meta.approvedSignatureValidators + ); - emit EscrowWithdrawalFinalized(escrowId, channelId, candidate); - } + emit EscrowWithdrawalFinalized(escrowId, channelId, candidate); } function initiateMigration(ChannelDefinition calldata def, State calldata candidate) external { @@ -1047,14 +1054,12 @@ contract ChannelHub is IVault, ReentrancyGuard { meta.status = effects.newStatus; } - if (effects.clearDispute) { - meta.status = ChannelStatus.OPERATING; - meta.challengeExpireAt = 0; + if (meta.challengeExpireAt != effects.newChallengeExpiry) { + meta.challengeExpireAt = effects.newChallengeExpiry; } if (effects.closeChannel) { meta.lockedFunds = 0; - meta.challengeExpireAt = 0; } } @@ -1124,11 +1129,7 @@ contract ChannelHub is IVault, ReentrancyGuard { } if (effects.updateInitState) { - meta.initState = candidate; - meta.channelId = channelId; - meta.user = user; - meta.node = node; - meta.approvedSignatureValidators = approvedSignatureValidators; + _initEscrowDepositMetadata(escrowId, channelId, candidate, user, node, approvedSignatureValidators); } if (effects.newUnlockAt > 0) { @@ -1184,11 +1185,7 @@ contract ChannelHub is IVault, ReentrancyGuard { } if (effects.updateInitState) { - meta.initState = candidate; - meta.channelId = channelId; - meta.user = user; - meta.node = node; - meta.approvedSignatureValidators = approvedSignatureValidators; + _initEscrowWithdrawalMetadata(escrowId, channelId, candidate, user, node, approvedSignatureValidators); } if (effects.newChallengeExpiry > 0) { @@ -1224,6 +1221,38 @@ contract ChannelHub is IVault, ReentrancyGuard { _purgeEscrowDeposits(); } + function _initEscrowDepositMetadata( + bytes32 escrowId, + bytes32 channelId, + State memory candidate, + address user, + address node, + uint256 approvedSignatureValidators + ) internal { + EscrowDepositMeta storage meta = _escrowDeposits[escrowId]; + meta.channelId = channelId; + meta.initState = candidate; + meta.user = user; + meta.node = node; + meta.approvedSignatureValidators = approvedSignatureValidators; + } + + function _initEscrowWithdrawalMetadata( + bytes32 escrowId, + bytes32 channelId, + State memory candidate, + address user, + address node, + uint256 approvedSignatureValidators + ) internal { + EscrowWithdrawalMeta storage meta = _escrowWithdrawals[escrowId]; + meta.channelId = channelId; + meta.initState = candidate; + meta.user = user; + meta.node = node; + meta.approvedSignatureValidators = approvedSignatureValidators; + } + function _requireValidDefinition(ChannelDefinition calldata def) internal pure { require(def.user != address(0), InvalidAddress()); require(def.node != address(0), InvalidAddress()); diff --git a/contracts/src/interfaces/ISignatureValidator.sol b/contracts/src/interfaces/ISignatureValidator.sol index c456937fc..2181aa6b1 100644 --- a/contracts/src/interfaces/ISignatureValidator.sol +++ b/contracts/src/interfaces/ISignatureValidator.sol @@ -22,6 +22,9 @@ ValidationResult constant VALIDATION_SUCCESS = ValidationResult.wrap(1); * construct the full message according to their specific signing scheme. */ interface ISignatureValidator { + error EmptyChannelId(); + error InvalidSignerAddress(); + /** * @notice Validates a participant's signature * @param channelId The channel identifier to be included in the signed message diff --git a/contracts/src/sigValidators/ECDSAValidator.sol b/contracts/src/sigValidators/ECDSAValidator.sol index f17ab9c51..5bee7b456 100644 --- a/contracts/src/sigValidators/ECDSAValidator.sol +++ b/contracts/src/sigValidators/ECDSAValidator.sol @@ -35,6 +35,9 @@ contract ECDSAValidator is ISignatureValidator { bytes calldata signature, address participant ) external pure returns (ValidationResult) { + require(channelId != bytes32(0), EmptyChannelId()); + require(participant != address(0), InvalidSignerAddress()); + bytes memory message = Utils.pack(channelId, signingData); if (EcdsaSignatureUtils.validateEcdsaSigner(message, signature, participant)) { return VALIDATION_SUCCESS; diff --git a/contracts/src/sigValidators/SessionKeyValidator.sol b/contracts/src/sigValidators/SessionKeyValidator.sol index 34c8288a3..0302e3cba 100644 --- a/contracts/src/sigValidators/SessionKeyValidator.sol +++ b/contracts/src/sigValidators/SessionKeyValidator.sol @@ -71,6 +71,9 @@ contract SessionKeyValidator is ISignatureValidator { bytes calldata signature, address participant ) external pure returns (ValidationResult) { + require(channelId != bytes32(0), EmptyChannelId()); + require(participant != address(0), InvalidSignerAddress()); + (SessionKeyAuthorization memory skAuth, bytes memory skSignature) = abi.decode(signature, (SessionKeyAuthorization, bytes)); diff --git a/contracts/test/ChannelHub_Base.t.sol b/contracts/test/ChannelHub_Base.t.sol index 7ae2cfc4b..a01bd3847 100644 --- a/contracts/test/ChannelHub_Base.t.sol +++ b/contracts/test/ChannelHub_Base.t.sol @@ -9,7 +9,7 @@ import {TestUtils, SESSION_KEY_VALIDATOR_ID} from "./TestUtils.sol"; import {ChannelHub} from "../src/ChannelHub.sol"; import {ECDSAValidator} from "../src/sigValidators/ECDSAValidator.sol"; import {SessionKeyValidator, SessionKeyAuthorization} from "../src/sigValidators/SessionKeyValidator.sol"; -import {State, StateIntent, Ledger} from "../src/interfaces/Types.sol"; +import {ChannelStatus, State, StateIntent, Ledger} from "../src/interfaces/Types.sol"; import {ISignatureValidator} from "../src/interfaces/ISignatureValidator.sol"; // forge-lint: disable-next-item(unsafe-typecast) @@ -197,32 +197,38 @@ contract ChannelHubTest_Base is Test { return state; } + function verifyChannelData( + bytes32 channelId, + ChannelStatus expectedStatus, + uint64 expectedVersion, + uint256 expectedChallengeExpiry, + string memory description + ) internal view { + (ChannelStatus status,, State memory latestState, uint256 challengeExpiry,) = cHub.getChannelData(channelId); + assertEq(uint8(status), uint8(expectedStatus), string.concat(description, ": Channel status: ")); + assertEq(latestState.version, expectedVersion, string.concat(description, ": Channel version: ")); + assertEq(challengeExpiry, expectedChallengeExpiry, string.concat(description, ": Challenge expiry: ")); + } + function verifyChannelState( bytes32 channelId, - uint256 expectedUserAllocation, - int256 expectedUserNetFlow, - uint256 expectedNodeAllocation, - int256 expectedNodeNetFlow, + uint256[2] memory allocations, + int256[2] memory netFlows, string memory description ) internal view { (,, State memory latestState,,) = cHub.getChannelData(channelId); assertEq( - latestState.homeLedger.userAllocation, - expectedUserAllocation, - string.concat("User allocation ", description) + latestState.homeLedger.userAllocation, allocations[0], string.concat(description, ": User allocation: ") ); - assertEq(latestState.homeLedger.userNetFlow, expectedUserNetFlow, string.concat("User net flow ", description)); + assertEq(latestState.homeLedger.userNetFlow, netFlows[0], string.concat(description, ": User net flow: ")); assertEq( - latestState.homeLedger.nodeAllocation, - expectedNodeAllocation, - string.concat("Node allocation ", description) + latestState.homeLedger.nodeAllocation, allocations[1], string.concat(description, ": Node allocation: ") ); - assertEq(latestState.homeLedger.nodeNetFlow, expectedNodeNetFlow, string.concat("Node net flow ", description)); + assertEq(latestState.homeLedger.nodeNetFlow, netFlows[1], string.concat(description, ": Node net flow: ")); uint256 nodeBalance = cHub.getAccountBalance(node, address(token)); - uint256 expectedNodeBalance = expectedNodeNetFlow < 0 - ? INITIAL_BALANCE + uint256(-expectedNodeNetFlow) - : INITIAL_BALANCE - uint256(expectedNodeNetFlow); - assertEq(nodeBalance, expectedNodeBalance, string.concat("Node vault balance ", description)); + uint256 expectedNodeBalance = + netFlows[1] < 0 ? INITIAL_BALANCE + uint256(-netFlows[1]) : INITIAL_BALANCE - uint256(netFlows[1]); + assertEq(nodeBalance, expectedNodeBalance, string.concat(description, ": Node balance: ")); } } diff --git a/contracts/test/ChannelHub_Challenge_Base.t.sol b/contracts/test/ChannelHub_Challenge_Base.t.sol new file mode 100644 index 000000000..92bccdf39 --- /dev/null +++ b/contracts/test/ChannelHub_Challenge_Base.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {ChannelHubTest_Base} from "./ChannelHub_Base.t.sol"; + +import {Utils} from "../src/Utils.sol"; +import {State, ChannelDefinition, StateIntent, Ledger, DEFAULT_SIG_VALIDATOR_ID} from "../src/interfaces/Types.sol"; +import {TestUtils} from "./TestUtils.sol"; + +/** + * @dev Base contract for challenge tests with common helper functions. + */ +abstract contract ChannelHubTest_Challenge_Base is ChannelHubTest_Base { + ChannelDefinition internal def; + bytes32 internal channelId; + State internal initState; + + uint64 constant NON_HOME_CHAIN_ID = 42; + address constant NON_HOME_TOKEN = address(0x42); + + function setUp() public virtual override { + super.setUp(); + + def = ChannelDefinition({ + challengeDuration: CHALLENGE_DURATION, + user: alice, + node: node, + nonce: NONCE, + approvedSignatureValidators: 0, + metadata: bytes32(0) + }); + + channelId = Utils.getChannelId(def, CHANNEL_HUB_VERSION); + } + + function createChannelWithDeposit() internal { + initState = State({ + version: 0, + intent: StateIntent.DEPOSIT, + metadata: bytes32(0), + homeLedger: Ledger({ + chainId: uint64(block.chainid), + token: address(token), + decimals: 18, + userAllocation: 1000, + userNetFlow: 1000, + nodeAllocation: 0, + nodeNetFlow: 0 + }), + nonHomeLedger: Ledger({ + chainId: 0, + token: address(0), + decimals: 0, + userAllocation: 0, + userNetFlow: 0, + nodeAllocation: 0, + nodeNetFlow: 0 + }), + userSig: "", + nodeSig: "" + }); + initState = mutualSignStateBothWithEcdsaValidator(initState, channelId, ALICE_PK); + + vm.prank(alice); + cHub.createChannel(def, initState); + } + + function signChallengeEip191WithEcdsaValidator(bytes32 channelId_, State memory state, uint256 privateKey) + internal + pure + returns (bytes memory) + { + bytes memory signingData = Utils.toSigningData(state); + bytes memory challengerSigningData = abi.encodePacked(signingData, "challenge"); + bytes memory message = Utils.pack(channelId_, challengerSigningData); + bytes memory signature = TestUtils.signEip191(vm, privateKey, message); + return abi.encodePacked(DEFAULT_SIG_VALIDATOR_ID, signature); + } +} diff --git a/contracts/test/ChannelHub_challengeHomeChain.t.sol b/contracts/test/ChannelHub_challengeHomeChain.t.sol new file mode 100644 index 000000000..6ce1b31ac --- /dev/null +++ b/contracts/test/ChannelHub_challengeHomeChain.t.sol @@ -0,0 +1,1101 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {ChannelHubTest_Challenge_Base} from "./ChannelHub_Challenge_Base.t.sol"; + +import {Utils} from "../src/Utils.sol"; +import { + State, + ChannelDefinition, + StateIntent, + Ledger, + ChannelStatus, + ParticipantIndex, + DEFAULT_SIG_VALIDATOR_ID +} from "../src/interfaces/Types.sol"; +import {ChannelHub} from "../src/ChannelHub.sol"; +import {ChannelEngine} from "../src/ChannelEngine.sol"; + +/* + * @dev This file uses integration / blackbox testing through ChannelHub to verify + * critical end-to-end challenge flows (signature validation, fund movements, storage updates, events). + * Complex state machine logic and edge cases are tested exhaustively in dedicated engine unit tests + * (ChannelEngine.t.sol, EscrowDepositEngine.t.sol, EscrowWithdrawalEngine.t.sol) for faster execution + * and better isolation. + */ +contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Challenge_Base { + /* + Test cases: + - a channel can be challenged with a newer state, which is enforced during challenge + - a channel can be challenged with existing state, which is NOT enforced the second time during challenge + - challenge is finalized (funds can be withdrawn) after `challengeExpireAt` time expires + - challenged "operating" state can be resolved with a newer state until `challengeExpireAt` time has NOT passed + - challenged state can NOT be resolved after `challengeExpireAt` time has passed + - a channel can NOT be challenged again during a challenge + - a channel can NOT be challenged with an earlier state + */ + + function setUp() public override { + super.setUp(); + createChannelWithDeposit(); + } + + function test_challengeWithNewerState_enforcesState() public { + // Off-chain: user transfers 100 to node + State memory stateV1 = + nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]); + stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK); + + // Off-chain: user transfers another 50 to node + State memory stateV2 = + nextState(stateV1, StateIntent.OPERATE, [uint256(850), uint256(0)], [int256(1000), int256(-150)]); + stateV2 = mutualSignStateBothWithEcdsaValidator(stateV2, channelId, ALICE_PK); + + // Node challenges with newer state V2, which should be enforced during challenge + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, stateV2, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, stateV2, challengerSig, ParticipantIndex.NODE); + + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + 2, + block.timestamp + CHALLENGE_DURATION, + "State V2 should be enforced during challenge" + ); + verifyChannelState( + channelId, + [uint256(850), uint256(0)], + [int256(1000), int256(-150)], + "State V2 should be enforced during challenge" + ); + } + + function test_challengeWithExistingState_notEnforcedAgain() public { + // Checkpoint a new state + State memory stateV1 = + nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]); + stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK); + + vm.prank(alice); + cHub.checkpointChannel(channelId, stateV1); + + // Verify state V1 is on-chain + (,, State memory latestStateBefore,,) = cHub.getChannelData(channelId); + assertEq(latestStateBefore.version, 1, "State version should be 1 before challenge"); + + // Node challenges with the same state V1 (already on-chain) + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, stateV1, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, stateV1, challengerSig, ParticipantIndex.NODE); + + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + 1, + block.timestamp + CHALLENGE_DURATION, + "State V1 should be enforced during challenge" + ); + verifyChannelState( + channelId, + [uint256(900), uint256(0)], + [int256(1000), int256(-100)], + "State V1 should be enforced during challenge" + ); + } + + function test_challengeFinalization_afterTimeout() public { + State memory stateV1 = + nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]); + stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK); + + // Challenge with current state + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, stateV1, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, stateV1, challengerSig, ParticipantIndex.NODE); + + vm.warp(block.timestamp + CHALLENGE_DURATION + 1); + + uint256 aliceBalanceBefore = token.balanceOf(alice); + uint256 nodeBalanceBefore = cHub.getAccountBalance(node, address(token)); + + // Finalize challenge by closing the channel (unilateral closure) + // When doing unilateral closure after timeout, any state works + vm.prank(alice); + cHub.closeChannel(channelId, initState); + + // Verify channel is CLOSED and funds were distributed according to last enforced state (V1) + verifyChannelData( + channelId, ChannelStatus.CLOSED, 1, 0, "Channel should be CLOSED after challenge finalization" + ); + + uint256 aliceBalanceAfter = token.balanceOf(alice); + uint256 nodeBalanceAfter = cHub.getAccountBalance(node, address(token)); + + assertEq(aliceBalanceAfter, aliceBalanceBefore + 900, "Alice should receive her allocation"); + // Node balance should remain unchanged because: + // 1. The node already received its 100 when the challenge was processed (nodeNetFlow -100 released funds) + // 2. During unilateral closure, node gets nodeAllocation (0) + assertEq( + nodeBalanceAfter, + nodeBalanceBefore, + "Node balance should remain unchanged (already received net flow during challenge)" + ); + } + + function test_resolveChallenge_withNewerState_beforeTimeout() public { + // State V1: user transfers 100 + State memory stateV1 = + nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]); + stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK); + + // Challenge with stateV1 + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, stateV1, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, stateV1, challengerSig, ParticipantIndex.NODE); + + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + 1, + block.timestamp + CHALLENGE_DURATION, + "Channel should be DISPUTED after challenge" + ); + + // State V2: user transfers another 50 (newer state to resolve challenge) + State memory stateV2 = + nextState(stateV1, StateIntent.OPERATE, [uint256(850), uint256(0)], [int256(1000), int256(-150)]); + stateV2 = mutualSignStateBothWithEcdsaValidator(stateV2, channelId, ALICE_PK); + + // Resolve challenge by checkpointing newer state (before timeout) + vm.prank(alice); + cHub.checkpointChannel(channelId, stateV2); + + verifyChannelData( + channelId, + ChannelStatus.OPERATING, + 2, + 0, + "Channel should be OPERATING after resolving challenge with newer state" + ); + verifyChannelState( + channelId, + [uint256(850), uint256(0)], + [int256(1000), int256(-150)], + "State V2 should be enforced after resolving challenge with newer state" + ); + } + + function test_revert_resolveChallenge_withOlderState_beforeTimeout() public { + // State V1: user transfers 100 + State memory stateV1 = + nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]); + stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK); + + // State V2: user receives 50 back + State memory stateV2 = + nextState(stateV1, StateIntent.OPERATE, [uint256(950), uint256(0)], [int256(1000), int256(-50)]); + stateV2 = mutualSignStateBothWithEcdsaValidator(stateV2, channelId, ALICE_PK); + + // Challenge with stateV2 + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, stateV2, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, stateV2, challengerSig, ParticipantIndex.NODE); + + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + 2, + block.timestamp + CHALLENGE_DURATION, + "Channel should be DISPUTED after challenge" + ); + + // Try to resolve with older state V1 (should fail) + vm.expectRevert(ChannelEngine.IncorrectStateVersion.selector); + vm.prank(alice); + cHub.checkpointChannel(channelId, stateV1); + } + + function test_revert_resolveChallenge_withNewerState_afterTimeout() public { + // State V1 + State memory stateV1 = + nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]); + stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK); + + // Challenge + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, stateV1, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, stateV1, challengerSig, ParticipantIndex.NODE); + + vm.warp(block.timestamp + CHALLENGE_DURATION + 1); + + // State V2: user transfers another 50 (newer state to resolve challenge) + State memory stateV2 = + nextState(stateV1, StateIntent.OPERATE, [uint256(850), uint256(0)], [int256(1000), int256(-150)]); + stateV2 = mutualSignStateBothWithEcdsaValidator(stateV2, channelId, ALICE_PK); + + // Cannot resolve challenge after timeout - must close channel instead + vm.expectRevert(ChannelEngine.ChallengeExpired.selector); + vm.prank(alice); + cHub.checkpointChannel(channelId, stateV2); + } + + function test_revert_challengeAlreadyChallengedChannel() public { + // First challenge + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, initState, challengerSig, ParticipantIndex.NODE); + + // Verify channel is DISPUTED + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + 0, + block.timestamp + CHALLENGE_DURATION, + "Channel should be DISPUTED after first challenge" + ); + + // Try to challenge again (should fail) + State memory stateV1 = + nextState(initState, StateIntent.OPERATE, [uint256(850), uint256(0)], [int256(1000), int256(-150)]); + stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK); + + bytes memory challengerSig2 = signChallengeEip191WithEcdsaValidator(channelId, stateV1, NODE_PK); + + vm.prank(node); + vm.expectRevert(ChannelHub.IncorrectChannelStatus.selector); + cHub.challengeChannel(channelId, stateV1, challengerSig2, ParticipantIndex.NODE); + } + + function test_revert_challengeWithOlderState() public { + // State V1 + State memory stateV1 = + nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]); + stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK); + + // Checkpoint V1 + vm.prank(alice); + cHub.checkpointChannel(channelId, stateV1); + + // Try to challenge with older state (initial) (should fail) + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initState, NODE_PK); + + vm.prank(node); + vm.expectRevert(ChannelHub.ChallengerVersionTooLow.selector); + cHub.challengeChannel(channelId, initState, challengerSig, ParticipantIndex.NODE); + } +} + +contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Challenge_Base { + /* + Test cases: + - a channel can be challenged with a newer state, which is enforced during challenge: + (new: InitiateEscrowDeposit, FinalizeEscrowDeposit) + - a channel can be challenged with existing state, which is NOT enforced the second time during challenge: + (existing: InitiateEscrowDeposit, FinalizeEscrowDeposit) + - a challenged channel can be resolved with "InitiateEscrowDeposit" / "FinalizeEscrowDeposit" state until `challengeExpireAt` time has NOT passed + */ + + bytes32 escrowId; + + uint64 initiateEscrowDepositVersion = 1; + State initiateEscrowDepositState; + uint64 finalizeEscrowDepositVersion = 2; + State finalizeEscrowDepositState; + + function setUp() public override { + super.setUp(); + createChannelWithDeposit(); + + initiateEscrowDepositState = nextState( + initState, + StateIntent.INITIATE_ESCROW_DEPOSIT, + [uint256(1000), uint256(500)], + [int256(1000), int256(500)], + NON_HOME_CHAIN_ID, + NON_HOME_TOKEN, + [uint256(500), uint256(0)], + [int256(500), int256(0)] + ); + initiateEscrowDepositState = + mutualSignStateBothWithEcdsaValidator(initiateEscrowDepositState, channelId, ALICE_PK); + + escrowId = Utils.getEscrowId(channelId, initiateEscrowDepositVersion); + + finalizeEscrowDepositState = nextState( + initiateEscrowDepositState, + StateIntent.FINALIZE_ESCROW_DEPOSIT, + [uint256(1500), uint256(0)], + [int256(1000), int256(500)], + NON_HOME_CHAIN_ID, + NON_HOME_TOKEN, + [uint256(0), uint256(0)], + [int256(500), int256(-500)] + ); + finalizeEscrowDepositState = + mutualSignStateBothWithEcdsaValidator(finalizeEscrowDepositState, channelId, ALICE_PK); + } + + function test_challenge_initiateEscrowDeposit_asNew() public { + bytes memory challengerSig = + signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, initiateEscrowDepositState, challengerSig, ParticipantIndex.NODE); + + // Verify channel is DISPUTED and initiateEscrowDepositState was enforced + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + initiateEscrowDepositVersion, + block.timestamp + CHALLENGE_DURATION, + "InitiateEscrowDepositState should be enforced" + ); + verifyChannelState( + channelId, + [uint256(1000), uint256(500)], + [int256(1000), int256(500)], + "InitiateEscrowDepositState should be enforced" + ); + } + + function test_challenge_initiateEscrowDeposit_asExisting() public { + vm.prank(alice); + cHub.initiateEscrowDeposit(def, initiateEscrowDepositState); + + // Challenge with already enforced initiateEscrowDepositState state + bytes memory challengerSig = + signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, initiateEscrowDepositState, challengerSig, ParticipantIndex.NODE); + + // Verify state is still initiateEscrowDepositState + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + initiateEscrowDepositVersion, + block.timestamp + CHALLENGE_DURATION, + "State should not be re-enforced" + ); + verifyChannelState( + channelId, [uint256(1000), uint256(500)], [int256(1000), int256(500)], "State should not be re-enforced" + ); + } + + function test_challenge_initiateEscrowDeposit_resolve() public { + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, initState, challengerSig, ParticipantIndex.NODE); + + // Resolve challenge with newer initiateEscrowDepositState state (before timeout) + vm.prank(alice); + cHub.initiateEscrowDeposit(def, initiateEscrowDepositState); + + // Verify challenge was resolved + verifyChannelData( + channelId, ChannelStatus.OPERATING, initiateEscrowDepositVersion, 0, "Challenge should be resolved" + ); + verifyChannelState( + channelId, + [uint256(1000), uint256(500)], + [int256(1000), int256(500)], + "initiateEscrowDepositState should be enforced" + ); + } + + function test_challenge_finalizeEscrowDeposit_asNew() public { + // First enforce INITIATE_ESCROW_DEPOSIT on-chain (required for FINALIZE to be valid) + vm.prank(alice); + cHub.initiateEscrowDeposit(def, initiateEscrowDepositState); + + // Now challenge with FINALIZE_ESCROW_DEPOSIT + bytes memory challengerSig = + signChallengeEip191WithEcdsaValidator(channelId, finalizeEscrowDepositState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, finalizeEscrowDepositState, challengerSig, ParticipantIndex.NODE); + + // Verify channel is DISPUTED and finalizeEscrowDepositState was enforced + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + finalizeEscrowDepositVersion, + block.timestamp + CHALLENGE_DURATION, + "FinalizeEscrowDepositState should be enforced" + ); + verifyChannelState( + channelId, + [uint256(1500), uint256(0)], + [int256(1000), int256(500)], + "finalizeEscrowDepositState should be enforced" + ); + } + + function test_challenge_finalizeEscrowDeposit_asExisting() public { + // First enforce INITIATE_ESCROW_DEPOSIT on-chain + vm.prank(alice); + cHub.initiateEscrowDeposit(def, initiateEscrowDepositState); + + // Then enforce FINALIZE_ESCROW_DEPOSIT on-chain + vm.prank(alice); + cHub.finalizeEscrowDeposit(channelId, escrowId, finalizeEscrowDepositState); + + // Challenge with already enforced finalizeEscrowDepositState state + bytes memory challengerSig = + signChallengeEip191WithEcdsaValidator(channelId, finalizeEscrowDepositState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, finalizeEscrowDepositState, challengerSig, ParticipantIndex.NODE); + + // Verify state is still finalizeEscrowDepositState + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + finalizeEscrowDepositVersion, + block.timestamp + CHALLENGE_DURATION, + "State should not be re-enforced" + ); + verifyChannelState( + channelId, [uint256(1500), uint256(0)], [int256(1000), int256(500)], "State should not be re-enforced" + ); + } + + function test_challenge_finalizeEscrowDeposit_resolve() public { + // First enforce INITIATE_ESCROW_DEPOSIT on-chain + vm.prank(alice); + cHub.initiateEscrowDeposit(def, initiateEscrowDepositState); + + // Challenge with older initiate state + bytes memory challengerSig = + signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, initiateEscrowDepositState, challengerSig, ParticipantIndex.NODE); + + // Resolve challenge with newer finalizeEscrowDepositState state (before timeout) + vm.prank(alice); + cHub.finalizeEscrowDeposit(channelId, escrowId, finalizeEscrowDepositState); + + // Verify challenge was resolved + verifyChannelData( + channelId, ChannelStatus.OPERATING, finalizeEscrowDepositVersion, 0, "Challenge should be resolved" + ); + verifyChannelState( + channelId, + [uint256(1500), uint256(0)], + [int256(1000), int256(500)], + "finalizeEscrowDepositState should be enforced" + ); + } + + function test_finalizeEscrowDeposit_resolve_newlyChallenged_initializeEscrowDeposit() public { + // Challenge with INITIATE_ESCROW_DEPOSIT state (without enforcing it on-chain first) + bytes memory challengerSig = + signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, initiateEscrowDepositState, challengerSig, ParticipantIndex.NODE); + + // Resolve challenge with finalizeEscrowDepositState state (before timeout) + vm.prank(alice); + cHub.finalizeEscrowDeposit(channelId, escrowId, finalizeEscrowDepositState); + + // Verify challenge was resolved + verifyChannelData( + channelId, ChannelStatus.OPERATING, finalizeEscrowDepositVersion, 0, "Challenge should be resolved" + ); + verifyChannelState( + channelId, + [uint256(1500), uint256(0)], + [int256(1000), int256(500)], + "finalizeEscrowDepositState should be enforced" + ); + } + + function test_revert_onChallengeEscrowDeposit() public { + // First enforce INITIATE_ESCROW_DEPOSIT on-chain + vm.prank(alice); + cHub.initiateEscrowDeposit(def, initiateEscrowDepositState); + + // Challenge with INITIATE_ESCROW_DEPOSIT state + bytes memory challengerSig = + signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK); + + vm.prank(node); + vm.expectRevert(ChannelHub.NoChannelIdFound.selector); + cHub.challengeEscrowDeposit(escrowId, challengerSig, ParticipantIndex.NODE); + } +} + +contract ChannelHubTest_Challenge_HomeChain_EscrowWithdrawal is ChannelHubTest_Challenge_Base { + /* + Test cases: + - a channel can be challenged with a newer state, which is enforced during challenge: + (new: InitiateEscrowWithdrawal, FinalizeEscrowWithdrawal) + - a channel can be challenged with existing state, which is NOT enforced the second time during challenge: + (existing: InitiateEscrowWithdrawal, FinalizeEscrowWithdrawal) + - a challenged channel can be resolved with "InitiateEscrowWithdrawal" / "FinalizeEscrowWithdrawal" state until `challengeExpireAt` time has NOT passed + */ + + bytes32 escrowId; + + uint64 initiateEscrowWithdrawalVersion = 1; + State initiateEscrowWithdrawalState; + uint64 finalizeEscrowWithdrawalVersion = 2; + State finalizeEscrowWithdrawalState; + + function setUp() public override { + super.setUp(); + createChannelWithDeposit(); + + initiateEscrowWithdrawalState = nextState( + initState, + StateIntent.INITIATE_ESCROW_WITHDRAWAL, + [uint256(1000), uint256(0)], + [int256(1000), int256(0)], + NON_HOME_CHAIN_ID, + NON_HOME_TOKEN, + [uint256(0), uint256(300)], + [int256(0), int256(300)] + ); + initiateEscrowWithdrawalState = + mutualSignStateBothWithEcdsaValidator(initiateEscrowWithdrawalState, channelId, ALICE_PK); + + escrowId = Utils.getEscrowId(channelId, initiateEscrowWithdrawalVersion); + + finalizeEscrowWithdrawalState = nextState( + initiateEscrowWithdrawalState, + StateIntent.FINALIZE_ESCROW_WITHDRAWAL, + [uint256(700), uint256(0)], + [int256(1000), int256(-300)], + NON_HOME_CHAIN_ID, + NON_HOME_TOKEN, + [uint256(0), uint256(0)], + [int256(-300), int256(300)] + ); + finalizeEscrowWithdrawalState = + mutualSignStateBothWithEcdsaValidator(finalizeEscrowWithdrawalState, channelId, ALICE_PK); + } + + function test_challenge_initiateEscrowWithdrawal_asNew() public { + bytes memory challengerSig = + signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, initiateEscrowWithdrawalState, challengerSig, ParticipantIndex.NODE); + + // Verify channel is DISPUTED and initiateEscrowWithdrawalState was enforced + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + initiateEscrowWithdrawalVersion, + block.timestamp + CHALLENGE_DURATION, + "InitiateEscrowWithdrawalState should be enforced" + ); + verifyChannelState( + channelId, + [uint256(1000), uint256(0)], + [int256(1000), int256(0)], + "InitiateEscrowWithdrawalState should be enforced" + ); + } + + function test_challenge_initiateEscrowWithdrawal_asExisting() public { + vm.prank(alice); + cHub.initiateEscrowWithdrawal(def, initiateEscrowWithdrawalState); + + // Challenge with already enforced initiateEscrowWithdrawalState state + bytes memory challengerSig = + signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, initiateEscrowWithdrawalState, challengerSig, ParticipantIndex.NODE); + + // Verify state is still initiateEscrowWithdrawalState + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + initiateEscrowWithdrawalVersion, + block.timestamp + CHALLENGE_DURATION, + "State should not be re-enforced" + ); + verifyChannelState( + channelId, [uint256(1000), uint256(0)], [int256(1000), int256(0)], "State should not be re-enforced" + ); + } + + function test_challenge_initiateEscrowWithdrawal_resolve() public { + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, initState, challengerSig, ParticipantIndex.NODE); + + // Resolve challenge with newer initiateEscrowWithdrawalState state (before timeout) + vm.prank(alice); + cHub.initiateEscrowWithdrawal(def, initiateEscrowWithdrawalState); + + // Verify challenge was resolved + verifyChannelData( + channelId, ChannelStatus.OPERATING, initiateEscrowWithdrawalVersion, 0, "Challenge should be resolved" + ); + verifyChannelState( + channelId, + [uint256(1000), uint256(0)], + [int256(1000), int256(0)], + "initiateEscrowWithdrawalState should be enforced" + ); + } + + function test_challenge_finalizeEscrowWithdrawal_asNew() public { + // INITIATE_ESCROW_WITHDRAWAL is NOT required to be enforced first on-chain + + // Challenge with FINALIZE_ESCROW_WITHDRAWAL + bytes memory challengerSig = + signChallengeEip191WithEcdsaValidator(channelId, finalizeEscrowWithdrawalState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, finalizeEscrowWithdrawalState, challengerSig, ParticipantIndex.NODE); + + // Verify channel is DISPUTED and finalizeEscrowWithdrawalState was enforced + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + finalizeEscrowWithdrawalVersion, + block.timestamp + CHALLENGE_DURATION, + "FinalizeEscrowWithdrawalState should be enforced" + ); + verifyChannelState( + channelId, + [uint256(700), uint256(0)], + [int256(1000), int256(-300)], + "finalizeEscrowWithdrawalState should be enforced" + ); + } + + function test_challenge_finalizeEscrowWithdrawal_asExisting() public { + // INITIATE_ESCROW_WITHDRAWAL is NOT required to be enforced first on-chain + + // Enforce FINALIZE_ESCROW_WITHDRAWAL on-chain + vm.prank(alice); + cHub.finalizeEscrowWithdrawal(channelId, escrowId, finalizeEscrowWithdrawalState); + + // Challenge with already enforced finalizeEscrowWithdrawalState state + bytes memory challengerSig = + signChallengeEip191WithEcdsaValidator(channelId, finalizeEscrowWithdrawalState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, finalizeEscrowWithdrawalState, challengerSig, ParticipantIndex.NODE); + + // Verify state is still finalizeEscrowWithdrawalState + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + finalizeEscrowWithdrawalVersion, + block.timestamp + CHALLENGE_DURATION, + "State should not be re-enforced" + ); + verifyChannelState( + channelId, [uint256(700), uint256(0)], [int256(1000), int256(-300)], "State should not be re-enforced" + ); + } + + function test_challenge_finalizeEscrowWithdrawal_resolve() public { + // INITIATE_ESCROW_WITHDRAWAL is NOT required to be enforced first on-chain + + // Challenge with older initiate state + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, initState, challengerSig, ParticipantIndex.NODE); + + // Resolve challenge with newer finalizeEscrowWithdrawalState state (before timeout) + vm.prank(alice); + cHub.finalizeEscrowWithdrawal(channelId, escrowId, finalizeEscrowWithdrawalState); + + // Verify challenge was resolved + verifyChannelData( + channelId, ChannelStatus.OPERATING, finalizeEscrowWithdrawalVersion, 0, "Challenge should be resolved" + ); + verifyChannelState( + channelId, + [uint256(700), uint256(0)], + [int256(1000), int256(-300)], + "finalizeEscrowWithdrawalState should be enforced" + ); + } + + function test_finalizeEscrowWithdrawal_resolve_newlyChallenged_initializeEscrowWithdrawal() public { + // Challenge with INITIATE_ESCROW_WITHDRAWAL state (without enforcing it on-chain first) + bytes memory challengerSig = + signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, initiateEscrowWithdrawalState, challengerSig, ParticipantIndex.NODE); + + // Resolve challenge with finalizeEscrowWithdrawalState state (before timeout) + vm.prank(alice); + cHub.finalizeEscrowWithdrawal(channelId, escrowId, finalizeEscrowWithdrawalState); + + // Verify challenge was resolved + verifyChannelData( + channelId, ChannelStatus.OPERATING, finalizeEscrowWithdrawalVersion, 0, "Challenge should be resolved" + ); + verifyChannelState( + channelId, + [uint256(700), uint256(0)], + [int256(1000), int256(-300)], + "finalizeEscrowWithdrawalState should be enforced" + ); + } + + function test_revert_onChallengeEscrowWithdrawal() public { + // First enforce INITIATE_ESCROW_WITHDRAWAL on-chain + vm.prank(alice); + cHub.initiateEscrowWithdrawal(def, initiateEscrowWithdrawalState); + + // Challenge with INITIATE_ESCROW_WITHDRAWAL state + bytes memory challengerSig = + signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK); + + vm.prank(node); + vm.expectRevert(ChannelHub.NoChannelIdFound.selector); + cHub.challengeEscrowWithdrawal(escrowId, challengerSig, ParticipantIndex.NODE); + } +} + +contract ChannelHubTest_Challenge_HomeChain_HomeMigration is ChannelHubTest_Challenge_Base { + /* + Test cases: + - a channel in Operate status can be challenged with initiated migration state + - a channel challenged with "InitiateMigration" state can be checkpointed calling "finalizeMigration" (-> MigratedOut status) + - a channel challenged with "InitiateMigration" state can be resolved with "operation" state + (although this should not happen in practice since the node should finalize migration instead of resolving with an older state, but just to be safe) + - a channel in Migrating_in status (empty channel after being called with `initiateMigration`) can be challenged with it + - a channel in Migrating_in status (empty channel after being called with `initiateMigration`) can be challenged with a newer Operation state + - a channel can NOT be challenged when in MIGRATED_OUT status + - a channel can NOT be challenged in Operating status with finalize migration state + */ + + uint64 initiateMigrationVersion = 1; + State initiateMigrationState; + uint64 finalizeMigrationVersion = 2; + State finalizeMigrationState; + uint64 operateAfterMigrationInitVersion = 2; + State operateAfterMigrationInitState; + + // New channel for testing NEW home chain behavior + ChannelDefinition newHomeDef; + bytes32 newHomeChannelId; + State newHomeInitiateMigrationState; + uint64 newHomeOperateVersion = 3; + State newHomeOperateState; + + function setUp() public override { + super.setUp(); + createChannelWithDeposit(); + + // INITIATE_MIGRATION state: + initiateMigrationState = nextState( + initState, + StateIntent.INITIATE_MIGRATION, + [uint256(700), uint256(0)], + [int256(1000), int256(-300)], + NON_HOME_CHAIN_ID, + NON_HOME_TOKEN, + [uint256(0), uint256(700)], // Node locks user allocation on new home + [int256(0), int256(700)] + ); + initiateMigrationState = mutualSignStateBothWithEcdsaValidator(initiateMigrationState, channelId, ALICE_PK); + + // FINALIZE_MIGRATION state: Allocations zero out on old home, user receives allocation on new home + finalizeMigrationState = nextState( + initiateMigrationState, + StateIntent.FINALIZE_MIGRATION, + [uint256(0), uint256(0)], // Old home: allocations zero out + [int256(1000), int256(-1000)], // Old home: net flows balance + NON_HOME_CHAIN_ID, + NON_HOME_TOKEN, + [uint256(700), uint256(0)], // New home: user receives allocation + [int256(0), int256(700)] + ); + // Swap home and non-home states as per migration protocol + Ledger memory temp = finalizeMigrationState.homeLedger; + finalizeMigrationState.homeLedger = finalizeMigrationState.nonHomeLedger; + finalizeMigrationState.nonHomeLedger = temp; + finalizeMigrationState = mutualSignStateBothWithEcdsaValidator(finalizeMigrationState, channelId, ALICE_PK); + + // OPERATE state after migration initiation (for resolving challenge) + operateAfterMigrationInitState = nextState( + initiateMigrationState, StateIntent.OPERATE, [uint256(650), uint256(0)], [int256(1000), int256(-350)] + ); + operateAfterMigrationInitState = + mutualSignStateBothWithEcdsaValidator(operateAfterMigrationInitState, channelId, ALICE_PK); + + // Setup for NEW home chain tests (migration IN) + // Create a new channel definition with different nonce + newHomeDef = ChannelDefinition({ + challengeDuration: CHALLENGE_DURATION, + user: alice, + node: node, + nonce: uint64(42), // Different nonce to create a new channel + approvedSignatureValidators: DEFAULT_SIG_VALIDATOR_ID, + metadata: bytes32(0) + }); + newHomeChannelId = Utils.getChannelId(newHomeDef, cHub.VERSION()); + + // INITIATE_MIGRATION state for NEW home chain (migration IN) + // homeLedger = OLD home chain (NON_HOME_CHAIN_ID) + // nonHomeLedger = NEW home chain (current chain) + newHomeInitiateMigrationState = State({ + version: initiateMigrationVersion, + intent: StateIntent.INITIATE_MIGRATION, + metadata: bytes32(0), + homeLedger: Ledger({ + chainId: NON_HOME_CHAIN_ID, + token: NON_HOME_TOKEN, + decimals: 18, + userAllocation: 500, + userNetFlow: 500, + nodeAllocation: 0, + nodeNetFlow: 0 + }), + nonHomeLedger: Ledger({ + chainId: uint64(block.chainid), + token: address(token), + decimals: 18, + userAllocation: 0, + userNetFlow: 0, + nodeAllocation: 500, // Node locks user allocation on new home + nodeNetFlow: 500 + }), + userSig: "", + nodeSig: "" + }); + newHomeInitiateMigrationState = + mutualSignStateBothWithEcdsaValidator(newHomeInitiateMigrationState, newHomeChannelId, ALICE_PK); + + // OPERATE state on NEW home chain after migration + // After initiateMigration on NEW home, ledgers are swapped, so homeLedger becomes current chain + // OPERATE requires userNfDelta == 0, so userNetFlow must stay 0 + newHomeOperateState = State({ + version: newHomeOperateVersion, + intent: StateIntent.OPERATE, + metadata: bytes32(0), + homeLedger: Ledger({ + chainId: uint64(block.chainid), + token: address(token), + decimals: 18, + userAllocation: 450, + userNetFlow: 0, + nodeAllocation: 0, + nodeNetFlow: 450 + }), + nonHomeLedger: Ledger({ + chainId: 0, + token: address(0), + decimals: 0, + userAllocation: 0, + userNetFlow: 0, + nodeAllocation: 0, + nodeNetFlow: 0 + }), + userSig: "", + nodeSig: "" + }); + newHomeOperateState = mutualSignStateBothWithEcdsaValidator(newHomeOperateState, newHomeChannelId, ALICE_PK); + } + + function test_challenge_initiateMigration_fromOperating() public { + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateMigrationState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, initiateMigrationState, challengerSig, ParticipantIndex.NODE); + + // Verify channel is DISPUTED and initiateMigrationState was enforced + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + initiateMigrationVersion, + block.timestamp + CHALLENGE_DURATION, + "InitiateMigrationState should be enforced" + ); + verifyChannelState( + channelId, + [uint256(700), uint256(0)], + [int256(1000), int256(-300)], + "InitiateMigrationState should be enforced" + ); + } + + function test_challenge_initiateMigration_resolve_withFinalizeMigration() public { + // Challenge with INITIATE_MIGRATION + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateMigrationState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, initiateMigrationState, challengerSig, ParticipantIndex.NODE); + + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + initiateMigrationVersion, + block.timestamp + CHALLENGE_DURATION, + "Channel should be DISPUTED" + ); + + // Resolve challenge with FINALIZE_MIGRATION (before timeout) + vm.prank(alice); + cHub.finalizeMigration(channelId, finalizeMigrationState); + + // Verify channel is MIGRATED_OUT and initiateMigrationState was enforced + verifyChannelData( + channelId, + ChannelStatus.MIGRATED_OUT, + finalizeMigrationVersion, + 0, + "finalizeMigration should resolve the challenge" + ); + } + + function test_challenge_initiateMigration_resolve_withOperate() public { + // Challenge with INITIATE_MIGRATION + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateMigrationState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(channelId, initiateMigrationState, challengerSig, ParticipantIndex.NODE); + + verifyChannelData( + channelId, + ChannelStatus.DISPUTED, + initiateMigrationVersion, + block.timestamp + CHALLENGE_DURATION, + "Channel should be DISPUTED" + ); + + // Resolve challenge with newer OPERATE state (before timeout) + // This is technically possible but shouldn't happen in practice as participants should NOT sign OPERATE state as direct successor of INITIATE_MIGRATION + vm.prank(alice); + cHub.checkpointChannel(channelId, operateAfterMigrationInitState); + + // Verify channel is back to OPERATING + verifyChannelData( + channelId, ChannelStatus.OPERATING, operateAfterMigrationInitVersion, 0, "Challenge should be resolved" + ); + verifyChannelState( + channelId, + [uint256(650), uint256(0)], + [int256(1000), int256(-350)], + "operateAfterMigrationInitState should be enforced" + ); + } + + function test_challenge_newHomeChain_withInitiateMigration_asExisting() public { + // Initiate migration IN on NEW home chain + vm.prank(alice); + cHub.initiateMigration(newHomeDef, newHomeInitiateMigrationState); + + // Verify channel is in MIGRATING_IN status + verifyChannelData( + newHomeChannelId, + ChannelStatus.MIGRATING_IN, + initiateMigrationVersion, + 0, + "newHomeInitiateMigrationState should be enforced" + ); + + // Challenge with the same INITIATE_MIGRATION state (already enforced) + bytes memory challengerSig = + signChallengeEip191WithEcdsaValidator(newHomeChannelId, newHomeInitiateMigrationState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(newHomeChannelId, newHomeInitiateMigrationState, challengerSig, ParticipantIndex.NODE); + + // Verify channel is DISPUTED and state is still version 0 + verifyChannelData( + newHomeChannelId, + ChannelStatus.DISPUTED, + initiateMigrationVersion, + block.timestamp + CHALLENGE_DURATION, + "initiateMigrationVersion should remain enforced" + ); + } + + function test_challenge_newHomeChain_withOperate_inMigratingIn() public { + // Initiate migration IN on NEW home chain + vm.prank(alice); + cHub.initiateMigration(newHomeDef, newHomeInitiateMigrationState); + + // Verify channel is in MIGRATING_IN status + verifyChannelData( + newHomeChannelId, + ChannelStatus.MIGRATING_IN, + initiateMigrationVersion, + 0, + "newHomeInitiateMigrationState should be enforced" + ); + + // Challenge with newer OPERATE state + bytes memory challengerSig = + signChallengeEip191WithEcdsaValidator(newHomeChannelId, newHomeOperateState, NODE_PK); + + vm.prank(node); + cHub.challengeChannel(newHomeChannelId, newHomeOperateState, challengerSig, ParticipantIndex.NODE); + + // Verify channel is DISPUTED and newHomeOperateState was enforced + verifyChannelData( + newHomeChannelId, + ChannelStatus.DISPUTED, + newHomeOperateVersion, + block.timestamp + CHALLENGE_DURATION, + "newHomeOperateState should start a challenge" + ); + verifyChannelState( + newHomeChannelId, + [uint256(450), uint256(0)], + [int256(0), int256(450)], + "newHomeOperateState should be enforced" + ); + } + + function test_revert_challenge_migratedOut() public { + // First initiate migration + vm.prank(alice); + cHub.initiateMigration(def, initiateMigrationState); + + // Then finalize migration to put channel in MIGRATED_OUT status + vm.prank(alice); + cHub.finalizeMigration(channelId, finalizeMigrationState); + + // Verify channel is in MIGRATED_OUT status + verifyChannelData( + channelId, ChannelStatus.MIGRATED_OUT, finalizeMigrationVersion, 0, "Channel should be MIGRATED_OUT" + ); + + // Try to challenge channel in MIGRATED_OUT status (should fail) + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, finalizeMigrationState, NODE_PK); + + vm.prank(node); + vm.expectRevert(ChannelHub.IncorrectChannelStatus.selector); + cHub.challengeChannel(channelId, finalizeMigrationState, challengerSig, ParticipantIndex.NODE); + } + + function test_revert_challenge_operating_withFinalizeMigration() public { + // Channel is in OPERATING status + // Try to challenge with FINALIZE_MIGRATION without INITIATE_MIGRATION first (should fail) + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, finalizeMigrationState, NODE_PK); + + vm.prank(node); + // NOTE: IncorrectHomeChainId check happens before IncorrectPreviousStateIntent check + // finalizeMigrationState has swapped ledgers, so homeLedger.chainId != block.chainid + vm.expectRevert(ChannelEngine.IncorrectHomeChainId.selector); + cHub.challengeChannel(channelId, finalizeMigrationState, challengerSig, ParticipantIndex.NODE); + } +} diff --git a/contracts/test/ChannelHub_challengeNonHomeChain.t.sol b/contracts/test/ChannelHub_challengeNonHomeChain.t.sol new file mode 100644 index 000000000..94c525641 --- /dev/null +++ b/contracts/test/ChannelHub_challengeNonHomeChain.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {ChannelHubTest_Base} from "./ChannelHub_Base.t.sol"; + +contract ChannelHubTest_Challenge_NonHomeChain_EscrowDeposit is ChannelHubTest_Base { + /* + - escrow deposit can be challenged until `unlockAt` time has NOT passed + - escrow deposit can not be challenged after `unlockAt` time has passed + - challenged escrow deposit funds can be withdrawn after `challengeExpireAt` time passes + - challenged escrow deposit can be resolved until `challengeExpireAt` time has passed with a newer finalization state, which removes challenge and unlock funds + - challenged escrow deposit can not be resolved if `challengeExpireAt` has passed + */ + + } + +contract ChannelHubTest_Challenge_NonHomeChain_EscrowWithdrawal is ChannelHubTest_Base { + /* + - escrow withdrawal can be challenged + - challenged escrow withdrawal funds can be withdrawn after `challengeExpireAt` time passes + - challenged escrow withdrawal can be resolved until `challengeExpireAt` time has passed with a newer finalization state, which removes challenge and unlock funds + - challenged escrow withdrawal can not be resolved if `challengeExpireAt` has passed + */ + + } + +contract ChannelHubTest_Challenge_NonHomeChain_Migration is ChannelHubTest_Base { + /* + - a channel in earlier state can be challenged with initiated migration state + - a channel in initiated migration state can be challenged with it + - a channel in earlier state can be challenged with finalize migration state + - a channel in finalize migration state can be challenged with it + */ + + } diff --git a/contracts/test/ChannelHub_crosschain.lifecycle.t.sol b/contracts/test/ChannelHub_crosschain.lifecycle.t.sol index 9dbfb9bbd..d7f3f6502 100644 --- a/contracts/test/ChannelHub_crosschain.lifecycle.t.sol +++ b/contracts/test/ChannelHub_crosschain.lifecycle.t.sol @@ -109,7 +109,9 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base { // Expected: user allocation = 958, user net flow = 1000, node allocation = 0, node net flow = -42 vm.prank(alice); cHub.initiateEscrowDeposit(def, state); - verifyChannelState(channelId, 958, 1000, 500, 458, "after cross chain deposit"); + verifyChannelState( + channelId, [uint256(958), uint256(500)], [int256(1000), int256(458)], "after cross chain deposit" + ); // finalize escrow deposit state = nextState( @@ -141,7 +143,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base { vm.prank(alice); cHub.withdrawFromChannel(channelId, state); - verifyChannelState(channelId, 1220, 750, 0, 470, "after withdrawal"); + verifyChannelState(channelId, [uint256(1220), uint256(0)], [int256(750), int256(470)], "after withdrawal"); // Verify user balance after withdrawal (withdrew 250) assertEq(token.balanceOf(alice), INITIAL_BALANCE - 750, "User balance after withdrawal"); @@ -199,7 +201,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base { // checkpoint on home chain vm.prank(alice); cHub.checkpointChannel(channelId, state); - verifyChannelState(channelId, 477, 750, 0, -273, "after checkpoint"); + verifyChannelState(channelId, [uint256(477), uint256(0)], [int256(750), int256(-273)], "after checkpoint"); // Verify user balance hasn't changed assertEq(token.balanceOf(alice), INITIAL_BALANCE - 750, "User balance after checkpoint"); @@ -258,7 +260,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base { cHub.finalizeMigration(channelId, state); // Verify channel is migrated out - verifyChannelState(channelId, 0, 750, 0, -750, "after migration"); + verifyChannelState(channelId, [uint256(0), uint256(0)], [int256(750), int256(-750)], "after migration"); // Verify user balance hasn't changed (migration doesn't move funds on home chain) assertEq(token.balanceOf(alice), INITIAL_BALANCE - 750, "User balance after migration"); @@ -356,7 +358,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base { state = mutualSignStateBothWithEcdsaValidator(state, bobChannelId, BOB_PK); vm.prank(node); - cHub.finalizeEscrowDeposit(escrowId, state); + cHub.finalizeEscrowDeposit(bobChannelId, escrowId, state); // Verify user balance after deposit finalized has NOT changed assertEq(token.balanceOf(bob), INITIAL_BALANCE - 500, "User balance after escrow deposit finalized"); @@ -469,7 +471,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base { state = mutualSignStateBothWithEcdsaValidator(state, bobChannelId, BOB_PK); vm.prank(node); - cHub.finalizeEscrowDeposit(escrowId, state); + cHub.finalizeEscrowDeposit(bobChannelId, escrowId, state); // Verify user balance after deposit finalized has NOT changed assertEq(token14dec.balanceOf(bob), 990 * 1e14, "User balance after escrow deposit finalized"); @@ -560,7 +562,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base { state = mutualSignStateBothWithEcdsaValidator(state, bobChannelId, BOB_PK); vm.prank(node); - cHub.finalizeEscrowWithdrawal(escrowId, state); + cHub.finalizeEscrowWithdrawal(bobChannelId, escrowId, state); // Verify user balance after withdrawal (withdrew 750) uint256 bobBalanceAfter = token.balanceOf(bob); @@ -660,7 +662,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base { state = mutualSignStateBothWithEcdsaValidator(state, bobChannelId, BOB_PK); vm.prank(node); - cHub.finalizeEscrowWithdrawal(escrowId, state); + cHub.finalizeEscrowWithdrawal(bobChannelId, escrowId, state); // Verify user received the withdrawal uint256 bobBalanceAfter = token8dec.balanceOf(bob); @@ -773,7 +775,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base { vm.prank(bob); cHub.withdrawFromChannel(bobChannelId, state); - verifyChannelState(bobChannelId, 61, -400, 0, 461, "after withdrawal"); + verifyChannelState(bobChannelId, [uint256(61), uint256(0)], [int256(-400), int256(461)], "after withdrawal"); // Verify user balance after withdrawal (withdrew 400) assertEq(token.balanceOf(bob), userBalanceBefore + 400, "User balance after withdrawal"); diff --git a/contracts/test/ChannelHub_singlechain.lifecycle.t.sol b/contracts/test/ChannelHub_singlechain.lifecycle.t.sol index 28e270261..6738c3fb4 100644 --- a/contracts/test/ChannelHub_singlechain.lifecycle.t.sol +++ b/contracts/test/ChannelHub_singlechain.lifecycle.t.sol @@ -81,7 +81,7 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base { // Expected: user allocation = 958, user net flow = 1000, node allocation = 0, node net flow = -42 vm.prank(alice); cHub.checkpointChannel(channelId, state); - verifyChannelState(channelId, 958, 1000, 0, -42, "after checkpoint"); + verifyChannelState(channelId, [uint256(958), uint256(0)], [int256(1000), int256(-42)], "after checkpoint"); // receive 24 (allocation increases by 24, node net flow increases by 24) state = nextState(state, StateIntent.OPERATE, [uint256(982), uint256(0)], [int256(1000), int256(-18)]); @@ -95,7 +95,7 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base { vm.prank(alice); cHub.depositToChannel(channelId, state); - verifyChannelState(channelId, 1482, 1500, 0, -18, "after deposit"); + verifyChannelState(channelId, [uint256(1482), uint256(0)], [int256(1500), int256(-18)], "after deposit"); // Verify user balance after first deposit (deposited 500 more) assertEq(token.balanceOf(alice), INITIAL_BALANCE - 1500, "User balance after first deposit"); @@ -116,7 +116,7 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base { vm.prank(alice); cHub.withdrawFromChannel(channelId, state); - verifyChannelState(channelId, 1379, 1400, 0, -21, "after withdrawal"); + verifyChannelState(channelId, [uint256(1379), uint256(0)], [int256(1400), int256(-21)], "after withdrawal"); // Verify user balance after first withdrawal (withdrew 100) assertEq(token.balanceOf(alice), INITIAL_BALANCE - 1400, "User balance after first withdrawal"); @@ -137,7 +137,7 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base { vm.prank(alice); cHub.depositToChannel(channelId, state); - verifyChannelState(channelId, 1586, 1600, 0, -14, "after second deposit"); + verifyChannelState(channelId, [uint256(1586), uint256(0)], [int256(1600), int256(-14)], "after second deposit"); // Verify user balance after second deposit (deposited 200 more) assertEq(token.balanceOf(alice), INITIAL_BALANCE - 1600, "User balance after second deposit"); @@ -170,7 +170,9 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base { vm.prank(alice); cHub.withdrawFromChannel(channelId, state); - verifyChannelState(channelId, 1289, 1300, 0, -11, "after second withdrawal"); + verifyChannelState( + channelId, [uint256(1289), uint256(0)], [int256(1300), int256(-11)], "after second withdrawal" + ); // Verify user balance after second withdrawal (withdrew 300) assertEq(token.balanceOf(alice), INITIAL_BALANCE - 1300, "User balance after second withdrawal"); @@ -259,7 +261,9 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base { cHub.createChannel(def, state); assertEq(token.balanceOf(alice), INITIAL_BALANCE, "User balance after OPERATE creation stays the same"); - verifyChannelState(channelId, 1000, 0, 0, 1000, "after create with OPERATE intent"); + verifyChannelState( + channelId, [uint256(1000), uint256(0)], [int256(0), int256(1000)], "after create with OPERATE intent" + ); (ChannelStatus status,, State memory latestState,, uint256 lockedFunds) = cHub.getChannelData(channelId); assertEq( uint8(status), uint8(ChannelStatus.OPERATING), "Channel created with OPERATE intent should be OPERATING" @@ -324,7 +328,9 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base { cHub.createChannel(def, state); assertEq(token.balanceOf(alice), INITIAL_BALANCE - 500, "User balance after DEPOSIT creation decreases"); - verifyChannelState(channelId, 1500, 500, 0, 1000, "after create with DEPOSIT intent"); + verifyChannelState( + channelId, [uint256(1500), uint256(0)], [int256(500), int256(1000)], "after create with DEPOSIT intent" + ); (ChannelStatus status,, State memory latestState,, uint256 lockedFunds) = cHub.getChannelData(channelId); assertEq( uint8(status), uint8(ChannelStatus.OPERATING), "Channel created with DEPOSIT intent should be OPERATING" @@ -389,7 +395,9 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base { cHub.createChannel(def, state); assertEq(token.balanceOf(alice), INITIAL_BALANCE + 500, "User balance after WITHDRAW creation increases"); - verifyChannelState(channelId, 500, -500, 0, 1000, "after create with WITHDRAW intent"); + verifyChannelState( + channelId, [uint256(500), uint256(0)], [int256(-500), int256(1000)], "after create with WITHDRAW intent" + ); (ChannelStatus status,, State memory latestState,, uint256 lockedFunds) = cHub.getChannelData(channelId); assertEq( uint8(status), uint8(ChannelStatus.OPERATING), "Channel created with WITHDRAW intent should be OPERATING" From bc528c9636605e69f28f4e7cab84e80ff28dbf46 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Tue, 3 Mar 2026 14:41:33 +0200 Subject: [PATCH 03/13] YNU-813: introduce basic rate limits (#600) --- clearnode/api/rate_limits.go | 51 ++++++++++ clearnode/api/rate_limits_test.go | 161 ++++++++++++++++++++++++++++++ clearnode/api/rpc_router.go | 40 ++++++-- clearnode/main.go | 15 ++- clearnode/runtime.go | 14 ++- pkg/rpc/node.go | 10 +- 6 files changed, 271 insertions(+), 20 deletions(-) create mode 100644 clearnode/api/rate_limits.go create mode 100644 clearnode/api/rate_limits_test.go diff --git a/clearnode/api/rate_limits.go b/clearnode/api/rate_limits.go new file mode 100644 index 000000000..28088bafe --- /dev/null +++ b/clearnode/api/rate_limits.go @@ -0,0 +1,51 @@ +package api + +import ( + "time" + + "github.com/erc7824/nitrolite/pkg/rpc" +) + +const ( + // rateLimitStorageKey is the key used to store the token bucket in connection storage. + rateLimitStorageKey = "rate_limiter" +) + +// tokenBucket holds the mutable state for per-connection rate limiting. +type tokenBucket struct { + tokens float64 + last time.Time +} + +// RateLimitMiddleware enforces per-connection rate limiting using a token bucket algorithm. +// It stores the token bucket in the connection's Storage for persistence across requests. +func (r *RPCRouter) RateLimitMiddleware(c *rpc.Context) { + bucket := &tokenBucket{ + tokens: r.rateLimitBurst, + last: time.Now().Add(-time.Second), + } + if val, ok := c.Storage.Get(rateLimitStorageKey); ok { + if b, ok := val.(*tokenBucket); ok { + bucket = b + } + } + + now := time.Now() + elapsed := now.Sub(bucket.last).Seconds() + bucket.last = now + + // Refill tokens based on elapsed time + bucket.tokens += elapsed * r.rateLimitPerSec + if bucket.tokens > r.rateLimitBurst { + bucket.tokens = r.rateLimitBurst + } + + if bucket.tokens < 1 { + c.Fail(nil, "rate limit exceeded") + return + } + bucket.tokens-- + c.Storage.Set(rateLimitStorageKey, bucket) + + c.Next() +} diff --git a/clearnode/api/rate_limits_test.go b/clearnode/api/rate_limits_test.go new file mode 100644 index 000000000..bc631fb85 --- /dev/null +++ b/clearnode/api/rate_limits_test.go @@ -0,0 +1,161 @@ +package api + +import ( + "testing" + "time" + + "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRateLimitMiddleware(t *testing.T) { + t.Parallel() + + newTestRouter := func(ratePerSec, burst float64) *RPCRouter { + return &RPCRouter{ + rateLimitPerSec: ratePerSec, + rateLimitBurst: burst, + } + } + + newTestContext := func(storage *rpc.SafeStorage, requestID uint64) *rpc.Context { + return &rpc.Context{ + Storage: storage, + Request: rpc.Message{RequestID: requestID}, + } + } + + isRateLimited := func(ctx *rpc.Context) bool { + err := ctx.Response.Error() + return err != nil && err.Error() == "rate limit exceeded" + } + + t.Run("allows requests within burst limit", func(t *testing.T) { + t.Parallel() + + router := newTestRouter(10, 5) + storage := rpc.NewSafeStorage() + + var allowed int + for i := 0; i < 5; i++ { + ctx := newTestContext(storage, uint64(i)) + router.RateLimitMiddleware(ctx) + if !isRateLimited(ctx) { + allowed++ + } + } + + assert.Equal(t, 5, allowed, "all requests within burst should be allowed") + }) + + t.Run("blocks requests exceeding burst", func(t *testing.T) { + t.Parallel() + + router := newTestRouter(10, 3) + storage := rpc.NewSafeStorage() + + var allowed, rateLimited int + for i := 0; i < 5; i++ { + ctx := newTestContext(storage, uint64(i)) + router.RateLimitMiddleware(ctx) + if isRateLimited(ctx) { + rateLimited++ + } else { + allowed++ + } + } + + assert.Equal(t, 3, allowed, "only burst amount of requests should be allowed") + assert.Equal(t, 2, rateLimited, "excess requests should be rate limited") + }) + + t.Run("returns rate limit error message", func(t *testing.T) { + t.Parallel() + + router := newTestRouter(10, 1) + storage := rpc.NewSafeStorage() + + // First request - allowed + ctx1 := newTestContext(storage, 1) + router.RateLimitMiddleware(ctx1) + require.False(t, isRateLimited(ctx1), "first request should be allowed") + + // Second request - should be rate limited + ctx2 := newTestContext(storage, 2) + router.RateLimitMiddleware(ctx2) + + require.True(t, isRateLimited(ctx2), "second request should be rate limited") + assert.Equal(t, "rate limit exceeded", ctx2.Response.Error().Error()) + }) + + t.Run("tokens refill over time", func(t *testing.T) { + t.Parallel() + + router := newTestRouter(100, 2) // 100 tokens per second = 1 token per 10ms + storage := rpc.NewSafeStorage() + + // Exhaust the bucket + for i := 0; i < 2; i++ { + ctx := newTestContext(storage, uint64(i)) + router.RateLimitMiddleware(ctx) + require.False(t, isRateLimited(ctx), "initial requests should be allowed") + } + + // This should be rate limited + ctx := newTestContext(storage, 100) + router.RateLimitMiddleware(ctx) + require.True(t, isRateLimited(ctx), "should be rate limited after exhausting bucket") + + // Wait for tokens to refill (need 1 token, at 100/sec = 10ms per token) + time.Sleep(15 * time.Millisecond) + + // Now it should work + ctx = newTestContext(storage, 101) + router.RateLimitMiddleware(ctx) + assert.False(t, isRateLimited(ctx), "should be allowed after refill") + }) + + t.Run("separate storage has separate buckets", func(t *testing.T) { + t.Parallel() + + router := newTestRouter(10, 2) + storage1 := rpc.NewSafeStorage() + storage2 := rpc.NewSafeStorage() + + // Exhaust storage1's bucket + for i := 0; i < 2; i++ { + ctx := newTestContext(storage1, uint64(i)) + router.RateLimitMiddleware(ctx) + } + + // storage1 should be rate limited + ctx1 := newTestContext(storage1, 100) + router.RateLimitMiddleware(ctx1) + assert.True(t, isRateLimited(ctx1), "storage1 should be rate limited") + + // storage2 should still have its own bucket + ctx2 := newTestContext(storage2, 200) + router.RateLimitMiddleware(ctx2) + assert.False(t, isRateLimited(ctx2), "storage2 should have its own bucket") + }) + + t.Run("bucket persists in storage", func(t *testing.T) { + t.Parallel() + + router := newTestRouter(10, 5) + storage := rpc.NewSafeStorage() + + // Make one request + ctx := newTestContext(storage, 1) + router.RateLimitMiddleware(ctx) + + // Check bucket is stored + val, ok := storage.Get(rateLimitStorageKey) + require.True(t, ok, "bucket should be stored") + + bucket, ok := val.(*tokenBucket) + require.True(t, ok, "stored value should be a tokenBucket") + assert.Less(t, bucket.tokens, 5.0, "tokens should have been consumed") + }) +} diff --git a/clearnode/api/rpc_router.go b/clearnode/api/rpc_router.go index 1a03dff6e..cfd5df4c8 100644 --- a/clearnode/api/rpc_router.go +++ b/clearnode/api/rpc_router.go @@ -21,26 +21,44 @@ type RPCRouter struct { Node rpc.Node lg log.Logger runtimeMetrics metrics.RuntimeMetricExporter + + rateLimitPerSec float64 + rateLimitBurst float64 +} + +type RPCRouterConfig struct { + NodeVersion string + MinChallenge uint32 + + MaxParticipants int + MaxSessionDataLen int + MaxAppMetadataLen int + MaxRebalanceSignedUpdates int + MaxSessionKeyIDs int + + RateLimitPerSec float64 + RateLimitBurst float64 } func NewRPCRouter( - nodeVersion string, - minChallenge uint32, + cfg RPCRouterConfig, node rpc.Node, signer sign.Signer, dbStore database.DatabaseStore, memoryStore memory.MemoryStore, runtimeMetrics metrics.RuntimeMetricExporter, logger log.Logger, - maxParticipants, maxSessionDataLen, maxAppMetadataLen, maxRebalanceSignedUpdates, maxSessionKeyIDs int, ) *RPCRouter { r := &RPCRouter{ - Node: node, - lg: logger.WithName("rpc-router"), - runtimeMetrics: runtimeMetrics, + Node: node, + lg: logger.WithName("rpc-router"), + runtimeMetrics: runtimeMetrics, + rateLimitPerSec: cfg.RateLimitPerSec, + rateLimitBurst: cfg.RateLimitBurst, } r.Node.Use(r.ObservabilityMiddleware) + r.Node.Use(r.RateLimitMiddleware) // Transaction wrapper helpers for each store type. // wrapWithMetrics executes fn inside a DB transaction with a metricStore wrapper, @@ -73,11 +91,11 @@ func NewRPCRouter( panic("failed to create channel wallet signer: " + err.Error()) } - channelV1Handler := channel_v1.NewHandler(useChannelV1StoreInTx, memoryStore, nodeChannelSigner, stateAdvancer, statePacker, nodeAddress, minChallenge, runtimeMetrics, maxSessionKeyIDs) + channelV1Handler := channel_v1.NewHandler(useChannelV1StoreInTx, memoryStore, nodeChannelSigner, stateAdvancer, statePacker, nodeAddress, cfg.MinChallenge, runtimeMetrics, cfg.MaxSessionKeyIDs) appSessionV1Handler := app_session_v1.NewHandler(useAppSessionV1StoreInTx, memoryStore, signer, stateAdvancer, statePacker, nodeAddress, runtimeMetrics, - maxParticipants, maxSessionDataLen, maxSessionKeyIDs, maxRebalanceSignedUpdates) - appsV1Handler := apps_v1.NewHandler(dbStore, maxAppMetadataLen) - nodeV1Handler := node_v1.NewHandler(memoryStore, nodeAddress, nodeVersion) + cfg.MaxParticipants, cfg.MaxSessionDataLen, cfg.MaxSessionKeyIDs, cfg.MaxRebalanceSignedUpdates) + appsV1Handler := apps_v1.NewHandler(dbStore, cfg.MaxAppMetadataLen) + nodeV1Handler := node_v1.NewHandler(memoryStore, nodeAddress, cfg.NodeVersion) userV1Handler := user_v1.NewHandler(dbStore) appSessionV1Group := r.Node.NewGroup(rpc.AppSessionsV1Group.String()) @@ -88,7 +106,7 @@ func NewRPCRouter( appSessionV1Group.Handle(rpc.AppSessionsV1GetAppSessionsMethod.String(), appSessionV1Handler.GetAppSessions) appSessionV1Group.Handle(rpc.AppSessionsV1SubmitSessionKeyStateMethod.String(), appSessionV1Handler.SubmitSessionKeyState) appSessionV1Group.Handle(rpc.AppSessionsV1GetLastKeyStatesMethod.String(), appSessionV1Handler.GetLastKeyStates) - if maxRebalanceSignedUpdates >= 2 { + if cfg.MaxRebalanceSignedUpdates >= 2 { appSessionV1Group.Handle(rpc.AppSessionsV1RebalanceAppSessionsMethod.String(), appSessionV1Handler.RebalanceAppSessions) } diff --git a/clearnode/main.go b/clearnode/main.go index 84adf725a..d374d9f28 100644 --- a/clearnode/main.go +++ b/clearnode/main.go @@ -31,9 +31,18 @@ func main() { ctx := context.Background() vl := bb.ValidationLimits - api.NewRPCRouter(bb.NodeVersion, bb.ChannelMinChallengeDuration, - bb.RpcNode, bb.StateSigner, bb.DbStore, bb.MemoryStore, bb.RuntimeMetrics, bb.Logger, - vl.MaxParticipants, vl.MaxSessionDataLen, vl.MaxAppMetadataLen, vl.MaxSignedUpdates, vl.MaxSessionKeyIDs) + rpcRouterCfg := api.RPCRouterConfig{ + NodeVersion: bb.NodeVersion, + MinChallenge: bb.ChannelMinChallengeDuration, + MaxParticipants: vl.MaxParticipants, + MaxSessionDataLen: vl.MaxSessionDataLen, + MaxAppMetadataLen: vl.MaxAppMetadataLen, + MaxRebalanceSignedUpdates: vl.MaxSignedUpdates, + MaxSessionKeyIDs: vl.MaxSessionKeyIDs, + RateLimitPerSec: bb.RateLimitPerSec, + RateLimitBurst: bb.RateLimitBurst, + } + api.NewRPCRouter(rpcRouterCfg, bb.RpcNode, bb.StateSigner, bb.DbStore, bb.MemoryStore, bb.RuntimeMetrics, bb.Logger) rpcListenAddr := ":7824" rpcListenEndpoint := "/ws" diff --git a/clearnode/runtime.go b/clearnode/runtime.go index fdff48027..0d47c4684 100644 --- a/clearnode/runtime.go +++ b/clearnode/runtime.go @@ -36,6 +36,8 @@ type Backbone struct { ChannelMinChallengeDuration uint32 BlockchainRPCs map[uint64]string ValidationLimits ValidationLimits + RateLimitPerSec float64 + RateLimitBurst float64 DbStore database.DatabaseStore MemoryStore memory.MemoryStore @@ -66,6 +68,10 @@ type Config struct { SignerKey string `yaml:"signer_key" env:"CLEARNODE_SIGNER_KEY"` // required when signer_type=key GCPKMSKeyName string `yaml:"gcp_kms_key_name" env:"CLEARNODE_GCP_KMS_KEY_NAME"` // required when signer_type=gcp-kms ValidationLimits ValidationLimits `yaml:"validation_limits"` + RateLimitPerSec float64 `yaml:"rate_limit_per_sec" env:"CLEARNODE_RATE_LIMIT_PER_SEC" env-default:"10"` + RateLimitBurst float64 `yaml:"rate_limit_burst" env:"CLEARNODE_RATE_LIMIT_BURST" env-default:"20"` + WsProcessBufferSize int `yaml:"ws_process_buffer_size" env:"CLEARNODE_WS_PROCESS_BUFFER_SIZE" env-default:"64"` + WsWriteBufferSize int `yaml:"ws_write_buffer_size" env:"CLEARNODE_WS_WRITE_BUFFER_SIZE" env-default:"64"` } // ValidationLimits defines configurable upper bounds for dynamic-length request fields. @@ -190,8 +196,10 @@ func InitBackbone() *Backbone { // ------------------------------------------------ rpcNode, err := rpc.NewWebsocketNode(rpc.WebsocketNodeConfig{ - Logger: logger, - ObserveConnections: runtimeMetrics.SetRPCConnections, + Logger: logger, + ObserveConnections: runtimeMetrics.SetRPCConnections, + WsConnProcessBufferSize: conf.WsProcessBufferSize, + WsConnWriteBufferSize: conf.WsWriteBufferSize, }) if err != nil { logger.Fatal("failed to initialize RPC node", "error", err) @@ -233,6 +241,8 @@ func InitBackbone() *Backbone { ChannelMinChallengeDuration: conf.ChannelMinChallengeDuration, BlockchainRPCs: blockchainRPCs, ValidationLimits: conf.ValidationLimits, + RateLimitPerSec: conf.RateLimitPerSec, + RateLimitBurst: conf.RateLimitBurst, DbStore: dbStore, MemoryStore: memoryStore, diff --git a/pkg/rpc/node.go b/pkg/rpc/node.go index c1a78c1b8..2679ab0dc 100644 --- a/pkg/rpc/node.go +++ b/pkg/rpc/node.go @@ -212,10 +212,12 @@ func (wn *WebsocketNode) ServeHTTP(w http.ResponseWriter, r *http.Request) { connectionID := uuid.NewString() connConfig := WebsocketConnectionConfig{ - ConnectionID: connectionID, - Origin: r.Header.Get("Origin"), - WebsocketConn: wsConnection, - Logger: wn.cfg.Logger, + ConnectionID: connectionID, + Origin: r.Header.Get("Origin"), + WebsocketConn: wsConnection, + Logger: wn.cfg.Logger, + ProcessBufferSize: wn.cfg.WsConnProcessBufferSize, + WriteBufferSize: wn.cfg.WsConnWriteBufferSize, } connection, err := NewWebsocketConnection(connConfig) if err != nil { From 422a2f96423192cc23774dd4c32c4a2ca1459d09 Mon Sep 17 00:00:00 2001 From: Dmytro Steblyna <80773046+dimast-x@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:04:34 +0000 Subject: [PATCH 04/13] YNU-812: Introduce Gated Actions (#601) * **New Features** * Action gateway enforcing per-action 24h allowances (scales with staked tokens and app count). * Action logging and per-blockchain staking to support allowances. * CLI: commands to view action allowances and manage app registry. * New RPC/API and SDK methods to fetch a wallet's action allowances. * **Configuration** * Environment-specific action gateway config and schema added. * **Breaking Changes** * App identifier fields renamed to application_id and app metadata now nested under an app definition in responses. --------- Co-authored-by: Anton Filonenko --- cerebro/commands.go | 96 ++++- cerebro/operator.go | 49 ++- clearnode/action_gateway/action_gateway.go | 209 ++++++++++ .../action_gateway/action_gateway_test.go | 356 ++++++++++++++++++ .../api/app_session_v1/create_app_session.go | 32 +- .../app_session_v1/create_app_session_test.go | 29 +- .../api/app_session_v1/get_app_definition.go | 2 +- .../app_session_v1/get_app_definition_test.go | 4 +- .../app_session_v1/get_app_sessions_test.go | 26 +- clearnode/api/app_session_v1/handler.go | 3 + clearnode/api/app_session_v1/interface.go | 8 + .../app_session_v1/rebalance_app_sessions.go | 14 +- .../rebalance_app_sessions_test.go | 71 +++- .../api/app_session_v1/submit_app_state.go | 14 +- .../app_session_v1/submit_app_state_test.go | 97 +++-- .../app_session_v1/submit_deposit_state.go | 71 ++-- .../submit_deposit_state_test.go | 35 +- clearnode/api/app_session_v1/testing.go | 38 ++ clearnode/api/app_session_v1/utils.go | 23 +- clearnode/api/apps_v1/get_apps_test.go | 8 +- clearnode/api/apps_v1/handler.go | 8 +- clearnode/api/apps_v1/interface.go | 20 +- clearnode/api/apps_v1/submit_app_version.go | 62 +-- .../api/apps_v1/submit_app_version_test.go | 11 +- clearnode/api/apps_v1/testing.go | 32 ++ clearnode/api/channel_v1/get_channels_test.go | 1 + .../api/channel_v1/get_escrow_channel_test.go | 1 + .../api/channel_v1/get_home_channel_test.go | 2 + .../api/channel_v1/get_latest_state_test.go | 2 + clearnode/api/channel_v1/handler.go | 3 + clearnode/api/channel_v1/interface.go | 8 + .../api/channel_v1/request_creation_test.go | 3 + clearnode/api/channel_v1/submit_state.go | 7 +- clearnode/api/channel_v1/submit_state_test.go | 9 + clearnode/api/channel_v1/testing.go | 39 ++ clearnode/api/metric_store.go | 2 +- clearnode/api/rpc_router.go | 17 +- .../api/user_v1/get_action_allowances.go | 59 +++ .../api/user_v1/get_action_allowances_test.go | 145 +++++++ clearnode/api/user_v1/handler.go | 10 +- clearnode/api/user_v1/interface.go | 17 + clearnode/api/user_v1/testing.go | 33 ++ .../chart/config/prod/action_gateway.yaml | 19 + clearnode/chart/config/prod/clearnode.yaml | 2 +- .../chart/config/sandbox/action_gateway.yaml | 19 + clearnode/chart/config/sandbox/clearnode.yaml | 2 +- .../chart/config/uat/action_gateway.yaml | 19 + clearnode/chart/config/uat/clearnode.yaml | 2 +- .../chart/config/v1-rc/action_gateway.yaml | 19 + clearnode/chart/config/v1-rc/clearnode.yaml | 2 +- clearnode/chart/templates/configmap.yaml | 2 + clearnode/chart/values.yaml | 4 +- .../20251222000000_initial_schema.sql | 30 +- .../config/schemas/action_gateway_schema.yaml | 31 ++ clearnode/main.go | 2 +- clearnode/runtime.go | 12 + clearnode/store/database/action_log.go | 91 +++++ clearnode/store/database/action_log_test.go | 249 ++++++++++++ clearnode/store/database/app.go | 10 + clearnode/store/database/app_ledger_test.go | 42 +-- clearnode/store/database/app_session.go | 2 +- .../database/app_session_key_state_test.go | 24 +- clearnode/store/database/app_session_test.go | 98 ++--- clearnode/store/database/database.go | 2 +- clearnode/store/database/interface.go | 25 ++ .../test/postgres_integration_test.go | 26 +- clearnode/store/database/testing.go | 4 +- clearnode/store/database/user_staked.go | 75 ++++ clearnode/store/database/utils.go | 20 +- clearnode/stress/app_session.go | 8 +- docs/api.yaml | 49 ++- pkg/app/app_session_v1.go | 60 +-- pkg/app/app_session_v1_test.go | 4 +- pkg/core/types.go | 76 +++- pkg/rpc/api.go | 12 + pkg/rpc/client.go | 9 + pkg/rpc/client_test.go | 11 +- pkg/rpc/error.go | 18 + pkg/rpc/methods.go | 7 +- pkg/rpc/types.go | 26 +- sdk/go/client_test.go | 16 +- sdk/go/examples/app_sessions/lifecycle.go | 4 +- sdk/go/user.go | 44 +++ sdk/go/utils.go | 39 +- sdk/go/utils_test.go | 20 +- .../src/components/WalletDashboard.tsx | 9 +- sdk/ts/src/app/packing.ts | 4 +- sdk/ts/src/app/types.ts | 6 +- sdk/ts/src/client.ts | 21 ++ sdk/ts/src/core/types.ts | 7 + sdk/ts/src/rpc/api.ts | 11 + sdk/ts/src/rpc/client.ts | 7 + sdk/ts/src/rpc/methods.ts | 1 + sdk/ts/src/rpc/types.ts | 18 + sdk/ts/src/utils.ts | 32 +- 95 files changed, 2607 insertions(+), 421 deletions(-) create mode 100644 clearnode/action_gateway/action_gateway.go create mode 100644 clearnode/action_gateway/action_gateway_test.go create mode 100644 clearnode/api/user_v1/get_action_allowances.go create mode 100644 clearnode/api/user_v1/get_action_allowances_test.go create mode 100644 clearnode/chart/config/prod/action_gateway.yaml create mode 100644 clearnode/chart/config/sandbox/action_gateway.yaml create mode 100644 clearnode/chart/config/uat/action_gateway.yaml create mode 100644 clearnode/chart/config/v1-rc/action_gateway.yaml create mode 100644 clearnode/config/schemas/action_gateway_schema.yaml create mode 100644 clearnode/store/database/action_log.go create mode 100644 clearnode/store/database/action_log_test.go create mode 100644 clearnode/store/database/user_staked.go diff --git a/cerebro/commands.go b/cerebro/commands.go index 822ccb14e..78ce6fa6e 100644 --- a/cerebro/commands.go +++ b/cerebro/commands.go @@ -52,12 +52,18 @@ NODE INFORMATION (Base Client) USER QUERIES (Base Client) balances [wallet] Get user balances (defaults to configured wallet) transactions [wallet] Get transaction history (defaults to configured wallet) + action-allowances [wallet] Get action allowances (defaults to configured wallet) LOW-LEVEL STATE MANAGEMENT (Base Client) state [wallet] Get latest state (wallet defaults to configured) home-channel [wallet] Get home channel (wallet defaults to configured) escrow-channel Get escrow channel by ID +APP REGISTRY + app-info Show application details + my-apps List your registered applications + register-app [no-approval] Register a new application + LOW-LEVEL APP SESSIONS (Base Client) app-sessions List app sessions @@ -714,6 +720,90 @@ func (o *Operator) listTransactions(ctx context.Context, wallet string) { } } +func (o *Operator) getActionAllowances(ctx context.Context, wallet string) { + allowances, err := o.client.GetActionAllowances(ctx, wallet) + if err != nil { + fmt.Printf("ERROR: Failed to get action allowances: %v\n", err) + return + } + + fmt.Printf("Action Allowances for %s\n", wallet) + fmt.Println("========================================") + if len(allowances) == 0 { + fmt.Println("No action allowances found") + return + } + + for _, a := range allowances { + fmt.Printf("- %s\n", a.GatedAction) + fmt.Printf(" Window: %s\n", a.TimeWindow) + fmt.Printf(" Used: %d / %d\n", a.Used, a.Allowance) + remaining := uint64(0) + if a.Allowance > a.Used { + remaining = a.Allowance - a.Used + } + fmt.Printf(" Remaining: %d\n", remaining) + } +} + +// ============================================================================ +// App Registry +// ============================================================================ + +func (o *Operator) getApps(ctx context.Context, appID *string, ownerWallet *string) { + fmt.Println("Fetching registered applications...") + + apps, _, err := o.client.GetApps(ctx, &sdk.GetAppsOptions{ + AppID: appID, + OwnerWallet: ownerWallet, + }) + if err != nil { + fmt.Printf("ERROR: Failed to get apps: %v\n", err) + return + } + + if len(apps) == 0 { + fmt.Println("No applications found.") + return + } + + fmt.Printf("Found %d application(s):\n\n", len(apps)) + for _, a := range apps { + fmt.Printf(" App ID: %s\n", a.App.ID) + fmt.Printf(" Owner: %s\n", a.App.OwnerWallet) + fmt.Printf(" Version: %d\n", a.App.Version) + if a.App.CreationApprovalNotRequired { + fmt.Println(" Approval: Not required") + } else { + fmt.Println(" Approval: Required") + } + if a.App.Metadata != "" { + fmt.Printf(" Metadata: %s\n", a.App.Metadata) + } + fmt.Printf(" Created: %s\n", a.CreatedAt.Format("2006-01-02 15:04:05")) + fmt.Printf(" Updated: %s\n", a.UpdatedAt.Format("2006-01-02 15:04:05")) + fmt.Println() + } +} + +func (o *Operator) registerApp(ctx context.Context, appID, metadata string, creationApprovalNotRequired bool) { + fmt.Printf("Registering application: %s...\n", appID) + + err := o.client.RegisterApp(ctx, appID, metadata, creationApprovalNotRequired) + if err != nil { + fmt.Printf("ERROR: Failed to register app: %v\n", err) + return + } + + fmt.Println("SUCCESS: Application registered") + fmt.Printf(" App ID: %s\n", appID) + if creationApprovalNotRequired { + fmt.Println(" Approval: Not required for session creation") + } else { + fmt.Println(" Approval: Required for session creation") + } +} + // ============================================================================ // Low-Level State Management (Base Client) // ============================================================================ @@ -774,10 +864,10 @@ func (o *Operator) listAppSessions(ctx context.Context, wallet string) { for _, session := range sessions { fmt.Printf("\n- Session %s\n", session.AppSessionID) fmt.Printf(" Version: %d\n", session.Version) - fmt.Printf(" Nonce: %d\n", session.Nonce) - fmt.Printf(" Quorum: %d\n", session.Quorum) + fmt.Printf(" Nonce: %d\n", session.AppDefinition.Nonce) + fmt.Printf(" Quorum: %d\n", session.AppDefinition.Quorum) fmt.Printf(" Closed: %v\n", session.IsClosed) - fmt.Printf(" Participants: %d\n", len(session.Participants)) + fmt.Printf(" Participants: %d\n", len(session.AppDefinition.Participants)) fmt.Printf(" Allocations: %d\n", len(session.Allocations)) } } diff --git a/cerebro/operator.go b/cerebro/operator.go index 47d7bf738..bb7a3f7e5 100644 --- a/cerebro/operator.go +++ b/cerebro/operator.go @@ -168,12 +168,18 @@ func (o *Operator) complete(d prompt.Document) []prompt.Suggest { // User queries {Text: "balances", Description: "Get user balances"}, {Text: "transactions", Description: "Get transaction history"}, + {Text: "action-allowances", Description: "Get action allowances"}, // State management {Text: "state", Description: "Get latest state"}, {Text: "home-channel", Description: "Get home channel"}, {Text: "escrow-channel", Description: "Get escrow channel"}, + // App registry + {Text: "app-info", Description: "Show application details"}, + {Text: "my-apps", Description: "List your registered applications"}, + {Text: "register-app", Description: "Register a new application"}, + // App sessions (Base Client - Low-level) {Text: "app-sessions", Description: "List app sessions"}, @@ -211,7 +217,7 @@ func (o *Operator) complete(d prompt.Document) []prompt.Suggest { case "transfer": // transfer return o.getWalletSuggestion() - case "balances", "transactions": + case "balances", "transactions", "action-allowances": return o.getWalletSuggestion() case "assets": return o.getChainSuggestions() @@ -452,6 +458,47 @@ func (o *Operator) Execute(s string) { } o.getEscrowChannel(ctx, args[1]) + // App registry + case "app-info": + if len(args) < 2 { + fmt.Println("ERROR: Usage: app-info ") + return + } + o.getApps(ctx, &args[1], nil) + + case "my-apps": + wallet := o.getImportedWalletAddress() + if wallet == "" { + fmt.Println("ERROR: No wallet configured. Use 'import wallet' first.") + return + } + o.getApps(ctx, nil, &wallet) + + case "register-app": + if len(args) < 2 { + fmt.Println("ERROR: Usage: register-app [no-approval]") + fmt.Println("INFO: Pass 'no-approval' as second arg to allow session creation without owner approval") + return + } + noApproval := len(args) >= 3 && args[2] == "no-approval" + o.registerApp(ctx, args[1], "", noApproval) + + // User action allowances + case "action-allowances": + wallet := "" + if len(args) >= 2 { + wallet = args[1] + } else { + wallet = o.getImportedWalletAddress() + if wallet == "" { + fmt.Println("ERROR: Usage: action-allowances ") + fmt.Println("INFO: No wallet configured. Use 'import wallet' first or specify a wallet address.") + return + } + fmt.Printf("INFO: Using configured wallet: %s\n", wallet) + } + o.getActionAllowances(ctx, wallet) + // App sessions case "app-sessions": wallet := o.getImportedWalletAddress() diff --git a/clearnode/action_gateway/action_gateway.go b/clearnode/action_gateway/action_gateway.go new file mode 100644 index 000000000..a0b19e280 --- /dev/null +++ b/clearnode/action_gateway/action_gateway.go @@ -0,0 +1,209 @@ +package action_gateway + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "time" + + "github.com/erc7824/nitrolite/pkg/core" + "github.com/shopspring/decimal" + "go.yaml.in/yaml/v2" +) + +const ( + actionGatewayFileName = "action_gateway.yaml" + defaultTimeWindow = 24 * time.Hour +) + +type ActionLimitConfig struct { + LevelStepTokens decimal.Decimal `yaml:"level_step_tokens"` + AppCost decimal.Decimal `yaml:"app_cost"` + ActionGates map[core.GatedAction]ActionGateConfig `yaml:"action_gates"` +} + +type ActionGateConfig struct { + FreeActionsAllowance uint64 `yaml:"free_actions_allowance"` + IncreasePerLevel uint64 `yaml:"increase_per_level"` +} + +type ActionGateway struct { + cfg ActionLimitConfig +} + +func NewActionGateway(cfg ActionLimitConfig) (*ActionGateway, error) { + if !cfg.LevelStepTokens.IsPositive() { + return nil, errors.New("LevelStepTokens must be greater than zero") + } + if !cfg.AppCost.IsPositive() { + return nil, errors.New("AppCost must be greater than zero") + } + + seenIDs := make(map[uint8]core.GatedAction, len(cfg.ActionGates)) + for action := range cfg.ActionGates { + id := action.ID() + if id == 0 { + return nil, fmt.Errorf("unknown action_gates key: %q", action) + } + if prev, exists := seenIDs[id]; exists && prev != action { + return nil, fmt.Errorf("duplicate gated action id %d for %q and %q", id, prev, action) + } + seenIDs[id] = action + } + + return &ActionGateway{ + cfg: cfg, + }, nil +} + +func NewActionGatewayFromYaml(configDirPath string) (*ActionGateway, error) { + assetsPath := filepath.Join(configDirPath, actionGatewayFileName) + f, err := os.Open(assetsPath) + if err != nil { + return nil, err + } + defer f.Close() + + var cfg ActionLimitConfig + if err := yaml.NewDecoder(f).Decode(&cfg); err != nil { + return nil, err + } + + return NewActionGateway(cfg) +} + +type Store interface { + // GetAppCount returns the total number of applications owned by a specific wallet. + GetAppCount(ownerWallet string) (uint64, error) + + // GetTotalUserStaked returns the total staked amount for a user across all blockchains. + GetTotalUserStaked(wallet string) (decimal.Decimal, error) + + // RecordAction inserts a new action log entry for a user. + RecordAction(wallet string, gatedAction core.GatedAction) error + + // GetUserActionCount returns the number of actions matching the given wallet and gated action + // within the specified time window (counting backwards from now). + GetUserActionCount(wallet string, gatedAction core.GatedAction, window time.Duration) (uint64, error) + + // GetUserActionCounts returns a map of gated actions to their respective counts for a user within the specified time window. + GetUserActionCounts(userWallet string, window time.Duration) (map[core.GatedAction]uint64, error) +} + +func (a *ActionGateway) AllowAction(tx Store, userAddress string, gatedAction core.GatedAction) error { + if _, ok := a.cfg.ActionGates[gatedAction]; !ok { + return nil + } + + stakedTokens, err := tx.GetTotalUserStaked(userAddress) + if err != nil { + return fmt.Errorf("failed to get user staked amount: %w", err) + } + + remainingStaked := stakedTokens + if stakedTokens.IsPositive() { + appCount, err := tx.GetAppCount(userAddress) + if err != nil { + return fmt.Errorf("failed to get app count: %w", err) + } + + maintenanceCost := a.cfg.AppCost.Mul(decimal.NewFromUint64(appCount)) + remainingStaked = stakedTokens.Sub(maintenanceCost) + } + + allowance := a.stakedTo24hActionsAllowance(gatedAction, remainingStaked) + + usedCount, err := tx.GetUserActionCount(userAddress, gatedAction, defaultTimeWindow) + if err != nil { + return fmt.Errorf("failed to get user action count: %w", err) + } + + if usedCount >= allowance { + return fmt.Errorf("action %s limit reached: used %d of %d allowed in 24h", gatedAction, usedCount, allowance) + } + + if err := tx.RecordAction(userAddress, gatedAction); err != nil { + return fmt.Errorf("failed to record action: %w", err) + } + + return nil +} + +func (v *ActionGateway) AllowAppRegistration(tx Store, userAddress string) error { + stakedTokens, err := tx.GetTotalUserStaked(userAddress) + if err != nil { + return fmt.Errorf("failed to get user staked amount: %w", err) + } + if stakedTokens.IsZero() { + return errors.New("cannot register an app with zero staked tokens") + } + + appCount, err := tx.GetAppCount(userAddress) + if err != nil { + return fmt.Errorf("failed to get app count: %w", err) + } + + maintenanceCost := v.cfg.AppCost.Mul(decimal.NewFromUint64(appCount + 1)) + if stakedTokens.LessThan(maintenanceCost) { + return fmt.Errorf("not enough staked tokens to register a new app: staked %s, required %s", stakedTokens.String(), maintenanceCost.String()) + } + + return nil +} + +// stakedTo24hActionsAllowance returns the number of executions allowed in 24 hours for a specific gated action. +func (a *ActionGateway) stakedTo24hActionsAllowance(gatedAction core.GatedAction, remainingStaked decimal.Decimal) uint64 { + actionLinitsConfig, ok := a.cfg.ActionGates[gatedAction] + if !ok { + return 0 + } + actionAllowance := uint64(actionLinitsConfig.FreeActionsAllowance) + if remainingStaked.IsPositive() { + levels := uint64(remainingStaked.Div(a.cfg.LevelStepTokens).IntPart()) + actionAllowance += actionLinitsConfig.IncreasePerLevel * levels + } + return actionAllowance +} + +// GetUserAllowances returns user allowance for every gated action. +func (a *ActionGateway) GetUserAllowances(tx Store, userAddress string) ([]core.ActionAllowance, error) { + stakedTokens, err := tx.GetTotalUserStaked(userAddress) + if err != nil { + return nil, fmt.Errorf("failed to get user staked amount: %w", err) + } + + actionCounts, err := tx.GetUserActionCounts(userAddress, defaultTimeWindow) + if err != nil { + return nil, err + } + + remainingStaked := stakedTokens + if stakedTokens.IsPositive() { + appCount, err := tx.GetAppCount(userAddress) + if err != nil { + return nil, fmt.Errorf("failed to get app count: %w", err) + } + + maintenanceCost := a.cfg.AppCost.Mul(decimal.NewFromUint64(appCount)) + remainingStaked = stakedTokens.Sub(maintenanceCost) + } + + timeWindowStr := defaultTimeWindow.String() + result := make([]core.ActionAllowance, 0, len(a.cfg.ActionGates)) + for action := range a.cfg.ActionGates { + result = append(result, core.ActionAllowance{ + GatedAction: action, + TimeWindow: timeWindowStr, + Allowance: a.stakedTo24hActionsAllowance(action, remainingStaked), + Used: actionCounts[action], + }) + } + + slices.SortFunc(result, func(a, b core.ActionAllowance) int { + return int(a.GatedAction.ID()) - int(b.GatedAction.ID()) + }) + + return result, nil +} diff --git a/clearnode/action_gateway/action_gateway_test.go b/clearnode/action_gateway/action_gateway_test.go new file mode 100644 index 000000000..d9486d094 --- /dev/null +++ b/clearnode/action_gateway/action_gateway_test.go @@ -0,0 +1,356 @@ +package action_gateway + +import ( + "errors" + "testing" + "time" + + "github.com/erc7824/nitrolite/pkg/core" + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// mockAVStore implements AVStore for unit tests. +type mockAVStore struct { + totalStaked decimal.Decimal + stakedErr error + appCount uint64 + appCountErr error + actionCount uint64 + actionErr error + actionCounts map[core.GatedAction]uint64 + actionCSErr error + recordedActions []core.GatedAction + recordErr error +} + +func (m *mockAVStore) GetTotalUserStaked(string) (decimal.Decimal, error) { + return m.totalStaked, m.stakedErr +} + +func (m *mockAVStore) GetAppCount(string) (uint64, error) { + return m.appCount, m.appCountErr +} + +func (m *mockAVStore) GetUserActionCount(string, core.GatedAction, time.Duration) (uint64, error) { + return m.actionCount, m.actionErr +} + +func (m *mockAVStore) GetUserActionCounts(string, time.Duration) (map[core.GatedAction]uint64, error) { + return m.actionCounts, m.actionCSErr +} + +func (m *mockAVStore) RecordAction(_ string, action core.GatedAction) error { + m.recordedActions = append(m.recordedActions, action) + return m.recordErr +} + +func defaultConfig() ActionLimitConfig { + return ActionLimitConfig{ + LevelStepTokens: decimal.NewFromInt(100), + AppCost: decimal.NewFromInt(50), + ActionGates: map[core.GatedAction]ActionGateConfig{ + core.GatedActionTransfer: {FreeActionsAllowance: 5, IncreasePerLevel: 10}, + }, + } +} + +func mustNewGateway(t *testing.T, cfg ActionLimitConfig) *ActionGateway { + t.Helper() + gw, err := NewActionGateway(cfg) + require.NoError(t, err) + return gw +} + +// --- NewActionGateway --- + +func TestNewActionGateway(t *testing.T) { + t.Run("valid config", func(t *testing.T) { + gw, err := NewActionGateway(defaultConfig()) + require.NoError(t, err) + assert.NotNil(t, gw) + }) + + t.Run("zero LevelStepTokens", func(t *testing.T) { + cfg := defaultConfig() + cfg.LevelStepTokens = decimal.Zero + _, err := NewActionGateway(cfg) + assert.Error(t, err) + assert.Contains(t, err.Error(), "LevelStepTokens") + }) + + t.Run("zero AppCost", func(t *testing.T) { + cfg := defaultConfig() + cfg.AppCost = decimal.Zero + _, err := NewActionGateway(cfg) + assert.Error(t, err) + assert.Contains(t, err.Error(), "AppCost") + }) +} + +// --- stakedTo24hActionsAllowance --- + +func TestStakedTo24hActionsAllowance(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + + t.Run("unknown action returns 0", func(t *testing.T) { + assert.Equal(t, uint64(0), gw.stakedTo24hActionsAllowance("unknown_action", decimal.NewFromInt(1000))) + }) + + t.Run("zero staked returns free allowance only", func(t *testing.T) { + assert.Equal(t, uint64(5), gw.stakedTo24hActionsAllowance(core.GatedActionTransfer, decimal.Zero)) + }) + + t.Run("negative staked returns free allowance only", func(t *testing.T) { + assert.Equal(t, uint64(5), gw.stakedTo24hActionsAllowance(core.GatedActionTransfer, decimal.NewFromInt(-100))) + }) + + t.Run("positive staked adds levels", func(t *testing.T) { + // 250 tokens / 100 step = 2 levels -> 5 + 2*10 = 25 + assert.Equal(t, uint64(25), gw.stakedTo24hActionsAllowance(core.GatedActionTransfer, decimal.NewFromInt(250))) + }) + + t.Run("partial level truncated", func(t *testing.T) { + // 199 tokens / 100 step = 1 level -> 5 + 1*10 = 15 + assert.Equal(t, uint64(15), gw.stakedTo24hActionsAllowance(core.GatedActionTransfer, decimal.NewFromInt(199))) + }) +} + +// --- AllowAction --- + +func TestAllowAction(t *testing.T) { + t.Run("allowed with free allowance, zero staked", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + store := &mockAVStore{ + totalStaked: decimal.Zero, + actionCount: 0, + } + err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer) + require.NoError(t, err) + assert.Equal(t, []core.GatedAction{core.GatedActionTransfer}, store.recordedActions) + }) + + t.Run("allowed with staked tokens and apps", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + // 300 staked - 2 apps * 50 cost = 200 remaining -> 2 levels -> 5 + 20 = 25 + store := &mockAVStore{ + totalStaked: decimal.NewFromInt(300), + appCount: 2, + actionCount: 24, // under 25 + } + err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer) + require.NoError(t, err) + }) + + t.Run("rejected at limit", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + store := &mockAVStore{ + totalStaked: decimal.Zero, + actionCount: 5, // equals free allowance + } + err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer) + assert.Error(t, err) + assert.Contains(t, err.Error(), "limit reached") + }) + + t.Run("rejected over limit", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + store := &mockAVStore{ + totalStaked: decimal.Zero, + actionCount: 10, + } + err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer) + assert.Error(t, err) + }) + + t.Run("unknown gated action returns nil without store calls", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + store := &mockAVStore{ + stakedErr: errors.New("should not be called"), + } + err := gw.AllowAction(store, "0xuser", "nonexistent") + require.NoError(t, err) + assert.Empty(t, store.recordedActions) + }) + + t.Run("staked error propagated", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + store := &mockAVStore{stakedErr: errors.New("db down")} + err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer) + assert.ErrorContains(t, err, "db down") + }) + + t.Run("app count error propagated", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + store := &mockAVStore{ + totalStaked: decimal.NewFromInt(100), + appCountErr: errors.New("db down"), + } + err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer) + assert.ErrorContains(t, err, "db down") + }) + + t.Run("action count error propagated", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + store := &mockAVStore{ + totalStaked: decimal.Zero, + actionErr: errors.New("db down"), + } + err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer) + assert.ErrorContains(t, err, "db down") + }) + + t.Run("record error propagated", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + store := &mockAVStore{ + totalStaked: decimal.Zero, + actionCount: 0, + recordErr: errors.New("write fail"), + } + err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer) + assert.ErrorContains(t, err, "write fail") + }) + + t.Run("skips GetAppCount when staked is zero", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + // If GetAppCount were called it would return an error + store := &mockAVStore{ + totalStaked: decimal.Zero, + appCountErr: errors.New("should not be called"), + actionCount: 0, + } + err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer) + require.NoError(t, err) + }) +} + +// --- AllowAppRegistration --- + +func TestAllowAppRegistration(t *testing.T) { + t.Run("allowed with enough stake", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + // 0 existing apps, cost for 1 = 50, staked = 100 + store := &mockAVStore{totalStaked: decimal.NewFromInt(100), appCount: 0} + err := gw.AllowAppRegistration(store, "0xuser") + require.NoError(t, err) + }) + + t.Run("allowed exact cost", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + // 1 existing app, cost for 2 = 100, staked = 100 + store := &mockAVStore{totalStaked: decimal.NewFromInt(100), appCount: 1} + err := gw.AllowAppRegistration(store, "0xuser") + require.NoError(t, err) + }) + + t.Run("rejected not enough stake", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + // 1 existing app, cost for 2 = 100, staked = 99 + store := &mockAVStore{totalStaked: decimal.NewFromInt(99), appCount: 1} + err := gw.AllowAppRegistration(store, "0xuser") + assert.Error(t, err) + assert.Contains(t, err.Error(), "not enough staked tokens") + }) + + t.Run("rejected zero staked", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + store := &mockAVStore{totalStaked: decimal.Zero} + err := gw.AllowAppRegistration(store, "0xuser") + assert.Error(t, err) + assert.Contains(t, err.Error(), "zero staked tokens") + }) +} + +// --- GetUserAllowances --- + +func TestGetUserAllowances(t *testing.T) { + multiActionConfig := ActionLimitConfig{ + LevelStepTokens: decimal.NewFromInt(100), + AppCost: decimal.NewFromInt(50), + ActionGates: map[core.GatedAction]ActionGateConfig{ + core.GatedActionTransfer: {FreeActionsAllowance: 5, IncreasePerLevel: 10}, + core.GatedActionAppSessionOperation: {FreeActionsAllowance: 20, IncreasePerLevel: 5}, + }, + } + + t.Run("zero staked returns free allowances", func(t *testing.T) { + gw := mustNewGateway(t, multiActionConfig) + store := &mockAVStore{ + totalStaked: decimal.Zero, + actionCounts: map[core.GatedAction]uint64{core.GatedActionTransfer: 3}, + } + result, err := gw.GetUserAllowances(store, "0xuser") + require.NoError(t, err) + require.Len(t, result, 2) + + // Results should be sorted by GatedAction ID + assert.Equal(t, core.GatedActionTransfer, result[0].GatedAction) + assert.Equal(t, uint64(5), result[0].Allowance) + assert.Equal(t, uint64(3), result[0].Used) + + assert.Equal(t, core.GatedActionAppSessionOperation, result[1].GatedAction) + assert.Equal(t, uint64(20), result[1].Allowance) + assert.Equal(t, uint64(0), result[1].Used) + }) + + t.Run("staked tokens increase allowances", func(t *testing.T) { + gw := mustNewGateway(t, multiActionConfig) + // 500 staked - 2 apps * 50 = 400 remaining -> 4 levels + store := &mockAVStore{ + totalStaked: decimal.NewFromInt(500), + appCount: 2, + actionCounts: map[core.GatedAction]uint64{}, + } + result, err := gw.GetUserAllowances(store, "0xuser") + require.NoError(t, err) + + // Transfer: 5 + 4*10 = 45 + assert.Equal(t, uint64(45), result[0].Allowance) + // AppSessionOperation: 20 + 4*5 = 40 + assert.Equal(t, uint64(40), result[1].Allowance) + }) + + t.Run("maintenance exceeds staked gives free allowance", func(t *testing.T) { + gw := mustNewGateway(t, multiActionConfig) + // 50 staked - 2 apps * 50 = -50 remaining + store := &mockAVStore{ + totalStaked: decimal.NewFromInt(50), + appCount: 2, + actionCounts: map[core.GatedAction]uint64{}, + } + result, err := gw.GetUserAllowances(store, "0xuser") + require.NoError(t, err) + + assert.Equal(t, uint64(5), result[0].Allowance) + assert.Equal(t, uint64(20), result[1].Allowance) + }) + + t.Run("time window is set", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + store := &mockAVStore{ + totalStaked: decimal.Zero, + actionCounts: map[core.GatedAction]uint64{}, + } + result, err := gw.GetUserAllowances(store, "0xuser") + require.NoError(t, err) + assert.Equal(t, "24h0m0s", result[0].TimeWindow) + }) + + t.Run("staked error propagated", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + store := &mockAVStore{stakedErr: errors.New("db down")} + _, err := gw.GetUserAllowances(store, "0xuser") + assert.ErrorContains(t, err, "db down") + }) + + t.Run("action counts error propagated", func(t *testing.T) { + gw := mustNewGateway(t, defaultConfig()) + store := &mockAVStore{ + totalStaked: decimal.Zero, + actionCSErr: errors.New("db down"), + } + _, err := gw.GetUserAllowances(store, "0xuser") + assert.ErrorContains(t, err, "db down") + }) +} diff --git a/clearnode/api/app_session_v1/create_app_session.go b/clearnode/api/app_session_v1/create_app_session.go index ad3db3cfc..3fbe49e77 100644 --- a/clearnode/api/app_session_v1/create_app_session.go +++ b/clearnode/api/app_session_v1/create_app_session.go @@ -5,6 +5,7 @@ import ( "time" "github.com/erc7824/nitrolite/pkg/app" + "github.com/erc7824/nitrolite/pkg/core" "github.com/erc7824/nitrolite/pkg/log" "github.com/erc7824/nitrolite/pkg/rpc" "github.com/ethereum/go-ethereum/common/hexutil" @@ -107,14 +108,14 @@ func (h *Handler) CreateAppSession(c *rpc.Context) { } err = h.useStoreInTx(func(tx Store) error { - registeredApp, err := tx.GetApp(appDef.Application) + registeredApp, err := tx.GetApp(appDef.ApplicationID) if err != nil { return rpc.Errorf("failed to look up application: %v", err) } // App must be registered regardless of CreationApprovalNotRequired flag. if registeredApp == nil { - return rpc.Errorf("application %s is not registered", appDef.Application) + return rpc.Errorf("application %s is not registered", appDef.ApplicationID) } if !registeredApp.App.CreationApprovalNotRequired { @@ -148,25 +149,30 @@ func (h *Handler) CreateAppSession(c *rpc.Context) { } } + err = h.actionGateway.AllowAction(tx, registeredApp.App.OwnerWallet, core.GatedActionAppSessionCreation) + if err != nil { + return rpc.NewError(err) + } + // Create app session with 0 allocations appSession := app.AppSessionV1{ - SessionID: appSessionID, - Application: appDef.Application, - Participants: appDef.Participants, - Quorum: appDef.Quorum, - Nonce: appDef.Nonce, - Status: app.AppSessionStatusOpen, - Version: 1, - SessionData: reqPayload.SessionData, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + SessionID: appSessionID, + ApplicationID: appDef.ApplicationID, + Participants: appDef.Participants, + Quorum: appDef.Quorum, + Nonce: appDef.Nonce, + Status: app.AppSessionStatusOpen, + Version: 1, + SessionData: reqPayload.SessionData, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } if err := tx.CreateAppSession(appSession); err != nil { return rpc.Errorf("failed to create app session: %v", err) } - if err := h.verifyQuorum(tx, appSessionID, appDef.Application, participantWeights, appDef.Quorum, packedRequest, reqPayload.QuorumSigs); err != nil { + if err := h.verifyQuorum(tx, appSessionID, appDef.ApplicationID, participantWeights, appDef.Quorum, packedRequest, reqPayload.QuorumSigs); err != nil { return err } diff --git a/clearnode/api/app_session_v1/create_app_session_test.go b/clearnode/api/app_session_v1/create_app_session_test.go index 40ede0537..4f9cc436a 100644 --- a/clearnode/api/app_session_v1/create_app_session_test.go +++ b/clearnode/api/app_session_v1/create_app_session_test.go @@ -29,6 +29,7 @@ func TestCreateAppSession_Success(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -44,7 +45,7 @@ func TestCreateAppSession_Success(t *testing.T) { // Build the app.AppDefinitionV1 for signing appDef := app.AppDefinitionV1{ - Application: "test-app", + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 1}, {WalletAddress: participant2, SignatureWeight: 1}, @@ -132,6 +133,7 @@ func TestCreateAppSession_QuorumWithMultipleSignatures(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -149,7 +151,7 @@ func TestCreateAppSession_QuorumWithMultipleSignatures(t *testing.T) { // Build the app.AppDefinitionV1 for signing appDef := app.AppDefinitionV1{ - Application: "test-app", + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 2}, {WalletAddress: participant2, SignatureWeight: 1}, @@ -233,6 +235,7 @@ func TestCreateAppSession_ZeroNonce(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -296,6 +299,7 @@ func TestCreateAppSession_QuorumExceedsTotalWeights(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -365,6 +369,7 @@ func TestCreateAppSession_NoSignatures(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -428,6 +433,7 @@ func TestCreateAppSession_SignatureFromNonParticipant(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -442,7 +448,7 @@ func TestCreateAppSession_SignatureFromNonParticipant(t *testing.T) { // Build the app.AppDefinitionV1 for signing (with participant1 only) appDef := app.AppDefinitionV1{ - Application: "test-app", + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 1}, }, @@ -509,6 +515,7 @@ func TestCreateAppSession_QuorumNotMet(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -525,7 +532,7 @@ func TestCreateAppSession_QuorumNotMet(t *testing.T) { // Build the app.AppDefinitionV1 for signing appDef := app.AppDefinitionV1{ - Application: "test-app", + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 1}, {WalletAddress: participant2, SignatureWeight: 1}, @@ -604,6 +611,7 @@ func TestCreateAppSession_DuplicateSignatures(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -619,7 +627,7 @@ func TestCreateAppSession_DuplicateSignatures(t *testing.T) { // Build the app.AppDefinitionV1 for signing appDef := app.AppDefinitionV1{ - Application: "test-app", + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 1}, {WalletAddress: participant2, SignatureWeight: 1}, @@ -696,6 +704,7 @@ func TestCreateAppSession_InvalidSignatureHex(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -764,6 +773,7 @@ func TestCreateAppSession_SignatureRecoveryFailure(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -832,6 +842,7 @@ func TestCreateAppSession_AppNotRegistered(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -844,7 +855,7 @@ func TestCreateAppSession_AppNotRegistered(t *testing.T) { participant1 := wallet1.Address appDef := app.AppDefinitionV1{ - Application: "unregistered-app", + ApplicationID: "unregistered-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 1}, }, @@ -900,6 +911,7 @@ func TestCreateAppSession_OwnerSigRequired(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -912,7 +924,7 @@ func TestCreateAppSession_OwnerSigRequired(t *testing.T) { participant1 := wallet1.Address appDef := app.AppDefinitionV1{ - Application: "restricted-app", + ApplicationID: "restricted-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 1}, }, @@ -975,6 +987,7 @@ func TestCreateAppSession_OwnerSigSuccess(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -989,7 +1002,7 @@ func TestCreateAppSession_OwnerSigSuccess(t *testing.T) { participant1 := wallet1.Address appDef := app.AppDefinitionV1{ - Application: "restricted-app", + ApplicationID: "restricted-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 1}, }, diff --git a/clearnode/api/app_session_v1/get_app_definition.go b/clearnode/api/app_session_v1/get_app_definition.go index a9706186f..32c4f0f10 100644 --- a/clearnode/api/app_session_v1/get_app_definition.go +++ b/clearnode/api/app_session_v1/get_app_definition.go @@ -36,7 +36,7 @@ func (h *Handler) GetAppDefinition(c *rpc.Context) { } definition = rpc.AppDefinitionV1{ - Application: session.Application, + Application: session.ApplicationID, Participants: participants, Quorum: session.Quorum, Nonce: strconv.FormatUint(session.Nonce, 10), diff --git a/clearnode/api/app_session_v1/get_app_definition_test.go b/clearnode/api/app_session_v1/get_app_definition_test.go index a07105b14..6ca17b7d7 100644 --- a/clearnode/api/app_session_v1/get_app_definition_test.go +++ b/clearnode/api/app_session_v1/get_app_definition_test.go @@ -39,8 +39,8 @@ func TestGetAppDefinition_Success(t *testing.T) { participant2 := "0x9876543210987654321098765432109876543210" session := &app.AppSessionV1{ - SessionID: sessionID, - Application: "game", + SessionID: sessionID, + ApplicationID: "game", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 1}, {WalletAddress: participant2, SignatureWeight: 1}, diff --git a/clearnode/api/app_session_v1/get_app_sessions_test.go b/clearnode/api/app_session_v1/get_app_sessions_test.go index b953d415b..ddf1428dd 100644 --- a/clearnode/api/app_session_v1/get_app_sessions_test.go +++ b/clearnode/api/app_session_v1/get_app_sessions_test.go @@ -41,8 +41,8 @@ func TestGetAppSessions_SuccessWithParticipant(t *testing.T) { sessions := []app.AppSessionV1{ { - SessionID: "session1", - Application: "game", + SessionID: "session1", + ApplicationID: "game", Participants: []app.AppParticipantV1{ {WalletAddress: participant, SignatureWeight: 1}, {WalletAddress: participant2, SignatureWeight: 1}, @@ -56,8 +56,8 @@ func TestGetAppSessions_SuccessWithParticipant(t *testing.T) { UpdatedAt: time.Now(), }, { - SessionID: "session2", - Application: "betting", + SessionID: "session2", + ApplicationID: "betting", Participants: []app.AppParticipantV1{ {WalletAddress: participant, SignatureWeight: 1}, }, @@ -116,17 +116,17 @@ func TestGetAppSessions_SuccessWithParticipant(t *testing.T) { // Verify first session assert.Equal(t, "session1", response.AppSessions[0].AppSessionID) assert.Equal(t, "closed", response.AppSessions[0].Status) - assert.Len(t, response.AppSessions[0].Participants, 2) - assert.Equal(t, participant, response.AppSessions[0].Participants[0].WalletAddress) - assert.Equal(t, uint8(2), response.AppSessions[0].Quorum) + assert.Len(t, response.AppSessions[0].AppDefinitionV1.Participants, 2) + assert.Equal(t, participant, response.AppSessions[0].AppDefinitionV1.Participants[0].WalletAddress) + assert.Equal(t, uint8(2), response.AppSessions[0].AppDefinitionV1.Quorum) assert.Equal(t, "1", response.AppSessions[0].Version) assert.NotNil(t, response.AppSessions[0].SessionData) // Verify second session assert.Equal(t, "session2", response.AppSessions[1].AppSessionID) assert.Equal(t, "open", response.AppSessions[1].Status) - assert.Len(t, response.AppSessions[1].Participants, 1) - assert.Equal(t, uint8(1), response.AppSessions[1].Quorum) + assert.Len(t, response.AppSessions[1].AppDefinitionV1.Participants, 1) + assert.Equal(t, uint8(1), response.AppSessions[1].AppDefinitionV1.Quorum) assert.Equal(t, "5", response.AppSessions[1].Version) assert.Nil(t, response.AppSessions[1].SessionData) @@ -165,8 +165,8 @@ func TestGetAppSessions_SuccessWithAppSessionID(t *testing.T) { sessions := []app.AppSessionV1{ { - SessionID: sessionID, - Application: "game", + SessionID: sessionID, + ApplicationID: "game", Participants: []app.AppParticipantV1{ {WalletAddress: participant, SignatureWeight: 1}, }, @@ -296,8 +296,8 @@ func TestGetAppSessions_WithStatusFilter(t *testing.T) { sessions := []app.AppSessionV1{ { - SessionID: "session1", - Application: "game", + SessionID: "session1", + ApplicationID: "game", Participants: []app.AppParticipantV1{ {WalletAddress: participant, SignatureWeight: 1}, }, diff --git a/clearnode/api/app_session_v1/handler.go b/clearnode/api/app_session_v1/handler.go index 1f3c59c78..672273367 100644 --- a/clearnode/api/app_session_v1/handler.go +++ b/clearnode/api/app_session_v1/handler.go @@ -19,6 +19,7 @@ import ( type Handler struct { useStoreInTx StoreTxProvider assetStore AssetStore + actionGateway ActionGateway signer sign.Signer stateAdvancer core.StateAdvancer statePacker core.StatePacker @@ -34,6 +35,7 @@ type Handler struct { func NewHandler( useStoreInTx StoreTxProvider, assetStore AssetStore, + actionGateway ActionGateway, signer sign.Signer, stateAdvancer core.StateAdvancer, statePacker core.StatePacker, @@ -44,6 +46,7 @@ func NewHandler( return &Handler{ useStoreInTx: useStoreInTx, assetStore: assetStore, + actionGateway: actionGateway, signer: signer, stateAdvancer: stateAdvancer, statePacker: statePacker, diff --git a/clearnode/api/app_session_v1/interface.go b/clearnode/api/app_session_v1/interface.go index f83d988d7..611695cf3 100644 --- a/clearnode/api/app_session_v1/interface.go +++ b/clearnode/api/app_session_v1/interface.go @@ -1,6 +1,7 @@ package app_session_v1 import ( + "github.com/erc7824/nitrolite/clearnode/action_gateway" "github.com/erc7824/nitrolite/pkg/app" "github.com/erc7824/nitrolite/pkg/core" "github.com/shopspring/decimal" @@ -45,6 +46,13 @@ type Store interface { // Channel Session key state operations ValidateChannelSessionKeyForAsset(wallet, sessionKey, asset, metadataHash string) (bool, error) + + action_gateway.Store +} + +type ActionGateway interface { + // AllowAction checks if a user is allowed to perform a specific gated action based on their past activity and allowances. + AllowAction(tx action_gateway.Store, userAddress string, gatedAction core.GatedAction) error } // StoreTxHandler is a function that executes Store operations within a transaction. diff --git a/clearnode/api/app_session_v1/rebalance_app_sessions.go b/clearnode/api/app_session_v1/rebalance_app_sessions.go index 4cebc5b01..e151a0dcb 100644 --- a/clearnode/api/app_session_v1/rebalance_app_sessions.go +++ b/clearnode/api/app_session_v1/rebalance_app_sessions.go @@ -103,6 +103,18 @@ func (h *Handler) RebalanceAppSessions(c *rpc.Context) { if appSession == nil { return rpc.Errorf("app session not found: %s", update.AppStateUpdate.AppSessionID) } + registeredApp, err := tx.GetApp(appSession.ApplicationID) + if err != nil { + return rpc.Errorf("failed to look up application: %v", err) + } + if registeredApp == nil { + return rpc.Errorf("application %s is not registered", appSession.ApplicationID) + } + + err = h.actionGateway.AllowAction(tx, registeredApp.App.OwnerWallet, update.AppStateUpdate.Intent.GatedAction()) + if err != nil { + return rpc.NewError(err) + } if len(update.QuorumSigs) > len(appSession.Participants) { return rpc.Errorf("quorum_sigs count (%d) exceeds participants count (%d)", len(update.QuorumSigs), len(appSession.Participants)) } @@ -125,7 +137,7 @@ func (h *Handler) RebalanceAppSessions(c *rpc.Context) { return rpc.Errorf("failed to pack app state update for session %s: %v", update.AppStateUpdate.AppSessionID, err) } - if err := h.verifyQuorum(tx, update.AppStateUpdate.AppSessionID, appSession.Application, participantWeights, appSession.Quorum, packedStateUpdate, update.QuorumSigs); err != nil { + if err := h.verifyQuorum(tx, update.AppStateUpdate.AppSessionID, appSession.ApplicationID, participantWeights, appSession.Quorum, packedStateUpdate, update.QuorumSigs); err != nil { return rpc.Errorf("quorum verification failed for session %s: %v", update.AppStateUpdate.AppSessionID, err) } diff --git a/clearnode/api/app_session_v1/rebalance_app_sessions_test.go b/clearnode/api/app_session_v1/rebalance_app_sessions_test.go index 5305b521e..073792a7e 100644 --- a/clearnode/api/app_session_v1/rebalance_app_sessions_test.go +++ b/clearnode/api/app_session_v1/rebalance_app_sessions_test.go @@ -43,6 +43,7 @@ func TestRebalanceAppSessions_Success_TwoSessions(t *testing.T) { handler := NewHandler( storeTxProvider, nil, + &MockActionGateway{}, nil, nil, nil, @@ -59,8 +60,8 @@ func TestRebalanceAppSessions_Success_TwoSessions(t *testing.T) { sessionID2 := "0x2222222222222222222222222222222222222222222222222222222222222222" session1 := &app.AppSessionV1{ - SessionID: sessionID1, - Application: "test-app", + SessionID: sessionID1, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: wallet1.Address, SignatureWeight: 10}, }, @@ -71,8 +72,8 @@ func TestRebalanceAppSessions_Success_TwoSessions(t *testing.T) { } session2 := &app.AppSessionV1{ - SessionID: sessionID2, - Application: "test-app", + SessionID: sessionID2, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: wallet2.Address, SignatureWeight: 10}, }, @@ -149,6 +150,9 @@ func TestRebalanceAppSessions_Success_TwoSessions(t *testing.T) { } // Mock expectations for session 1 + mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil) mockStore.On("GetAppSession", sessionID1).Return(session1, nil) mockStore.On("GetParticipantAllocations", sessionID1).Return(currentAllocations1, nil) mockStore.On("UpdateAppSession", mock.MatchedBy(func(session app.AppSessionV1) bool { @@ -158,6 +162,9 @@ func TestRebalanceAppSessions_Success_TwoSessions(t *testing.T) { })).Return(nil).Once() // Mock expectations for session 2 + mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil) mockStore.On("GetAppSession", sessionID2).Return(session2, nil) mockStore.On("GetParticipantAllocations", sessionID2).Return(currentAllocations2, nil) mockStore.On("UpdateAppSession", mock.MatchedBy(func(session app.AppSessionV1) bool { @@ -207,6 +214,7 @@ func TestRebalanceAppSessions_Success_MultiAsset(t *testing.T) { handler := NewHandler( storeTxProvider, nil, + &MockActionGateway{}, nil, nil, nil, @@ -223,8 +231,8 @@ func TestRebalanceAppSessions_Success_MultiAsset(t *testing.T) { sessionID2 := "0x2222222222222222222222222222222222222222222222222222222222222222" session1 := &app.AppSessionV1{ - SessionID: sessionID1, - Application: "test-app", + SessionID: sessionID1, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: wallet1.Address, SignatureWeight: 10}, }, @@ -234,8 +242,8 @@ func TestRebalanceAppSessions_Success_MultiAsset(t *testing.T) { } session2 := &app.AppSessionV1{ - SessionID: sessionID2, - Application: "test-app", + SessionID: sessionID2, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: wallet2.Address, SignatureWeight: 10}, }, @@ -313,12 +321,18 @@ func TestRebalanceAppSessions_Success_MultiAsset(t *testing.T) { } // Mock expectations + mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil) mockStore.On("GetAppSession", sessionID1).Return(session1, nil) mockStore.On("GetParticipantAllocations", sessionID1).Return(currentAllocations1, nil) mockStore.On("UpdateAppSession", mock.MatchedBy(func(s app.AppSessionV1) bool { return s.SessionID == sessionID1 && s.Version == 2 })).Return(nil).Once() + mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil) mockStore.On("GetAppSession", sessionID2).Return(session2, nil) mockStore.On("GetParticipantAllocations", sessionID2).Return(currentAllocations2, nil) mockStore.On("UpdateAppSession", mock.MatchedBy(func(s app.AppSessionV1) bool { @@ -360,6 +374,7 @@ func TestRebalanceAppSessions_Error_InsufficientSessions(t *testing.T) { handler := NewHandler( storeTxProvider, nil, + &MockActionGateway{}, nil, nil, nil, @@ -417,6 +432,7 @@ func TestRebalanceAppSessions_Error_InvalidIntent(t *testing.T) { handler := NewHandler( storeTxProvider, nil, + &MockActionGateway{}, nil, nil, nil, @@ -490,6 +506,7 @@ func TestRebalanceAppSessions_Error_DuplicateSession(t *testing.T) { handler := NewHandler( storeTxProvider, nil, + &MockActionGateway{}, nil, nil, nil, @@ -565,6 +582,7 @@ func TestRebalanceAppSessions_Error_ConservationViolation(t *testing.T) { handler := NewHandler( storeTxProvider, nil, + &MockActionGateway{}, nil, nil, nil, @@ -581,8 +599,8 @@ func TestRebalanceAppSessions_Error_ConservationViolation(t *testing.T) { sessionID2 := "0x2222222222222222222222222222222222222222222222222222222222222222" session1 := &app.AppSessionV1{ - SessionID: sessionID1, - Application: "test-app", + SessionID: sessionID1, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: wallet1.Address, SignatureWeight: 10}, }, @@ -592,8 +610,8 @@ func TestRebalanceAppSessions_Error_ConservationViolation(t *testing.T) { } session2 := &app.AppSessionV1{ - SessionID: sessionID2, - Application: "test-app", + SessionID: sessionID2, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: wallet2.Address, SignatureWeight: 10}, }, @@ -664,12 +682,18 @@ func TestRebalanceAppSessions_Error_ConservationViolation(t *testing.T) { } // Mock expectations + mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil) mockStore.On("GetAppSession", sessionID1).Return(session1, nil) mockStore.On("GetParticipantAllocations", sessionID1).Return(currentAllocations1, nil) mockStore.On("UpdateAppSession", mock.MatchedBy(func(s app.AppSessionV1) bool { return s.SessionID == sessionID1 && s.Version == 2 })).Return(nil).Once() + mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil) mockStore.On("GetAppSession", sessionID2).Return(session2, nil) mockStore.On("GetParticipantAllocations", sessionID2).Return(currentAllocations2, nil) mockStore.On("UpdateAppSession", mock.MatchedBy(func(s app.AppSessionV1) bool { @@ -704,6 +728,7 @@ func TestRebalanceAppSessions_Error_SessionNotFound(t *testing.T) { handler := NewHandler( storeTxProvider, nil, + &MockActionGateway{}, nil, nil, nil, @@ -784,6 +809,7 @@ func TestRebalanceAppSessions_Error_ClosedSession(t *testing.T) { handler := NewHandler( storeTxProvider, nil, + &MockActionGateway{}, nil, nil, nil, @@ -799,9 +825,10 @@ func TestRebalanceAppSessions_Error_ClosedSession(t *testing.T) { sessionID2 := "0x2222222222222222222222222222222222222222222222222222222222222222" session1 := &app.AppSessionV1{ - SessionID: sessionID1, - Status: app.AppSessionStatusClosed, // Closed - Version: 1, + SessionID: sessionID1, + ApplicationID: "test-app", + Status: app.AppSessionStatusClosed, // Closed + Version: 1, Participants: []app.AppParticipantV1{ {WalletAddress: wallet1.Address, SignatureWeight: 1}, {WalletAddress: wallet2.Address, SignatureWeight: 1}, @@ -844,6 +871,9 @@ func TestRebalanceAppSessions_Error_ClosedSession(t *testing.T) { }, } + mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil) mockStore.On("GetAppSession", sessionID1).Return(session1, nil) payload, err := rpc.NewPayload(reqPayload) @@ -874,6 +904,7 @@ func TestRebalanceAppSessions_Error_InvalidVersion(t *testing.T) { handler := NewHandler( storeTxProvider, nil, + &MockActionGateway{}, nil, nil, nil, @@ -889,9 +920,10 @@ func TestRebalanceAppSessions_Error_InvalidVersion(t *testing.T) { sessionID2 := "0x2222222222222222222222222222222222222222222222222222222222222222" session1 := &app.AppSessionV1{ - SessionID: sessionID1, - Status: app.AppSessionStatusOpen, - Version: 5, // Current version is 5 + SessionID: sessionID1, + ApplicationID: "test-app", + Status: app.AppSessionStatusOpen, + Version: 5, // Current version is 5 Participants: []app.AppParticipantV1{ {WalletAddress: wallet1.Address, SignatureWeight: 1}, {WalletAddress: wallet2.Address, SignatureWeight: 1}, @@ -934,6 +966,9 @@ func TestRebalanceAppSessions_Error_InvalidVersion(t *testing.T) { }, } + mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil) mockStore.On("GetAppSession", sessionID1).Return(session1, nil) payload, err := rpc.NewPayload(reqPayload) diff --git a/clearnode/api/app_session_v1/submit_app_state.go b/clearnode/api/app_session_v1/submit_app_state.go index d03707d01..d62bfbc00 100644 --- a/clearnode/api/app_session_v1/submit_app_state.go +++ b/clearnode/api/app_session_v1/submit_app_state.go @@ -65,6 +65,18 @@ func (h *Handler) SubmitAppState(c *rpc.Context) { return rpc.Errorf("app session is already closed") } + registeredApp, err := tx.GetApp(appSession.ApplicationID) + if err != nil { + return rpc.Errorf("failed to look up application: %v", err) + } + if registeredApp == nil { + return rpc.Errorf("application %s is not registered", appSession.ApplicationID) + } + err = h.actionGateway.AllowAction(tx, registeredApp.App.OwnerWallet, appStateUpd.Intent.GatedAction()) + if err != nil { + return rpc.NewError(err) + } + if len(reqPayload.QuorumSigs) > len(appSession.Participants) { return rpc.Errorf("quorum_sigs count (%d) exceeds participants count (%d)", len(reqPayload.QuorumSigs), len(appSession.Participants)) } @@ -84,7 +96,7 @@ func (h *Handler) SubmitAppState(c *rpc.Context) { return rpc.Errorf("failed to pack app state update: %v", err) } - if err := h.verifyQuorum(tx, appStateUpd.AppSessionID, appSession.Application, participantWeights, appSession.Quorum, packedStateUpdate, reqPayload.QuorumSigs); err != nil { + if err := h.verifyQuorum(tx, appStateUpd.AppSessionID, appSession.ApplicationID, participantWeights, appSession.Quorum, packedStateUpdate, reqPayload.QuorumSigs); err != nil { return err } diff --git a/clearnode/api/app_session_v1/submit_app_state_test.go b/clearnode/api/app_session_v1/submit_app_state_test.go index 0300f49d9..a0bb83866 100644 --- a/clearnode/api/app_session_v1/submit_app_state_test.go +++ b/clearnode/api/app_session_v1/submit_app_state_test.go @@ -29,6 +29,7 @@ func TestSubmitAppState_OperateIntent_NoRedistribution_Success(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -43,8 +44,8 @@ func TestSubmitAppState_OperateIntent_NoRedistribution_Success(t *testing.T) { participant2 := "0x2222222222222222222222222222222222222222" existingSession := &app.AppSessionV1{ - SessionID: appSessionID, - Application: "test-app", + SessionID: appSessionID, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 5}, {WalletAddress: participant2, SignatureWeight: 5}, @@ -96,6 +97,9 @@ func TestSubmitAppState_OperateIntent_NoRedistribution_Success(t *testing.T) { } // Mock expectations + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil).Maybe() mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil) mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil) mockStore.On("GetAppSessionBalances", appSessionID).Return(sessionBalances, nil) @@ -140,6 +144,7 @@ func TestSubmitAppState_OperateIntent_WithRedistribution_Success(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -154,8 +159,8 @@ func TestSubmitAppState_OperateIntent_WithRedistribution_Success(t *testing.T) { participant2 := "0x2222222222222222222222222222222222222222" existingSession := &app.AppSessionV1{ - SessionID: appSessionID, - Application: "test-app", + SessionID: appSessionID, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 5}, {WalletAddress: participant2, SignatureWeight: 5}, @@ -209,6 +214,9 @@ func TestSubmitAppState_OperateIntent_WithRedistribution_Success(t *testing.T) { } // Mock expectations + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil).Maybe() mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil) mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil) mockStore.On("GetAppSessionBalances", appSessionID).Return(sessionBalances, nil) @@ -257,6 +265,7 @@ func TestSubmitAppState_WithdrawIntent_Success(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -270,8 +279,8 @@ func TestSubmitAppState_WithdrawIntent_Success(t *testing.T) { participant1 := wallet1.Address existingSession := &app.AppSessionV1{ - SessionID: appSessionID, - Application: "test-app", + SessionID: appSessionID, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 10}, }, @@ -313,6 +322,9 @@ func TestSubmitAppState_WithdrawIntent_Success(t *testing.T) { } // Mock expectations + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil).Maybe() mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil) mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil) mockAssetStore.On("GetAssetDecimals", "USDC").Return(uint8(6), nil) @@ -367,6 +379,7 @@ func TestSubmitAppState_CloseIntent_Success(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -381,8 +394,8 @@ func TestSubmitAppState_CloseIntent_Success(t *testing.T) { participant2 := "0x2222222222222222222222222222222222222222" existingSession := &app.AppSessionV1{ - SessionID: appSessionID, - Application: "test-app", + SessionID: appSessionID, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 5}, {WalletAddress: participant2, SignatureWeight: 5}, @@ -430,6 +443,9 @@ func TestSubmitAppState_CloseIntent_Success(t *testing.T) { } // Mock expectations + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil).Maybe() mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil) mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil) mockAssetStore.On("GetAssetDecimals", "USDC").Return(uint8(6), nil) @@ -491,6 +507,7 @@ func TestSubmitAppState_CloseIntent_AllocationMismatch_Rejected(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -504,8 +521,8 @@ func TestSubmitAppState_CloseIntent_AllocationMismatch_Rejected(t *testing.T) { participant1 := wallet1.Address existingSession := &app.AppSessionV1{ - SessionID: appSessionID, - Application: "test-app", + SessionID: appSessionID, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 10}, }, @@ -547,6 +564,9 @@ func TestSubmitAppState_CloseIntent_AllocationMismatch_Rejected(t *testing.T) { } // Mock expectations + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil).Maybe() mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil) mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil) mockAssetStore.On("GetAssetDecimals", "USDC").Return(uint8(6), nil).Maybe() @@ -586,6 +606,7 @@ func TestSubmitAppState_OperateIntent_MissingAllocation_Rejected(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -600,8 +621,8 @@ func TestSubmitAppState_OperateIntent_MissingAllocation_Rejected(t *testing.T) { participant2 := "0x2222222222222222222222222222222222222222" existingSession := &app.AppSessionV1{ - SessionID: appSessionID, - Application: "test-app", + SessionID: appSessionID, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 5}, {WalletAddress: participant2, SignatureWeight: 5}, @@ -651,6 +672,9 @@ func TestSubmitAppState_OperateIntent_MissingAllocation_Rejected(t *testing.T) { } // Mock expectations + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil).Maybe() mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil) mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil) mockStore.On("GetAppSessionBalances", appSessionID).Return(sessionBalances, nil) @@ -695,6 +719,7 @@ func TestSubmitAppState_WithdrawIntent_MissingAllocation_Rejected(t *testing.T) handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -708,8 +733,8 @@ func TestSubmitAppState_WithdrawIntent_MissingAllocation_Rejected(t *testing.T) participant1 := wallet1.Address existingSession := &app.AppSessionV1{ - SessionID: appSessionID, - Application: "test-app", + SessionID: appSessionID, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 10}, }, @@ -752,6 +777,9 @@ func TestSubmitAppState_WithdrawIntent_MissingAllocation_Rejected(t *testing.T) } // Mock expectations + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil).Maybe() mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil) mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil) mockAssetStore.On("GetAssetDecimals", "USDC").Return(uint8(6), nil).Maybe() @@ -802,6 +830,7 @@ func TestSubmitAppState_DepositIntent_Rejected(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -856,6 +885,7 @@ func TestSubmitAppState_ClosedSession_Rejected(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -884,6 +914,9 @@ func TestSubmitAppState_ClosedSession_Rejected(t *testing.T) { } // Mock expectations + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil).Maybe() mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil) // Create RPC context @@ -922,6 +955,7 @@ func TestSubmitAppState_InvalidVersion_Rejected(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -933,9 +967,10 @@ func TestSubmitAppState_InvalidVersion_Rejected(t *testing.T) { appSessionID := "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" existingSession := &app.AppSessionV1{ - SessionID: appSessionID, - Status: app.AppSessionStatusOpen, - Version: 5, // Current version is 5 + SessionID: appSessionID, + ApplicationID: "test-app", + Status: app.AppSessionStatusOpen, + Version: 5, // Current version is 5 Participants: []app.AppParticipantV1{ {WalletAddress: "0x1111111111111111111111111111111111111111", SignatureWeight: 1}, }, @@ -954,6 +989,9 @@ func TestSubmitAppState_InvalidVersion_Rejected(t *testing.T) { } // Mock expectations + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil).Maybe() mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil) // Create RPC context @@ -992,6 +1030,7 @@ func TestSubmitAppState_SessionNotFound_Rejected(t *testing.T) { handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -1051,6 +1090,7 @@ func TestSubmitAppState_OperateIntent_InvalidDecimalPrecision_Rejected(t *testin handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -1064,8 +1104,8 @@ func TestSubmitAppState_OperateIntent_InvalidDecimalPrecision_Rejected(t *testin participant1 := wallet1.Address existingSession := &app.AppSessionV1{ - SessionID: appSessionID, - Application: "test-app", + SessionID: appSessionID, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 10}, }, @@ -1113,6 +1153,9 @@ func TestSubmitAppState_OperateIntent_InvalidDecimalPrecision_Rejected(t *testin } // Mock expectations + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil).Maybe() mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil) mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil) mockStore.On("GetAppSessionBalances", appSessionID).Return(sessionBalances, nil) @@ -1155,6 +1198,7 @@ func TestSubmitAppState_WithdrawIntent_InvalidDecimalPrecision_Rejected(t *testi handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -1168,8 +1212,8 @@ func TestSubmitAppState_WithdrawIntent_InvalidDecimalPrecision_Rejected(t *testi participant1 := wallet1.Address existingSession := &app.AppSessionV1{ - SessionID: appSessionID, - Application: "test-app", + SessionID: appSessionID, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 10}, }, @@ -1213,6 +1257,9 @@ func TestSubmitAppState_WithdrawIntent_InvalidDecimalPrecision_Rejected(t *testi } // Mock expectations + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil).Maybe() mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil) mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil) mockAssetStore.On("GetAssetDecimals", "USDC").Return(uint8(6), nil) @@ -1255,6 +1302,7 @@ func TestSubmitAppState_OperateIntent_RedistributeToNewParticipant_Success(t *te handler := NewHandler( storeTxProvider, mockAssetStore, + &MockActionGateway{}, mockSigner, core.NewStateAdvancerV1(mockAssetStore), mockStatePacker, @@ -1270,8 +1318,8 @@ func TestSubmitAppState_OperateIntent_RedistributeToNewParticipant_Success(t *te participant2 := wallet2.Address existingSession := &app.AppSessionV1{ - SessionID: appSessionID, - Application: "test-app", + SessionID: appSessionID, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: participant1, SignatureWeight: 50}, {WalletAddress: participant2, SignatureWeight: 50}, @@ -1322,6 +1370,9 @@ func TestSubmitAppState_OperateIntent_RedistributeToNewParticipant_Success(t *te } // Mock expectations + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil).Maybe() mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil) mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil) mockStore.On("GetAppSessionBalances", appSessionID).Return(sessionBalances, nil) diff --git a/clearnode/api/app_session_v1/submit_deposit_state.go b/clearnode/api/app_session_v1/submit_deposit_state.go index 0097b5ff8..0fdc96a02 100644 --- a/clearnode/api/app_session_v1/submit_deposit_state.go +++ b/clearnode/api/app_session_v1/submit_deposit_state.go @@ -44,8 +44,48 @@ func (h *Handler) SubmitDepositState(c *rpc.Context) { var nodeSig string err = h.useStoreInTx(func(tx Store) error { + appSession, err := tx.GetAppSession(appStateUpd.AppSessionID) + if err != nil { + return rpc.Errorf("app session not found: %v", err) + } + if appSession == nil { + return rpc.Errorf("app session not found") + } + if len(reqPayload.QuorumSigs) > len(appSession.Participants) { + return rpc.Errorf("quorum_sigs count (%d) exceeds participants count (%d)", len(reqPayload.QuorumSigs), len(appSession.Participants)) + } + if appSession.Status == app.AppSessionStatusClosed { + return rpc.Errorf("app session is already closed") + } + if appStateUpd.Version != appSession.Version+1 { + return rpc.Errorf("invalid app session version: expected %d, got %d", appSession.Version+1, appStateUpd.Version) + } + + if appStateUpd.Intent != app.AppStateUpdateIntentDeposit { + return rpc.Errorf("invalid intent: expected 'deposit', got '%s'", appStateUpd.Intent) + } + + participantWeights := getParticipantWeights(appSession.Participants) + + if len(reqPayload.QuorumSigs) == 0 { + return rpc.Errorf("no signatures provided") + } + + registeredApp, err := tx.GetApp(appSession.ApplicationID) + if err != nil { + return rpc.Errorf("failed to look up application: %v", err) + } + if registeredApp == nil { + return rpc.Errorf("application %s is not registered", appSession.ApplicationID) + } + + err = h.actionGateway.AllowAction(tx, registeredApp.App.OwnerWallet, appStateUpd.Intent.GatedAction()) + if err != nil { + return rpc.NewError(err) + } + // Lock the user's state to prevent concurrent modifications - _, err := tx.LockUserState(userState.UserWallet, userState.Asset) + _, err = tx.LockUserState(userState.UserWallet, userState.Asset) if err != nil { return rpc.Errorf("failed to lock user state: %v", err) } @@ -116,40 +156,13 @@ func (h *Handler) SubmitDepositState(c *rpc.Context) { } h.metrics.IncChannelStateSigValidation(sigType, true) - appSession, err := tx.GetAppSession(appStateUpd.AppSessionID) - if err != nil { - return rpc.Errorf("app session not found: %v", err) - } - if appSession == nil { - return rpc.Errorf("app session not found") - } - if len(reqPayload.QuorumSigs) > len(appSession.Participants) { - return rpc.Errorf("quorum_sigs count (%d) exceeds participants count (%d)", len(reqPayload.QuorumSigs), len(appSession.Participants)) - } - if appSession.Status == app.AppSessionStatusClosed { - return rpc.Errorf("app session is already closed") - } - if appStateUpd.Version != appSession.Version+1 { - return rpc.Errorf("invalid app session version: expected %d, got %d", appSession.Version+1, appStateUpd.Version) - } - - if appStateUpd.Intent != app.AppStateUpdateIntentDeposit { - return rpc.Errorf("invalid intent: expected 'deposit', got '%s'", appStateUpd.Intent) - } - - participantWeights := getParticipantWeights(appSession.Participants) - - if len(reqPayload.QuorumSigs) == 0 { - return rpc.Errorf("no signatures provided") - } - // Pack the app state update for signature verification packedStateUpdate, err := app.PackAppStateUpdateV1(appStateUpd) if err != nil { return rpc.Errorf("failed to pack app state update: %v", err) } - if err := h.verifyQuorum(tx, appStateUpd.AppSessionID, appSession.Application, participantWeights, appSession.Quorum, packedStateUpdate, reqPayload.QuorumSigs); err != nil { + if err := h.verifyQuorum(tx, appStateUpd.AppSessionID, appSession.ApplicationID, participantWeights, appSession.Quorum, packedStateUpdate, reqPayload.QuorumSigs); err != nil { return err } diff --git a/clearnode/api/app_session_v1/submit_deposit_state_test.go b/clearnode/api/app_session_v1/submit_deposit_state_test.go index 52d53e080..0612ec9a0 100644 --- a/clearnode/api/app_session_v1/submit_deposit_state_test.go +++ b/clearnode/api/app_session_v1/submit_deposit_state_test.go @@ -29,6 +29,7 @@ func TestSubmitDepositState_Success(t *testing.T) { handler := &Handler{ assetStore: mockAssetStore, + actionGateway: &MockActionGateway{}, stateAdvancer: core.NewStateAdvancerV1(mockAssetStore), statePacker: mockStatePacker, useStoreInTx: func(handler StoreTxHandler) error { @@ -56,8 +57,8 @@ func TestSubmitDepositState_Success(t *testing.T) { // Create existing app session existingAppSession := &app.AppSessionV1{ - SessionID: appSessionID, - Application: "test-app", + SessionID: appSessionID, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ { WalletAddress: participant1, @@ -152,6 +153,9 @@ func TestSubmitDepositState_Success(t *testing.T) { mockStore.On("GetLastUserState", participant1, asset, false).Return(currentUserState, nil).Once() mockStore.On("EnsureNoOngoingStateTransitions", participant1, asset).Return(nil).Once() mockAssetStore.On("GetAssetDecimals", asset).Return(uint8(6), nil) + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil).Maybe() mockStore.On("GetAppSession", appSessionID).Return(existingAppSession, nil).Once() // Mock allocations check - empty initially @@ -234,6 +238,7 @@ func TestSubmitDepositState_InvalidTransitionType(t *testing.T) { handler := &Handler{ assetStore: mockAssetStore, + actionGateway: &MockActionGateway{}, stateAdvancer: core.NewStateAdvancerV1(mockAssetStore), statePacker: mockStatePacker, useStoreInTx: func(handler StoreTxHandler) error { @@ -319,8 +324,22 @@ func TestSubmitDepositState_InvalidTransitionType(t *testing.T) { }, } - // Mock expectations - LockUserState is called before transition type check - mockStore.On("LockUserState", participant1, asset).Return(decimal.Zero, nil).Once() + // Mock expectations + existingAppSession := &app.AppSessionV1{ + SessionID: appSessionID, + ApplicationID: "test-app", + Participants: []app.AppParticipantV1{ + {WalletAddress: participant1, SignatureWeight: 1}, + }, + Quorum: 1, + Status: app.AppSessionStatusOpen, + Version: 1, + } + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil).Maybe() + mockStore.On("GetAppSession", appSessionID).Return(existingAppSession, nil).Once() + mockStore.On("LockUserState", participant1, asset).Return(decimal.Zero, nil).Maybe() // Create RPC request rpcState := toRPCState(userState) @@ -363,6 +382,7 @@ func TestSubmitDepositState_QuorumNotMet(t *testing.T) { handler := &Handler{ assetStore: mockAssetStore, + actionGateway: &MockActionGateway{}, stateAdvancer: core.NewStateAdvancerV1(mockAssetStore), statePacker: mockStatePacker, useStoreInTx: func(handler StoreTxHandler) error { @@ -390,8 +410,8 @@ func TestSubmitDepositState_QuorumNotMet(t *testing.T) { // Create existing app session with higher quorum requirement existingAppSession := &app.AppSessionV1{ - SessionID: appSessionID, - Application: "test-app", + SessionID: appSessionID, + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ { WalletAddress: participant1, @@ -477,6 +497,9 @@ func TestSubmitDepositState_QuorumNotMet(t *testing.T) { mockStore.On("GetLastUserState", participant1, asset, false).Return(currentUserState, nil).Once() mockStore.On("EnsureNoOngoingStateTransitions", participant1, asset).Return(nil).Once() mockAssetStore.On("GetAssetDecimals", asset).Return(uint8(6), nil) + mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{ + App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"}, + }, nil).Maybe() mockStore.On("GetAppSession", appSessionID).Return(existingAppSession, nil).Once() // Create RPC request diff --git a/clearnode/api/app_session_v1/testing.go b/clearnode/api/app_session_v1/testing.go index faf9bcfff..0756b2505 100644 --- a/clearnode/api/app_session_v1/testing.go +++ b/clearnode/api/app_session_v1/testing.go @@ -3,6 +3,7 @@ package app_session_v1 import ( "strings" "testing" + "time" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" @@ -10,6 +11,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/erc7824/nitrolite/clearnode/action_gateway" "github.com/erc7824/nitrolite/pkg/app" "github.com/erc7824/nitrolite/pkg/core" "github.com/erc7824/nitrolite/pkg/sign" @@ -141,6 +143,42 @@ func (m *MockStore) ValidateChannelSessionKeyForAsset(wallet, sessionKey, asset, return args.Bool(0), args.Error(1) } +func (m *MockStore) GetAppCount(ownerWallet string) (uint64, error) { + args := m.Called(ownerWallet) + return args.Get(0).(uint64), args.Error(1) +} + +func (m *MockStore) GetTotalUserStaked(wallet string) (decimal.Decimal, error) { + args := m.Called(wallet) + return args.Get(0).(decimal.Decimal), args.Error(1) +} + +func (m *MockStore) RecordAction(wallet string, gatedAction core.GatedAction) error { + args := m.Called(wallet, gatedAction) + return args.Error(0) +} + +func (m *MockStore) GetUserActionCount(wallet string, gatedAction core.GatedAction, window time.Duration) (uint64, error) { + args := m.Called(wallet, gatedAction, window) + return args.Get(0).(uint64), args.Error(1) +} + +func (m *MockStore) GetUserActionCounts(userWallet string, window time.Duration) (map[core.GatedAction]uint64, error) { + args := m.Called(userWallet, window) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(map[core.GatedAction]uint64), args.Error(1) +} + +type MockActionGateway struct { + Err error +} + +func (m *MockActionGateway) AllowAction(_ action_gateway.Store, _ string, _ core.GatedAction) error { + return m.Err +} + // MockSigValidator is a mock implementation of the SigValidator interface type MockSigValidator struct { mock.Mock diff --git a/clearnode/api/app_session_v1/utils.go b/clearnode/api/app_session_v1/utils.go index 98d5f7df0..400e3703e 100644 --- a/clearnode/api/app_session_v1/utils.go +++ b/clearnode/api/app_session_v1/utils.go @@ -28,10 +28,10 @@ func unmapAppDefinitionV1(def rpc.AppDefinitionV1) (app.AppDefinitionV1, error) } return app.AppDefinitionV1{ - Application: def.Application, - Participants: participants, - Quorum: def.Quorum, - Nonce: nonce, + ApplicationID: def.Application, + Participants: participants, + Quorum: def.Quorum, + Nonce: nonce, }, nil } @@ -219,12 +219,15 @@ func mapAppSessionInfoV1(session app.AppSessionV1, allocations map[string]map[st return rpc.AppSessionInfoV1{ AppSessionID: session.SessionID, Status: session.Status.String(), - Participants: participants, - SessionData: sessionData, - Quorum: session.Quorum, - Version: strconv.FormatUint(session.Version, 10), - Nonce: strconv.FormatUint(session.Nonce, 10), - Allocations: rpcAllocations, + AppDefinitionV1: rpc.AppDefinitionV1{ + Application: session.ApplicationID, + Participants: participants, + Quorum: session.Quorum, + Nonce: strconv.FormatUint(session.Nonce, 10), + }, + SessionData: sessionData, + Version: strconv.FormatUint(session.Version, 10), + Allocations: rpcAllocations, } } diff --git a/clearnode/api/apps_v1/get_apps_test.go b/clearnode/api/apps_v1/get_apps_test.go index cf4c567f5..206202c90 100644 --- a/clearnode/api/apps_v1/get_apps_test.go +++ b/clearnode/api/apps_v1/get_apps_test.go @@ -21,7 +21,7 @@ func TestGetApps_Success(t *testing.T) { }, } - handler := NewHandler(mockStore, 4096) + handler := NewHandler(mockStore, nil, nil, 4096) reqPayload := rpc.AppsV1GetAppsRequest{} payload, err := rpc.NewPayload(reqPayload) @@ -52,7 +52,7 @@ func TestGetApps_EmptyResults(t *testing.T) { }, } - handler := NewHandler(mockStore, 4096) + handler := NewHandler(mockStore, nil, nil, 4096) reqPayload := rpc.AppsV1GetAppsRequest{} payload, err := rpc.NewPayload(reqPayload) @@ -85,7 +85,7 @@ func TestGetApps_FilterByAppID(t *testing.T) { }, } - handler := NewHandler(mockStore, 4096) + handler := NewHandler(mockStore, nil, nil, 4096) aid := "app-1" reqPayload := rpc.AppsV1GetAppsRequest{AppID: &aid} @@ -119,7 +119,7 @@ func TestGetApps_FilterByOwnerWallet(t *testing.T) { }, } - handler := NewHandler(mockStore, 4096) + handler := NewHandler(mockStore, nil, nil, 4096) owner := "0x1111" reqPayload := rpc.AppsV1GetAppsRequest{OwnerWallet: &owner} diff --git a/clearnode/api/apps_v1/handler.go b/clearnode/api/apps_v1/handler.go index 16cc5804e..280e6e453 100644 --- a/clearnode/api/apps_v1/handler.go +++ b/clearnode/api/apps_v1/handler.go @@ -2,15 +2,19 @@ package apps_v1 // Handler manages app registry operations and provides RPC endpoints. type Handler struct { - store Store + store Store + useStoreInTx StoreTxProvider + actionGateway ActionGateway maxAppMetadataLen int } // NewHandler creates a new Handler instance with the provided dependencies. -func NewHandler(store Store, maxAppMetadataLen int) *Handler { +func NewHandler(store Store, useStoreInTx StoreTxProvider, actionGateway ActionGateway, maxAppMetadataLen int) *Handler { return &Handler{ store: store, + useStoreInTx: useStoreInTx, + actionGateway: actionGateway, maxAppMetadataLen: maxAppMetadataLen, } } diff --git a/clearnode/api/apps_v1/interface.go b/clearnode/api/apps_v1/interface.go index 94dfdbd73..4a5fa21a5 100644 --- a/clearnode/api/apps_v1/interface.go +++ b/clearnode/api/apps_v1/interface.go @@ -1,15 +1,33 @@ package apps_v1 import ( + "github.com/erc7824/nitrolite/clearnode/action_gateway" "github.com/erc7824/nitrolite/pkg/app" "github.com/erc7824/nitrolite/pkg/core" ) -// Store defines the persistence layer interface for app registry operations. +// StoreTxHandler is a function that executes Store operations within a transaction. +// If the handler returns an error, the transaction is rolled back; otherwise it's committed. +type StoreTxHandler func(Store) error + +// StoreTxProvider wraps Store operations in a database transaction. +// It accepts a StoreTxHandler and manages transaction lifecycle (begin, commit, rollback). +// Returns an error if the handler fails or the transaction cannot be committed. +type StoreTxProvider func(StoreTxHandler) error + +// Store defines the persistence layer interface for user data management. +// All methods should be implemented to work within database transactions. type Store interface { // CreateApp registers a new application. Returns an error if the app ID already exists. CreateApp(entry app.AppV1) error // GetApps retrieves applications with optional filtering. GetApps(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) + + action_gateway.Store +} + +type ActionGateway interface { + // AllowAppRegistration checks if a user is allowed to register a new application based on their past activity and allowances. + AllowAppRegistration(tx action_gateway.Store, userAddress string) error } diff --git a/clearnode/api/apps_v1/submit_app_version.go b/clearnode/api/apps_v1/submit_app_version.go index c1b78984e..c96df82b7 100644 --- a/clearnode/api/apps_v1/submit_app_version.go +++ b/clearnode/api/apps_v1/submit_app_version.go @@ -48,38 +48,46 @@ func (h *Handler) SubmitAppVersion(c *rpc.Context) { return } - appEntry := app.AppV1{ - ID: strings.ToLower(req.App.ID), - OwnerWallet: strings.ToLower(req.App.OwnerWallet), - Metadata: req.App.Metadata, - Version: version, - CreationApprovalNotRequired: req.App.CreationApprovalNotRequired, - } + err = h.useStoreInTx(func(tx Store) error { + err := h.actionGateway.AllowAppRegistration(tx, req.App.OwnerWallet) + if err != nil { + return rpc.NewError(err) + } - packedApp, err := app.PackAppV1(appEntry) - if err != nil { - c.Fail(rpc.Errorf("failed to pack app data: %v", err), "") - return - } + appEntry := app.AppV1{ + ID: strings.ToLower(req.App.ID), + OwnerWallet: strings.ToLower(req.App.OwnerWallet), + Metadata: req.App.Metadata, + Version: version, + CreationApprovalNotRequired: req.App.CreationApprovalNotRequired, + } - sigBytes, err := hexutil.Decode(req.OwnerSig) - if err != nil { - c.Fail(rpc.Errorf("failed to decode owner signature: %v", err), "") - return - } + packedApp, err := app.PackAppV1(appEntry) + if err != nil { + return rpc.Errorf("failed to pack app data: %v", err) + } - sigValidator, err := sign.NewSigValidator(sign.TypeEthereumMsg) - if err != nil { - c.Fail(rpc.Errorf("failed to create signature validator: %v", err), "") - return - } + sigBytes, err := hexutil.Decode(req.OwnerSig) + if err != nil { + return rpc.Errorf("failed to decode owner signature: %v", err) + } - if err := sigValidator.Verify(appEntry.OwnerWallet, packedApp, sigBytes); err != nil { - c.Fail(rpc.Errorf("invalid owner signature: %v", err), "") - return - } + sigValidator, err := sign.NewSigValidator(sign.TypeEthereumMsg) + if err != nil { + return rpc.Errorf("failed to create signature validator: %v", err) + } - if err := h.store.CreateApp(appEntry); err != nil { + if err := sigValidator.Verify(appEntry.OwnerWallet, packedApp, sigBytes); err != nil { + return rpc.Errorf("invalid owner signature: %v", err) + } + + if err := tx.CreateApp(appEntry); err != nil { + return rpc.Errorf("failed to create app") + } + + return nil + }) + if err != nil { c.Fail(err, "failed to create app") return } diff --git a/clearnode/api/apps_v1/submit_app_version_test.go b/clearnode/api/apps_v1/submit_app_version_test.go index 1ade9743e..840bca5ca 100644 --- a/clearnode/api/apps_v1/submit_app_version_test.go +++ b/clearnode/api/apps_v1/submit_app_version_test.go @@ -40,7 +40,10 @@ func testOwnerWallet(t *testing.T, appEntry app.AppV1) (address string, sig stri } func newHandlerWithDefaults(store Store) *Handler { - return NewHandler(store, 4096) + storeTxProvider := func(fn StoreTxHandler) error { + return fn(store) + } + return NewHandler(store, storeTxProvider, &MockActionGateway{}, 4096) } func TestSubmitAppVersion_Success(t *testing.T) { @@ -60,7 +63,11 @@ func TestSubmitAppVersion_Success(t *testing.T) { }, } - handler := newHandlerWithDefaults(mockStore) + storeTxProvider := func(fn StoreTxHandler) error { + return fn(mockStore) + } + + handler := NewHandler(mockStore, storeTxProvider, &MockActionGateway{}, 4096) reqPayload := rpc.AppsV1SubmitAppVersionRequest{ App: rpc.AppV1{ diff --git a/clearnode/api/apps_v1/testing.go b/clearnode/api/apps_v1/testing.go index 6a3c5a5a0..ed77642ff 100644 --- a/clearnode/api/apps_v1/testing.go +++ b/clearnode/api/apps_v1/testing.go @@ -1,8 +1,12 @@ package apps_v1 import ( + "time" + + "github.com/erc7824/nitrolite/clearnode/action_gateway" "github.com/erc7824/nitrolite/pkg/app" "github.com/erc7824/nitrolite/pkg/core" + "github.com/shopspring/decimal" ) // MockStore implements the Store interface for testing. @@ -24,3 +28,31 @@ func (m *MockStore) GetApps(appID *string, ownerWallet *string, pagination *core } return nil, core.PaginationMetadata{}, nil } + +func (m *MockStore) GetAppCount(_ string) (uint64, error) { + return 0, nil +} + +func (m *MockStore) GetTotalUserStaked(_ string) (decimal.Decimal, error) { + return decimal.Zero, nil +} + +func (m *MockStore) RecordAction(_ string, _ core.GatedAction) error { + return nil +} + +func (m *MockStore) GetUserActionCount(_ string, _ core.GatedAction, _ time.Duration) (uint64, error) { + return 0, nil +} + +func (m *MockStore) GetUserActionCounts(_ string, _ time.Duration) (map[core.GatedAction]uint64, error) { + return nil, nil +} + +type MockActionGateway struct { + Err error +} + +func (m *MockActionGateway) AllowAppRegistration(_ action_gateway.Store, _ string) error { + return m.Err +} diff --git a/clearnode/api/channel_v1/get_channels_test.go b/clearnode/api/channel_v1/get_channels_test.go index aa8b1f593..e10cf449f 100644 --- a/clearnode/api/channel_v1/get_channels_test.go +++ b/clearnode/api/channel_v1/get_channels_test.go @@ -31,6 +31,7 @@ func newGetChannelsHandler(mockTxStore *MockStore) *Handler { minChallenge: uint32(3600), metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } } diff --git a/clearnode/api/channel_v1/get_escrow_channel_test.go b/clearnode/api/channel_v1/get_escrow_channel_test.go index 3232fe3be..087cff6cc 100644 --- a/clearnode/api/channel_v1/get_escrow_channel_test.go +++ b/clearnode/api/channel_v1/get_escrow_channel_test.go @@ -37,6 +37,7 @@ func TestGetEscrowChannel_Success(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data diff --git a/clearnode/api/channel_v1/get_home_channel_test.go b/clearnode/api/channel_v1/get_home_channel_test.go index 9f1198cc5..cd5a31f9c 100644 --- a/clearnode/api/channel_v1/get_home_channel_test.go +++ b/clearnode/api/channel_v1/get_home_channel_test.go @@ -37,6 +37,7 @@ func TestGetHomeChannel_Success(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data @@ -124,6 +125,7 @@ func TestGetHomeChannel_NotFound(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data diff --git a/clearnode/api/channel_v1/get_latest_state_test.go b/clearnode/api/channel_v1/get_latest_state_test.go index 837547cc3..8392f853b 100644 --- a/clearnode/api/channel_v1/get_latest_state_test.go +++ b/clearnode/api/channel_v1/get_latest_state_test.go @@ -38,6 +38,7 @@ func TestGetLatestState_Success(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data @@ -136,6 +137,7 @@ func TestGetLatestState_OnlySigned(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data diff --git a/clearnode/api/channel_v1/handler.go b/clearnode/api/channel_v1/handler.go index d9086b6b2..4e2058522 100644 --- a/clearnode/api/channel_v1/handler.go +++ b/clearnode/api/channel_v1/handler.go @@ -13,6 +13,7 @@ import ( type Handler struct { useStoreInTx StoreTxProvider memoryStore MemoryStore + actionGateway ActionGateway nodeSigner *core.ChannelDefaultSigner stateAdvancer core.StateAdvancer statePacker core.StatePacker @@ -26,6 +27,7 @@ type Handler struct { func NewHandler( useStoreInTx StoreTxProvider, memoryStore MemoryStore, + actionGateway ActionGateway, nodeSigner *core.ChannelDefaultSigner, stateAdvancer core.StateAdvancer, statePacker core.StatePacker, @@ -39,6 +41,7 @@ func NewHandler( statePacker: statePacker, useStoreInTx: useStoreInTx, memoryStore: memoryStore, + actionGateway: actionGateway, nodeSigner: nodeSigner, nodeAddress: nodeAddress, minChallenge: minChallenge, diff --git a/clearnode/api/channel_v1/interface.go b/clearnode/api/channel_v1/interface.go index 28206873c..61f55860b 100644 --- a/clearnode/api/channel_v1/interface.go +++ b/clearnode/api/channel_v1/interface.go @@ -1,6 +1,7 @@ package channel_v1 import ( + "github.com/erc7824/nitrolite/clearnode/action_gateway" "github.com/erc7824/nitrolite/pkg/core" "github.com/shopspring/decimal" ) @@ -78,6 +79,13 @@ type Store interface { // exists at its latest version for the (wallet, sessionKey) pair, includes the given asset, // and matches the metadata hash. ValidateChannelSessionKeyForAsset(wallet, sessionKey, asset, metadataHash string) (bool, error) + + action_gateway.Store +} + +type ActionGateway interface { + // AllowAction checks if a user is allowed to perform a specific gated action based on their past activity and allowances. + AllowAction(tx action_gateway.Store, userAddress string, gatedAction core.GatedAction) error } // SigValidator validates cryptographic signatures on state transitions. diff --git a/clearnode/api/channel_v1/request_creation_test.go b/clearnode/api/channel_v1/request_creation_test.go index f43810d0a..c324d92cc 100644 --- a/clearnode/api/channel_v1/request_creation_test.go +++ b/clearnode/api/channel_v1/request_creation_test.go @@ -42,6 +42,7 @@ func TestRequestCreation_Success(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data - derive userWallet from a user signer key @@ -191,6 +192,7 @@ func TestRequestCreation_Acknowledgement_Success(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data - derive userWallet from a user signer key @@ -332,6 +334,7 @@ func TestRequestCreation_InvalidChallenge(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data diff --git a/clearnode/api/channel_v1/submit_state.go b/clearnode/api/channel_v1/submit_state.go index 1f3758b6b..11b144f8b 100644 --- a/clearnode/api/channel_v1/submit_state.go +++ b/clearnode/api/channel_v1/submit_state.go @@ -30,7 +30,12 @@ func (h *Handler) SubmitState(c *rpc.Context) { var nodeSig string incomingTransition := incomingState.Transition err = h.useStoreInTx(func(tx Store) error { - _, err := tx.LockUserState(incomingState.UserWallet, incomingState.Asset) + err := h.actionGateway.AllowAction(tx, incomingState.UserWallet, incomingState.Transition.Type.GatedAction()) + if err != nil { + return rpc.NewError(err) + } + + _, err = tx.LockUserState(incomingState.UserWallet, incomingState.Asset) if err != nil { return rpc.Errorf("failed to lock user state: %v", err) } diff --git a/clearnode/api/channel_v1/submit_state_test.go b/clearnode/api/channel_v1/submit_state_test.go index 73bd45ab1..b6086305d 100644 --- a/clearnode/api/channel_v1/submit_state_test.go +++ b/clearnode/api/channel_v1/submit_state_test.go @@ -42,6 +42,7 @@ func TestSubmitState_TransferSend_Success(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data - derive senderWallet from a user signer key @@ -216,6 +217,7 @@ func TestSubmitState_EscrowLock_Success(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data - derive userWallet from a user signer key @@ -371,6 +373,7 @@ func TestSubmitState_EscrowWithdraw_Success(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data - derive userWallet from a user signer key @@ -524,6 +527,7 @@ func TestSubmitState_HomeDeposit_Success(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data - derive userWallet from a user signer key @@ -655,6 +659,7 @@ func TestSubmitState_HomeWithdrawal_Success(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data - derive userWallet from a user signer key @@ -788,6 +793,7 @@ func TestSubmitState_MutualLock_Success(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data - derive userWallet from a user signer key @@ -943,6 +949,7 @@ func TestSubmitState_EscrowDeposit_Success(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data - derive userWallet from a user signer key @@ -1098,6 +1105,7 @@ func TestSubmitState_Finalize_Success(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data - derive userWallet from a user signer key @@ -1237,6 +1245,7 @@ func TestSubmitState_Acknowledgement_Success(t *testing.T) { minChallenge: minChallenge, metrics: metrics.NewNoopRuntimeMetricExporter(), maxSessionKeyIDs: 256, + actionGateway: &MockActionGateway{}, } // Test data - derive userWallet from a user signer key diff --git a/clearnode/api/channel_v1/testing.go b/clearnode/api/channel_v1/testing.go index 198f3efb0..c8ba6da70 100644 --- a/clearnode/api/channel_v1/testing.go +++ b/clearnode/api/channel_v1/testing.go @@ -1,11 +1,14 @@ package channel_v1 import ( + "time" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "github.com/shopspring/decimal" "github.com/stretchr/testify/mock" + "github.com/erc7824/nitrolite/clearnode/action_gateway" "github.com/erc7824/nitrolite/pkg/core" "github.com/erc7824/nitrolite/pkg/sign" ) @@ -111,6 +114,34 @@ func (m *MockStore) ValidateChannelSessionKeyForAsset(wallet, sessionKey, asset, return args.Bool(0), args.Error(1) } +func (m *MockStore) GetAppCount(ownerWallet string) (uint64, error) { + args := m.Called(ownerWallet) + return args.Get(0).(uint64), args.Error(1) +} + +func (m *MockStore) GetTotalUserStaked(wallet string) (decimal.Decimal, error) { + args := m.Called(wallet) + return args.Get(0).(decimal.Decimal), args.Error(1) +} + +func (m *MockStore) RecordAction(wallet string, gatedAction core.GatedAction) error { + args := m.Called(wallet, gatedAction) + return args.Error(0) +} + +func (m *MockStore) GetUserActionCount(wallet string, gatedAction core.GatedAction, window time.Duration) (uint64, error) { + args := m.Called(wallet, gatedAction, window) + return args.Get(0).(uint64), args.Error(1) +} + +func (m *MockStore) GetUserActionCounts(userWallet string, window time.Duration) (map[core.GatedAction]uint64, error) { + args := m.Called(userWallet, window) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(map[core.GatedAction]uint64), args.Error(1) +} + func NewMockSigner() sign.Signer { key, _ := crypto.GenerateKey() @@ -171,3 +202,11 @@ func (m *MockStatePacker) PackState(state core.State) ([]byte, error) { args := m.Called(state) return args.Get(0).([]byte), args.Error(1) } + +type MockActionGateway struct { + Err error +} + +func (m *MockActionGateway) AllowAction(_ action_gateway.Store, _ string, _ core.GatedAction) error { + return m.Err +} diff --git a/clearnode/api/metric_store.go b/clearnode/api/metric_store.go index b9a84117c..5c6ee9b1d 100644 --- a/clearnode/api/metric_store.go +++ b/clearnode/api/metric_store.go @@ -40,7 +40,7 @@ func (s *metricStore) UpdateAppSession(session app.AppSessionV1) error { return err } s.callbacks = append(s.callbacks, func() { - s.m.IncAppStateUpdate(session.Application) + s.m.IncAppStateUpdate(session.ApplicationID) }) return nil } diff --git a/clearnode/api/rpc_router.go b/clearnode/api/rpc_router.go index cfd5df4c8..1f362e330 100644 --- a/clearnode/api/rpc_router.go +++ b/clearnode/api/rpc_router.go @@ -3,6 +3,7 @@ package api import ( "time" + "github.com/erc7824/nitrolite/clearnode/action_gateway" "github.com/erc7824/nitrolite/clearnode/api/app_session_v1" "github.com/erc7824/nitrolite/clearnode/api/apps_v1" "github.com/erc7824/nitrolite/clearnode/api/channel_v1" @@ -46,6 +47,7 @@ func NewRPCRouter( signer sign.Signer, dbStore database.DatabaseStore, memoryStore memory.MemoryStore, + actionGateway *action_gateway.ActionGateway, runtimeMetrics metrics.RuntimeMetricExporter, logger log.Logger, ) *RPCRouter { @@ -80,6 +82,12 @@ func NewRPCRouter( useAppSessionV1StoreInTx := func(h app_session_v1.StoreTxHandler) error { return wrapWithMetrics(func(ms *metricStore) error { return h(ms) }) } + useAppV1StoreInTx := func(h apps_v1.StoreTxHandler) error { + return wrapWithMetrics(func(ms *metricStore) error { return h(ms) }) + } + useUserV1StoreInTx := func(h user_v1.StoreTxHandler) error { + return wrapWithMetrics(func(ms *metricStore) error { return h(ms) }) + } nodeAddress := signer.PublicKey().Address().String() @@ -91,12 +99,12 @@ func NewRPCRouter( panic("failed to create channel wallet signer: " + err.Error()) } - channelV1Handler := channel_v1.NewHandler(useChannelV1StoreInTx, memoryStore, nodeChannelSigner, stateAdvancer, statePacker, nodeAddress, cfg.MinChallenge, runtimeMetrics, cfg.MaxSessionKeyIDs) - appSessionV1Handler := app_session_v1.NewHandler(useAppSessionV1StoreInTx, memoryStore, signer, stateAdvancer, statePacker, nodeAddress, runtimeMetrics, + channelV1Handler := channel_v1.NewHandler(useChannelV1StoreInTx, memoryStore, actionGateway, nodeChannelSigner, stateAdvancer, statePacker, nodeAddress, cfg.MinChallenge, runtimeMetrics, cfg.MaxSessionKeyIDs) + appSessionV1Handler := app_session_v1.NewHandler(useAppSessionV1StoreInTx, memoryStore, actionGateway, signer, stateAdvancer, statePacker, nodeAddress, runtimeMetrics, cfg.MaxParticipants, cfg.MaxSessionDataLen, cfg.MaxSessionKeyIDs, cfg.MaxRebalanceSignedUpdates) - appsV1Handler := apps_v1.NewHandler(dbStore, cfg.MaxAppMetadataLen) + appsV1Handler := apps_v1.NewHandler(dbStore, useAppV1StoreInTx, actionGateway, cfg.MaxAppMetadataLen) nodeV1Handler := node_v1.NewHandler(memoryStore, nodeAddress, cfg.NodeVersion) - userV1Handler := user_v1.NewHandler(dbStore) + userV1Handler := user_v1.NewHandler(dbStore, useUserV1StoreInTx, actionGateway) appSessionV1Group := r.Node.NewGroup(rpc.AppSessionsV1Group.String()) appSessionV1Group.Handle(rpc.AppSessionsV1SubmitDepositStateMethod.String(), appSessionV1Handler.SubmitDepositState) @@ -132,6 +140,7 @@ func NewRPCRouter( userV1Group := r.Node.NewGroup(rpc.UserV1Group.String()) userV1Group.Handle(rpc.UserV1GetBalancesMethod.String(), userV1Handler.GetBalances) userV1Group.Handle(rpc.UserV1GetTransactionsMethod.String(), userV1Handler.GetTransactions) + userV1Group.Handle(rpc.UserV1GetActionAllowancesMethod.String(), userV1Handler.GetActionAllowances) return r } diff --git a/clearnode/api/user_v1/get_action_allowances.go b/clearnode/api/user_v1/get_action_allowances.go new file mode 100644 index 000000000..bbeb23fd5 --- /dev/null +++ b/clearnode/api/user_v1/get_action_allowances.go @@ -0,0 +1,59 @@ +package user_v1 + +import ( + "strconv" + + "github.com/erc7824/nitrolite/pkg/core" + "github.com/erc7824/nitrolite/pkg/rpc" +) + +// GetActionAllowances retrieves the action allowances for a user. +func (h *Handler) GetActionAllowances(c *rpc.Context) { + var req rpc.UserV1GetActionAllowancesRequest + if err := c.Request.Payload.Translate(&req); err != nil { + c.Fail(err, "failed to parse parameters") + return + } + + if req.Wallet == "" { + c.Fail(nil, "wallet is required") + return + } + + var allowances []core.ActionAllowance + err := h.useStoreInTx(func(tx Store) error { + var err error + allowances, err = h.actionGateway.GetUserAllowances(h.store, req.Wallet) + if err != nil { + return rpc.Errorf("failed to retrieve action allowances: %w", err) + } + + return nil + }) + if err != nil { + c.Fail(err, "failed to retrieve action allowances") + return + } + + rpcAllowances := make([]rpc.ActionAllowanceV1, len(allowances)) + for i, a := range allowances { + rpcAllowances[i] = rpc.ActionAllowanceV1{ + GatedAction: a.GatedAction, + TimeWindow: a.TimeWindow, + Allowance: strconv.FormatUint(a.Allowance, 10), + Used: strconv.FormatUint(a.Used, 10), + } + } + + response := rpc.UserV1GetActionAllowancesResponse{ + Allowances: rpcAllowances, + } + + payload, err := rpc.NewPayload(response) + if err != nil { + c.Fail(err, "failed to create response") + return + } + + c.Succeed(c.Request.Method, payload) +} diff --git a/clearnode/api/user_v1/get_action_allowances_test.go b/clearnode/api/user_v1/get_action_allowances_test.go new file mode 100644 index 000000000..04113ee28 --- /dev/null +++ b/clearnode/api/user_v1/get_action_allowances_test.go @@ -0,0 +1,145 @@ +package user_v1 + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/erc7824/nitrolite/pkg/core" + "github.com/erc7824/nitrolite/pkg/rpc" +) + +func TestGetActionAllowances_Success(t *testing.T) { + mockStore := new(MockStore) + storeTxProvider := func(fn StoreTxHandler) error { + return fn(mockStore) + } + + handler := NewHandler( + mockStore, + storeTxProvider, + &MockActionGateway{ + Allowances: []core.ActionAllowance{ + {GatedAction: core.GatedActionTransfer, TimeWindow: "24h", Allowance: 100, Used: 5}, + {GatedAction: core.GatedActionAppSessionCreation, TimeWindow: "24h", Allowance: 50, Used: 0}, + }, + }, + ) + + reqPayload := rpc.UserV1GetActionAllowancesRequest{ + Wallet: "0x1234567890123456789012345678901234567890", + } + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, "user.v1.get_action_allowances", payload), + } + + handler.GetActionAllowances(ctx) + + require.NotNil(t, ctx.Response) + require.NoError(t, ctx.Response.Error()) + + var response rpc.UserV1GetActionAllowancesResponse + err = ctx.Response.Payload.Translate(&response) + require.NoError(t, err) + + assert.Len(t, response.Allowances, 2) + assert.Equal(t, core.GatedActionTransfer, response.Allowances[0].GatedAction) + assert.Equal(t, "24h", response.Allowances[0].TimeWindow) + assert.Equal(t, "100", response.Allowances[0].Allowance) + assert.Equal(t, "5", response.Allowances[0].Used) + assert.Equal(t, core.GatedActionAppSessionCreation, response.Allowances[1].GatedAction) + assert.Equal(t, "50", response.Allowances[1].Allowance) + assert.Equal(t, "0", response.Allowances[1].Used) +} + +func TestGetActionAllowances_EmptyResult(t *testing.T) { + mockStore := new(MockStore) + storeTxProvider := func(fn StoreTxHandler) error { + return fn(mockStore) + } + + handler := NewHandler(mockStore, storeTxProvider, &MockActionGateway{}) + + reqPayload := rpc.UserV1GetActionAllowancesRequest{ + Wallet: "0x1234567890123456789012345678901234567890", + } + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, "user.v1.get_action_allowances", payload), + } + + handler.GetActionAllowances(ctx) + + require.NotNil(t, ctx.Response) + require.NoError(t, ctx.Response.Error()) + + var response rpc.UserV1GetActionAllowancesResponse + err = ctx.Response.Payload.Translate(&response) + require.NoError(t, err) + + assert.Empty(t, response.Allowances) +} + +func TestGetActionAllowances_MissingWallet(t *testing.T) { + mockStore := new(MockStore) + storeTxProvider := func(fn StoreTxHandler) error { + return fn(mockStore) + } + + handler := NewHandler(mockStore, storeTxProvider, &MockActionGateway{}) + + reqPayload := rpc.UserV1GetActionAllowancesRequest{} + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, "user.v1.get_action_allowances", payload), + } + + handler.GetActionAllowances(ctx) + + require.NotNil(t, ctx.Response) + err = ctx.Response.Error() + require.Error(t, err) + assert.Contains(t, err.Error(), "wallet is required") +} + +func TestGetActionAllowances_GatewayError(t *testing.T) { + mockStore := new(MockStore) + storeTxProvider := func(fn StoreTxHandler) error { + return fn(mockStore) + } + + handler := NewHandler(mockStore, storeTxProvider, &MockActionGateway{ + Err: fmt.Errorf("gateway failure"), + }) + + reqPayload := rpc.UserV1GetActionAllowancesRequest{ + Wallet: "0x1234567890123456789012345678901234567890", + } + payload, err := rpc.NewPayload(reqPayload) + require.NoError(t, err) + + ctx := &rpc.Context{ + Context: context.Background(), + Request: rpc.NewRequest(1, "user.v1.get_action_allowances", payload), + } + + handler.GetActionAllowances(ctx) + + require.NotNil(t, ctx.Response) + err = ctx.Response.Error() + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to retrieve action allowances") +} diff --git a/clearnode/api/user_v1/handler.go b/clearnode/api/user_v1/handler.go index 2ebdb7c52..d0d68d10a 100644 --- a/clearnode/api/user_v1/handler.go +++ b/clearnode/api/user_v1/handler.go @@ -2,14 +2,20 @@ package user_v1 // Handler manages user data operations and provides RPC endpoints. type Handler struct { - store Store + store Store + useStoreInTx StoreTxProvider + actionGateway ActionGateway } // NewHandler creates a new Handler instance with the provided dependencies. func NewHandler( store Store, + useStoreInTx StoreTxProvider, + actionGateway ActionGateway, ) *Handler { return &Handler{ - store: store, + store: store, + useStoreInTx: useStoreInTx, + actionGateway: actionGateway, } } diff --git a/clearnode/api/user_v1/interface.go b/clearnode/api/user_v1/interface.go index f8e68edde..6caeb4eee 100644 --- a/clearnode/api/user_v1/interface.go +++ b/clearnode/api/user_v1/interface.go @@ -1,9 +1,19 @@ package user_v1 import ( + "github.com/erc7824/nitrolite/clearnode/action_gateway" "github.com/erc7824/nitrolite/pkg/core" ) +// StoreTxHandler is a function that executes Store operations within a transaction. +// If the handler returns an error, the transaction is rolled back; otherwise it's committed. +type StoreTxHandler func(Store) error + +// StoreTxProvider wraps Store operations in a database transaction. +// It accepts a StoreTxHandler and manages transaction lifecycle (begin, commit, rollback). +// Returns an error if the handler fails or the transaction cannot be committed. +type StoreTxProvider func(StoreTxHandler) error + // Store defines the persistence layer interface for user data management. // All methods should be implemented to work within database transactions. type Store interface { @@ -17,4 +27,11 @@ type Store interface { FromTime *uint64, ToTime *uint64, Paginate *core.PaginationParams) ([]core.Transaction, core.PaginationMetadata, error) + + action_gateway.Store +} + +type ActionGateway interface { + // GetUserAllowances retrieves the action allowances for a user, which define what actions the user is permitted to perform. + GetUserAllowances(tx action_gateway.Store, userAddress string) ([]core.ActionAllowance, error) } diff --git a/clearnode/api/user_v1/testing.go b/clearnode/api/user_v1/testing.go index 98079450b..17b02ae96 100644 --- a/clearnode/api/user_v1/testing.go +++ b/clearnode/api/user_v1/testing.go @@ -1,8 +1,12 @@ package user_v1 import ( + "time" + + "github.com/shopspring/decimal" "github.com/stretchr/testify/mock" + "github.com/erc7824/nitrolite/clearnode/action_gateway" "github.com/erc7824/nitrolite/pkg/core" ) @@ -30,3 +34,32 @@ func (m *MockStore) GetUserTransactions(Wallet string, Asset *string, TxType *co } return args.Get(0).([]core.Transaction), metadata, args.Error(2) } + +func (m *MockStore) GetAppCount(_ string) (uint64, error) { + return 0, nil +} + +func (m *MockStore) GetTotalUserStaked(_ string) (decimal.Decimal, error) { + return decimal.Zero, nil +} + +func (m *MockStore) RecordAction(_ string, _ core.GatedAction) error { + return nil +} + +func (m *MockStore) GetUserActionCount(_ string, _ core.GatedAction, _ time.Duration) (uint64, error) { + return 0, nil +} + +func (m *MockStore) GetUserActionCounts(_ string, _ time.Duration) (map[core.GatedAction]uint64, error) { + return nil, nil +} + +type MockActionGateway struct { + Allowances []core.ActionAllowance + Err error +} + +func (m *MockActionGateway) GetUserAllowances(_ action_gateway.Store, _ string) ([]core.ActionAllowance, error) { + return m.Allowances, m.Err +} diff --git a/clearnode/chart/config/prod/action_gateway.yaml b/clearnode/chart/config/prod/action_gateway.yaml new file mode 100644 index 000000000..67ac1e8bc --- /dev/null +++ b/clearnode/chart/config/prod/action_gateway.yaml @@ -0,0 +1,19 @@ +# yaml-language-server: $schema=../../../config/schemas/action_gateway_schema.yaml +level_step_tokens: "100" +app_cost: "200" +action_gates: + transfer: + free_actions_allowance: 10 + increase_per_level: 5 + app_session_creation: + free_actions_allowance: 5 + increase_per_level: 2 + app_session_operation: + free_actions_allowance: 20 + increase_per_level: 10 + app_session_deposit: + free_actions_allowance: 15 + increase_per_level: 7 + app_session_withdrawal: + free_actions_allowance: 15 + increase_per_level: 7 diff --git a/clearnode/chart/config/prod/clearnode.yaml b/clearnode/chart/config/prod/clearnode.yaml index 351caf01f..10a5a9f10 100644 --- a/clearnode/chart/config/prod/clearnode.yaml +++ b/clearnode/chart/config/prod/clearnode.yaml @@ -12,7 +12,7 @@ config: MSG_EXPIRY_TIME: "60" image: - repository: ghcr.io/erc7824/nitrolite/clearnode + repository: ghcr.io/layer-3/nitrolite/clearnode tag: 0.3.1-rc.18 service: diff --git a/clearnode/chart/config/sandbox/action_gateway.yaml b/clearnode/chart/config/sandbox/action_gateway.yaml new file mode 100644 index 000000000..67ac1e8bc --- /dev/null +++ b/clearnode/chart/config/sandbox/action_gateway.yaml @@ -0,0 +1,19 @@ +# yaml-language-server: $schema=../../../config/schemas/action_gateway_schema.yaml +level_step_tokens: "100" +app_cost: "200" +action_gates: + transfer: + free_actions_allowance: 10 + increase_per_level: 5 + app_session_creation: + free_actions_allowance: 5 + increase_per_level: 2 + app_session_operation: + free_actions_allowance: 20 + increase_per_level: 10 + app_session_deposit: + free_actions_allowance: 15 + increase_per_level: 7 + app_session_withdrawal: + free_actions_allowance: 15 + increase_per_level: 7 diff --git a/clearnode/chart/config/sandbox/clearnode.yaml b/clearnode/chart/config/sandbox/clearnode.yaml index 808785df9..420ccec06 100644 --- a/clearnode/chart/config/sandbox/clearnode.yaml +++ b/clearnode/chart/config/sandbox/clearnode.yaml @@ -12,7 +12,7 @@ config: MSG_EXPIRY_TIME: "60" image: - repository: ghcr.io/erc7824/nitrolite/clearnode + repository: ghcr.io/layer-3/nitrolite/clearnode tag: 6069d30 service: diff --git a/clearnode/chart/config/uat/action_gateway.yaml b/clearnode/chart/config/uat/action_gateway.yaml new file mode 100644 index 000000000..67ac1e8bc --- /dev/null +++ b/clearnode/chart/config/uat/action_gateway.yaml @@ -0,0 +1,19 @@ +# yaml-language-server: $schema=../../../config/schemas/action_gateway_schema.yaml +level_step_tokens: "100" +app_cost: "200" +action_gates: + transfer: + free_actions_allowance: 10 + increase_per_level: 5 + app_session_creation: + free_actions_allowance: 5 + increase_per_level: 2 + app_session_operation: + free_actions_allowance: 20 + increase_per_level: 10 + app_session_deposit: + free_actions_allowance: 15 + increase_per_level: 7 + app_session_withdrawal: + free_actions_allowance: 15 + increase_per_level: 7 diff --git a/clearnode/chart/config/uat/clearnode.yaml b/clearnode/chart/config/uat/clearnode.yaml index 8ab3f821f..da5db6b9f 100644 --- a/clearnode/chart/config/uat/clearnode.yaml +++ b/clearnode/chart/config/uat/clearnode.yaml @@ -12,7 +12,7 @@ config: MSG_EXPIRY_TIME: "60" image: - repository: ghcr.io/erc7824/nitrolite/clearnode + repository: ghcr.io/layer-3/nitrolite/clearnode tag: c9c4d4c service: diff --git a/clearnode/chart/config/v1-rc/action_gateway.yaml b/clearnode/chart/config/v1-rc/action_gateway.yaml new file mode 100644 index 000000000..67ac1e8bc --- /dev/null +++ b/clearnode/chart/config/v1-rc/action_gateway.yaml @@ -0,0 +1,19 @@ +# yaml-language-server: $schema=../../../config/schemas/action_gateway_schema.yaml +level_step_tokens: "100" +app_cost: "200" +action_gates: + transfer: + free_actions_allowance: 10 + increase_per_level: 5 + app_session_creation: + free_actions_allowance: 5 + increase_per_level: 2 + app_session_operation: + free_actions_allowance: 20 + increase_per_level: 10 + app_session_deposit: + free_actions_allowance: 15 + increase_per_level: 7 + app_session_withdrawal: + free_actions_allowance: 15 + increase_per_level: 7 diff --git a/clearnode/chart/config/v1-rc/clearnode.yaml b/clearnode/chart/config/v1-rc/clearnode.yaml index 3a21f8b52..b92af869c 100644 --- a/clearnode/chart/config/v1-rc/clearnode.yaml +++ b/clearnode/chart/config/v1-rc/clearnode.yaml @@ -22,7 +22,7 @@ config: CLEARNODE_MAX_SESSION_KEY_IDS: "256" image: - repository: ghcr.io/erc7824/nitrolite/clearnode + repository: ghcr.io/layer-3/nitrolite/clearnode tag: v1.0.0-rc.0 service: diff --git a/clearnode/chart/templates/configmap.yaml b/clearnode/chart/templates/configmap.yaml index fa64af12d..ff5a9d6f5 100644 --- a/clearnode/chart/templates/configmap.yaml +++ b/clearnode/chart/templates/configmap.yaml @@ -13,3 +13,5 @@ data: {{ .Values.config.blockchains | indent 4 }} assets.yaml: |- {{ .Values.config.assets | indent 4 }} + action_gateway.yaml: |- +{{ .Values.config.actionGateway | indent 4 }} diff --git a/clearnode/chart/values.yaml b/clearnode/chart/values.yaml index f4e303c2c..76213d450 100644 --- a/clearnode/chart/values.yaml +++ b/clearnode/chart/values.yaml @@ -37,13 +37,15 @@ config: blockchains: "" # -- Assets configuration assets: "" + # -- Action Gateway configuration + actionGateway: "" # -- Number of replicas replicaCount: 1 image: # -- Docker image repository - repository: ghcr.io/erc7824/nitrolite/clearnode + repository: ghcr.io/layer-3/nitrolite/clearnode # -- Docker image tag tag: v1.0.0-rc.0 diff --git a/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql b/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql index 5fb7e073f..c1cd2da03 100644 --- a/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql +++ b/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql @@ -119,7 +119,7 @@ CREATE TABLE app_sessions_v1 ( updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -CREATE INDEX idx_app_sessions_v1_application ON app_sessions_v1(application); +CREATE INDEX idx_app_sessions_v1_application ON app_sessions_v1(application_id); CREATE INDEX idx_app_sessions_v1_status ON app_sessions_v1(status); -- App Session Participants table: Participants in application sessions @@ -135,7 +135,7 @@ CREATE INDEX idx_app_session_participants_v1_wallet ON app_session_participants_ -- App Ledger table: Internal ledger entries for application sessions CREATE TABLE app_ledger_v1 ( - id CHAR(36) PRIMARY KEY, -- UUID + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_id CHAR(66) NOT NULL, -- Session ID asset_symbol VARCHAR(20) NOT NULL, wallet CHAR(42) NOT NULL, @@ -259,7 +259,33 @@ CREATE TABLE user_balances ( CREATE INDEX idx_user_balances_user_wallet ON user_balances(user_wallet); +-- User staked table: Stores staked amounts per user per blockchain +CREATE TABLE user_staked_v1 ( + user_wallet CHAR(42) NOT NULL, + blockchain_id NUMERIC(20,0) NOT NULL, + amount NUMERIC(78, 18) NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_wallet, blockchain_id) +); + +CREATE INDEX idx_user_staked_v1_user_wallet ON user_staked_v1(user_wallet); + +-- Action log table: Records user actions for rate limiting and auditing +CREATE TABLE action_log_v1 ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_wallet CHAR(42) NOT NULL, + gated_action SMALLINT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_action_log_v1_wallet_gated_action_created ON action_log_v1(user_wallet, gated_action, created_at DESC); + -- +goose Down +DROP INDEX IF EXISTS idx_action_log_v1_wallet_gated_action_created; +DROP TABLE IF EXISTS action_log_v1; +DROP INDEX IF EXISTS idx_user_staked_v1_user_wallet; +DROP TABLE IF EXISTS user_staked_v1; DROP INDEX IF EXISTS idx_user_balances_user_wallet; DROP TABLE IF EXISTS user_balances; DROP INDEX IF EXISTS idx_channel_session_key_assets_v1_asset; diff --git a/clearnode/config/schemas/action_gateway_schema.yaml b/clearnode/config/schemas/action_gateway_schema.yaml new file mode 100644 index 000000000..4f4997a67 --- /dev/null +++ b/clearnode/config/schemas/action_gateway_schema.yaml @@ -0,0 +1,31 @@ +$schema: "http://json-schema.org" +type: object +required: + - level_step_tokens + - app_cost + - action_gates +properties: + level_step_tokens: + type: string + pattern: "^(?:[1-9][0-9]*(\\.[0-9]+)?|0\\.[0-9]*[1-9][0-9]*)$" + description: "Decimal amount for level step tokens" + app_cost: + type: string + pattern: "^(?:[1-9][0-9]*(\\.[0-9]+)?|0\\.[0-9]*[1-9][0-9]*)$" + action_gates: + type: object + additionalProperties: + $ref: "#/definitions/ActionGateConfig" +definitions: + ActionGateConfig: + type: object + required: + - free_actions_allowance + - increase_per_level + properties: + free_actions_allowance: + type: integer + minimum: 0 + increase_per_level: + type: integer + minimum: 0 diff --git a/clearnode/main.go b/clearnode/main.go index d374d9f28..1e834277f 100644 --- a/clearnode/main.go +++ b/clearnode/main.go @@ -42,7 +42,7 @@ func main() { RateLimitPerSec: bb.RateLimitPerSec, RateLimitBurst: bb.RateLimitBurst, } - api.NewRPCRouter(rpcRouterCfg, bb.RpcNode, bb.StateSigner, bb.DbStore, bb.MemoryStore, bb.RuntimeMetrics, bb.Logger) + api.NewRPCRouter(rpcRouterCfg, bb.RpcNode, bb.StateSigner, bb.DbStore, bb.MemoryStore, bb.ActionGateway, bb.RuntimeMetrics, bb.Logger) rpcListenAddr := ":7824" rpcListenEndpoint := "/ws" diff --git a/clearnode/runtime.go b/clearnode/runtime.go index 0d47c4684..c8df85a83 100644 --- a/clearnode/runtime.go +++ b/clearnode/runtime.go @@ -15,6 +15,7 @@ import ( "github.com/joho/godotenv" "github.com/prometheus/client_golang/prometheus" + "github.com/erc7824/nitrolite/clearnode/action_gateway" "github.com/erc7824/nitrolite/clearnode/metrics" "github.com/erc7824/nitrolite/clearnode/store/database" "github.com/erc7824/nitrolite/clearnode/store/memory" @@ -41,6 +42,7 @@ type Backbone struct { DbStore database.DatabaseStore MemoryStore memory.MemoryStore + ActionGateway *action_gateway.ActionGateway RpcNode rpc.Node StateSigner sign.Signer TxSigner sign.Signer @@ -137,6 +139,15 @@ func InitBackbone() *Backbone { logger.Fatal("failed to load blockchains", "error", err) } + // ------------------------------------------------ + // Action Gateway + // ------------------------------------------------ + + actionGateway, err := action_gateway.NewActionGatewayFromYaml(configDirPath) + if err != nil { + logger.Fatal("failed to initialize action gateway", "error", err) + } + // ------------------------------------------------ // Signer // ------------------------------------------------ @@ -246,6 +257,7 @@ func InitBackbone() *Backbone { DbStore: dbStore, MemoryStore: memoryStore, + ActionGateway: actionGateway, RpcNode: rpcNode, StateSigner: stateSigner, TxSigner: txSigner, diff --git a/clearnode/store/database/action_log.go b/clearnode/store/database/action_log.go new file mode 100644 index 000000000..01de38444 --- /dev/null +++ b/clearnode/store/database/action_log.go @@ -0,0 +1,91 @@ +package database + +import ( + "fmt" + "strings" + "time" + + "github.com/erc7824/nitrolite/pkg/core" + "github.com/google/uuid" +) + +type ActionLogEntryV1 struct { + ID uuid.UUID `gorm:"type:char(36);primaryKey"` + UserWallet string `gorm:"column:user_wallet;not null"` + GatedAction uint8 `gorm:"column:gated_action;not null"` + CreatedAt time.Time +} + +func (ActionLogEntryV1) TableName() string { + return "action_log_v1" +} + +// RecordAction inserts a new action log entry for a user. +func (s *DBStore) RecordAction(wallet string, gatedAction core.GatedAction) error { + if gatedAction.ID() == 0 { + return fmt.Errorf("invalid gated action ID") + } + + wallet = strings.ToLower(wallet) + + entry := ActionLogEntryV1{ + ID: uuid.New(), + UserWallet: wallet, + GatedAction: gatedAction.ID(), + CreatedAt: time.Now(), + } + + if err := s.db.Create(&entry).Error; err != nil { + return fmt.Errorf("failed to record action log entry: %w", err) + } + + return nil +} + +// GetUserActionCount returns the number of actions matching the given wallet and gated action +// within the specified time window (counting backwards from now). +func (s *DBStore) GetUserActionCount(wallet string, gatedAction core.GatedAction, window time.Duration) (uint64, error) { + wallet = strings.ToLower(wallet) + since := time.Now().Add(-window) + + query := s.db.Model(&ActionLogEntryV1{}). + Where("user_wallet = ? AND gated_action = ? AND created_at >= ?", wallet, gatedAction.ID(), since) + + var count int64 + if err := query.Count(&count).Error; err != nil { + return 0, fmt.Errorf("failed to get user action count: %w", err) + } + + return uint64(count), nil +} + +func (s *DBStore) GetUserActionCounts(userWallet string, window time.Duration) (map[core.GatedAction]uint64, error) { + userWallet = strings.ToLower(userWallet) + since := time.Now().Add(-window) + + query := s.db.Model(&ActionLogEntryV1{}). + Select("gated_action, COUNT(id) as count"). + Where("user_wallet = ? AND created_at >= ?", userWallet, since). + Group("gated_action") + + type Result struct { + GatedAction uint8 + Count int64 + } + + var results []Result + if err := query.Scan(&results).Error; err != nil { + return nil, fmt.Errorf("failed to get user action counts: %w", err) + } + + counts := make(map[core.GatedAction]uint64) + for _, r := range results { + action, ok := core.GatedActionFromID(r.GatedAction) + if !ok { + continue + } + counts[action] = uint64(r.Count) + } + + return counts, nil +} diff --git a/clearnode/store/database/action_log_test.go b/clearnode/store/database/action_log_test.go new file mode 100644 index 000000000..b416135c2 --- /dev/null +++ b/clearnode/store/database/action_log_test.go @@ -0,0 +1,249 @@ +package database + +import ( + "testing" + "time" + + "github.com/erc7824/nitrolite/pkg/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRecordAction(t *testing.T) { + t.Run("records action successfully", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := NewDBStore(db) + + err := store.RecordAction("0xUser123", core.GatedActionTransfer) + require.NoError(t, err) + + count, err := store.GetUserActionCount("0xuser123", core.GatedActionTransfer, time.Hour) + require.NoError(t, err) + assert.Equal(t, uint64(1), count) + }) + + t.Run("records multiple actions", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := NewDBStore(db) + + require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer)) + require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer)) + require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer)) + + count, err := store.GetUserActionCount("0xuser", core.GatedActionTransfer, time.Hour) + require.NoError(t, err) + assert.Equal(t, uint64(3), count) + }) + + t.Run("normalizes wallet to lowercase", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := NewDBStore(db) + + require.NoError(t, store.RecordAction("0xABCDEF", core.GatedActionTransfer)) + + count, err := store.GetUserActionCount("0xabcdef", core.GatedActionTransfer, time.Hour) + require.NoError(t, err) + assert.Equal(t, uint64(1), count) + }) +} + +func TestGetUserActionCount(t *testing.T) { + t.Run("returns zero for no actions", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := NewDBStore(db) + + count, err := store.GetUserActionCount("0xuser", core.GatedActionTransfer, time.Hour) + require.NoError(t, err) + assert.Equal(t, uint64(0), count) + }) + + t.Run("filters by gated action", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := NewDBStore(db) + + require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer)) + require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer)) + require.NoError(t, store.RecordAction("0xuser", core.GatedActionAppSessionOperation)) + + transferCount, err := store.GetUserActionCount("0xuser", core.GatedActionTransfer, time.Hour) + require.NoError(t, err) + assert.Equal(t, uint64(2), transferCount) + + opCount, err := store.GetUserActionCount("0xuser", core.GatedActionAppSessionOperation, time.Hour) + require.NoError(t, err) + assert.Equal(t, uint64(1), opCount) + }) + + t.Run("filters by wallet", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := NewDBStore(db) + + require.NoError(t, store.RecordAction("0xuser1", core.GatedActionTransfer)) + require.NoError(t, store.RecordAction("0xuser1", core.GatedActionTransfer)) + require.NoError(t, store.RecordAction("0xuser2", core.GatedActionTransfer)) + + count, err := store.GetUserActionCount("0xuser1", core.GatedActionTransfer, time.Hour) + require.NoError(t, err) + assert.Equal(t, uint64(2), count) + + count, err = store.GetUserActionCount("0xuser2", core.GatedActionTransfer, time.Hour) + require.NoError(t, err) + assert.Equal(t, uint64(1), count) + }) + + t.Run("respects time window", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := NewDBStore(db) + + oldEntry := ActionLogEntryV1{ + ID: [16]byte{1}, + UserWallet: "0xuser", + GatedAction: core.GatedActionTransfer.ID(), + CreatedAt: time.Now().Add(-2 * time.Hour), + } + require.NoError(t, db.Create(&oldEntry).Error) + require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer)) + + count, err := store.GetUserActionCount("0xuser", core.GatedActionTransfer, time.Hour) + require.NoError(t, err) + assert.Equal(t, uint64(1), count) + + count, err = store.GetUserActionCount("0xuser", core.GatedActionTransfer, 3*time.Hour) + require.NoError(t, err) + assert.Equal(t, uint64(2), count) + }) +} + +func TestGetUserActionCounts(t *testing.T) { + t.Run("returns empty map for no actions", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := NewDBStore(db) + + counts, err := store.GetUserActionCounts("0xuser", time.Hour) + require.NoError(t, err) + assert.Empty(t, counts) + }) + + t.Run("groups by gated action", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := NewDBStore(db) + + require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer)) + require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer)) + require.NoError(t, store.RecordAction("0xuser", core.GatedActionAppSessionOperation)) + + counts, err := store.GetUserActionCounts("0xuser", time.Hour) + require.NoError(t, err) + + assert.Equal(t, uint64(2), counts[core.GatedActionTransfer]) + assert.Equal(t, uint64(1), counts[core.GatedActionAppSessionOperation]) + }) + + t.Run("filters by wallet", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := NewDBStore(db) + + require.NoError(t, store.RecordAction("0xuser1", core.GatedActionTransfer)) + require.NoError(t, store.RecordAction("0xuser2", core.GatedActionTransfer)) + + counts, err := store.GetUserActionCounts("0xuser1", time.Hour) + require.NoError(t, err) + assert.Len(t, counts, 1) + }) + + t.Run("respects time window", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := NewDBStore(db) + + oldEntry := ActionLogEntryV1{ + ID: [16]byte{1}, + UserWallet: "0xuser", + GatedAction: core.GatedActionTransfer.ID(), + CreatedAt: time.Now().Add(-2 * time.Hour), + } + require.NoError(t, db.Create(&oldEntry).Error) + require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer)) + + counts, err := store.GetUserActionCounts("0xuser", time.Hour) + require.NoError(t, err) + assert.Equal(t, uint64(1), counts[core.GatedActionTransfer]) + + counts, err = store.GetUserActionCounts("0xuser", 3*time.Hour) + require.NoError(t, err) + assert.Equal(t, uint64(2), counts[core.GatedActionTransfer]) + }) + + t.Run("skips unknown gated action IDs", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := NewDBStore(db) + + // Insert an entry with an unknown gated action ID directly + entry := ActionLogEntryV1{ + ID: [16]byte{99}, + UserWallet: "0xuser", + GatedAction: 255, // unknown ID + CreatedAt: time.Now(), + } + require.NoError(t, db.Create(&entry).Error) + require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer)) + + counts, err := store.GetUserActionCounts("0xuser", time.Hour) + require.NoError(t, err) + assert.Len(t, counts, 1) + assert.Equal(t, uint64(1), counts[core.GatedActionTransfer]) + }) +} + +func TestGetAppCount(t *testing.T) { + t.Run("returns zero for no apps", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := NewDBStore(db) + + count, err := store.GetAppCount("0xowner") + require.NoError(t, err) + assert.Equal(t, uint64(0), count) + }) + + t.Run("counts apps for owner", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := NewDBStore(db) + + require.NoError(t, db.Create(&AppV1{ID: "app1", OwnerWallet: "0xowner1", Metadata: "{}"}).Error) + require.NoError(t, db.Create(&AppV1{ID: "app2", OwnerWallet: "0xowner1", Metadata: "{}"}).Error) + require.NoError(t, db.Create(&AppV1{ID: "app3", OwnerWallet: "0xowner2", Metadata: "{}"}).Error) + + count, err := store.GetAppCount("0xowner1") + require.NoError(t, err) + assert.Equal(t, uint64(2), count) + + count, err = store.GetAppCount("0xowner2") + require.NoError(t, err) + assert.Equal(t, uint64(1), count) + }) + + t.Run("normalizes wallet to lowercase", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := NewDBStore(db) + + require.NoError(t, db.Create(&AppV1{ID: "app1", OwnerWallet: "0xabcdef", Metadata: "{}"}).Error) + + count, err := store.GetAppCount("0xABCDEF") + require.NoError(t, err) + assert.Equal(t, uint64(1), count) + }) +} diff --git a/clearnode/store/database/app.go b/clearnode/store/database/app.go index cd0eed5ea..9e38bce21 100644 --- a/clearnode/store/database/app.go +++ b/clearnode/store/database/app.go @@ -106,3 +106,13 @@ func databaseAppToCore(dbApp *AppV1) app.AppInfoV1 { UpdatedAt: dbApp.UpdatedAt, } } + +func (s *DBStore) GetAppCount(ownerWallet string) (uint64, error) { + var count int64 + err := s.db.Model(&AppV1{}).Where("owner_wallet = ?", strings.ToLower(ownerWallet)).Count(&count).Error + if err != nil { + return 0, fmt.Errorf("failed to count apps: %w", err) + } + + return uint64(count), nil +} diff --git a/clearnode/store/database/app_ledger_test.go b/clearnode/store/database/app_ledger_test.go index ac4cc7fca..9ff2f2443 100644 --- a/clearnode/store/database/app_ledger_test.go +++ b/clearnode/store/database/app_ledger_test.go @@ -204,9 +204,9 @@ func TestDBStore_GetParticipantAllocations(t *testing.T) { // Create app session with participant session := app.AppSessionV1{ - SessionID: "session123", - Application: "poker", - Nonce: 1, + SessionID: "session123", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -242,9 +242,9 @@ func TestDBStore_GetParticipantAllocations(t *testing.T) { // Create app session with participants session := app.AppSessionV1{ - SessionID: "session456", - Application: "poker", - Nonce: 1, + SessionID: "session456", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -286,9 +286,9 @@ func TestDBStore_GetParticipantAllocations(t *testing.T) { // Create app session with participant session := app.AppSessionV1{ - SessionID: "session789", - Application: "poker", - Nonce: 1, + SessionID: "session789", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -326,9 +326,9 @@ func TestDBStore_GetParticipantAllocations(t *testing.T) { // Create app session with participant but no ledger entries session := app.AppSessionV1{ - SessionID: "session100", - Application: "poker", - Nonce: 1, + SessionID: "session100", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -366,9 +366,9 @@ func TestDBStore_GetParticipantAllocations(t *testing.T) { // Create app session with one participant session := app.AppSessionV1{ - SessionID: "session200", - Application: "poker", - Nonce: 1, + SessionID: "session200", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -406,9 +406,9 @@ func TestDBStore_GetParticipantAllocations(t *testing.T) { // Create two app sessions with the same participant session1 := app.AppSessionV1{ - SessionID: "session300", - Application: "poker", - Nonce: 1, + SessionID: "session300", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -423,9 +423,9 @@ func TestDBStore_GetParticipantAllocations(t *testing.T) { require.NoError(t, store.CreateAppSession(session1)) session2 := app.AppSessionV1{ - SessionID: "session400", - Application: "poker", - Nonce: 2, + SessionID: "session400", + ApplicationID: "poker", + Nonce: 2, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", diff --git a/clearnode/store/database/app_session.go b/clearnode/store/database/app_session.go index d82d6d7c5..f7500a710 100644 --- a/clearnode/store/database/app_session.go +++ b/clearnode/store/database/app_session.go @@ -52,7 +52,7 @@ func (s *DBStore) CreateAppSession(session app.AppSessionV1) error { dbSession := AppSessionV1{ ID: strings.ToLower(session.SessionID), - ApplicationID: session.Application, + ApplicationID: strings.ToLower(session.ApplicationID), Nonce: session.Nonce, Participants: participants, SessionData: session.SessionData, diff --git a/clearnode/store/database/app_session_key_state_test.go b/clearnode/store/database/app_session_key_state_test.go index 2818e1507..22dab70ce 100644 --- a/clearnode/store/database/app_session_key_state_test.go +++ b/clearnode/store/database/app_session_key_state_test.go @@ -852,9 +852,9 @@ func TestDBStore_GetAppSessionKeyOwner(t *testing.T) { // Create an app session that the session key references appSession := app.AppSessionV1{ - SessionID: testSess1, - Application: "poker", - Nonce: 1, + SessionID: testSess1, + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ {WalletAddress: testUser1, SignatureWeight: 100}, }, @@ -891,9 +891,9 @@ func TestDBStore_GetAppSessionKeyOwner(t *testing.T) { // Create an app session with a specific application appSession := app.AppSessionV1{ - SessionID: testSess1, - Application: testApp1, - Nonce: 1, + SessionID: testSess1, + ApplicationID: testApp1, + Nonce: 1, Participants: []app.AppParticipantV1{ {WalletAddress: testUser1, SignatureWeight: 100}, }, @@ -941,9 +941,9 @@ func TestDBStore_GetAppSessionKeyOwner(t *testing.T) { // Create an app session appSession := app.AppSessionV1{ - SessionID: testSess1, - Application: "poker", - Nonce: 1, + SessionID: testSess1, + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ {WalletAddress: testUser1, SignatureWeight: 100}, }, @@ -980,9 +980,9 @@ func TestDBStore_GetAppSessionKeyOwner(t *testing.T) { // Create an app session appSession := app.AppSessionV1{ - SessionID: testSess1, - Application: "poker", - Nonce: 1, + SessionID: testSess1, + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ {WalletAddress: testUser1, SignatureWeight: 100}, }, diff --git a/clearnode/store/database/app_session_test.go b/clearnode/store/database/app_session_test.go index 71948e093..239745cc4 100644 --- a/clearnode/store/database/app_session_test.go +++ b/clearnode/store/database/app_session_test.go @@ -28,9 +28,9 @@ func TestDBStore_CreateAppSession(t *testing.T) { store := NewDBStore(db) session := app.AppSessionV1{ - SessionID: "session123", - Application: "poker", - Nonce: 1, + SessionID: "session123", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -72,9 +72,9 @@ func TestDBStore_CreateAppSession(t *testing.T) { store := NewDBStore(db) session := app.AppSessionV1{ - SessionID: "session456", - Application: "chess", - Nonce: 1, + SessionID: "session456", + ApplicationID: "chess", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -118,9 +118,9 @@ func TestDBStore_CreateAppSession(t *testing.T) { store := NewDBStore(db) session := app.AppSessionV1{ - SessionID: "session789", - Application: "poker", - Nonce: 1, + SessionID: "session789", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -152,9 +152,9 @@ func TestDBStore_GetAppSession(t *testing.T) { store := NewDBStore(db) session := app.AppSessionV1{ - SessionID: "session123", - Application: "poker", - Nonce: 1, + SessionID: "session123", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -176,7 +176,7 @@ func TestDBStore_GetAppSession(t *testing.T) { require.NotNil(t, result) assert.Equal(t, "session123", result.SessionID) - assert.Equal(t, "poker", result.Application) + assert.Equal(t, "poker", result.ApplicationID) assert.Equal(t, uint64(1), result.Nonce) assert.Equal(t, `{"state": "active"}`, result.SessionData) assert.Equal(t, uint8(100), result.Quorum) @@ -207,9 +207,9 @@ func TestDBStore_GetAppSessions(t *testing.T) { // Create multiple sessions session1 := app.AppSessionV1{ - SessionID: "session1", - Application: "poker", - Nonce: 1, + SessionID: "session1", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -225,9 +225,9 @@ func TestDBStore_GetAppSessions(t *testing.T) { } session2 := app.AppSessionV1{ - SessionID: "session2", - Application: "chess", - Nonce: 1, + SessionID: "session2", + ApplicationID: "chess", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser456", @@ -264,9 +264,9 @@ func TestDBStore_GetAppSessions(t *testing.T) { store := NewDBStore(db) session1 := app.AppSessionV1{ - SessionID: "session1", - Application: "poker", - Nonce: 1, + SessionID: "session1", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -282,9 +282,9 @@ func TestDBStore_GetAppSessions(t *testing.T) { } session2 := app.AppSessionV1{ - SessionID: "session2", - Application: "chess", - Nonce: 1, + SessionID: "session2", + ApplicationID: "chess", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser456", @@ -319,9 +319,9 @@ func TestDBStore_GetAppSessions(t *testing.T) { store := NewDBStore(db) session1 := app.AppSessionV1{ - SessionID: "session1", - Application: "poker", - Nonce: 1, + SessionID: "session1", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -337,9 +337,9 @@ func TestDBStore_GetAppSessions(t *testing.T) { } session2 := app.AppSessionV1{ - SessionID: "session2", - Application: "chess", - Nonce: 1, + SessionID: "session2", + ApplicationID: "chess", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser456", @@ -374,9 +374,9 @@ func TestDBStore_GetAppSessions(t *testing.T) { store := NewDBStore(db) session1 := app.AppSessionV1{ - SessionID: "session1", - Application: "poker", - Nonce: 1, + SessionID: "session1", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -392,9 +392,9 @@ func TestDBStore_GetAppSessions(t *testing.T) { } session2 := app.AppSessionV1{ - SessionID: "session2", - Application: "chess", - Nonce: 1, + SessionID: "session2", + ApplicationID: "chess", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser456", @@ -432,9 +432,9 @@ func TestDBStore_GetAppSessions(t *testing.T) { for i := 1; i <= 3; i++ { sessionID := "session" + string(rune(i+'0')) session := app.AppSessionV1{ - SessionID: sessionID, - Application: "poker", - Nonce: uint64(i), + SessionID: sessionID, + ApplicationID: "poker", + Nonce: uint64(i), Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -500,9 +500,9 @@ func TestDBStore_UpdateAppSession(t *testing.T) { store := NewDBStore(db) session := app.AppSessionV1{ - SessionID: "session123", - Application: "poker", - Nonce: 1, + SessionID: "session123", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -544,9 +544,9 @@ func TestDBStore_UpdateAppSession(t *testing.T) { store := NewDBStore(db) session := app.AppSessionV1{ - SessionID: "nonexistent", - Application: "poker", - Nonce: 1, + SessionID: "nonexistent", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", @@ -573,9 +573,9 @@ func TestDBStore_UpdateAppSession(t *testing.T) { store := NewDBStore(db) session := app.AppSessionV1{ - SessionID: "session456", - Application: "poker", - Nonce: 1, + SessionID: "session456", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0xuser123", diff --git a/clearnode/store/database/database.go b/clearnode/store/database/database.go index a29e31ea0..4371a6592 100644 --- a/clearnode/store/database/database.go +++ b/clearnode/store/database/database.go @@ -213,7 +213,7 @@ func migratePostgres(cnf DatabaseConfig, embedMigrations embed.FS) error { } func migrateSqlite(db *gorm.DB) error { - if err := db.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &UserBalance{}); err != nil { + if err := db.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &UserBalance{}, &UserStakedV1{}, &ActionLogEntryV1{}); err != nil { return err } return nil diff --git a/clearnode/store/database/interface.go b/clearnode/store/database/interface.go index 62dfd16da..68b87a147 100644 --- a/clearnode/store/database/interface.go +++ b/clearnode/store/database/interface.go @@ -1,6 +1,8 @@ package database import ( + "time" + "github.com/erc7824/nitrolite/pkg/app" "github.com/erc7824/nitrolite/pkg/core" "github.com/shopspring/decimal" @@ -124,6 +126,9 @@ type DatabaseStore interface { // GetApps retrieves applications with optional filtering by app ID, owner wallet, and pagination. GetApps(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) + // GetAppCount returns the total number of applications owned by a specific wallet. + GetAppCount(ownerWallet string) (uint64, error) + // --- App Session Operations --- // CreateAppSession initializes a new application session. @@ -193,6 +198,26 @@ type DatabaseStore interface { // CountChannelsByStatus returns channel counts grouped by (asset, status). CountChannelsByStatus() ([]ChannelCount, error) + // --- User Staked Operations --- + + // UpdateUserStaked upserts the staked amount for a user on a specific blockchain. + UpdateUserStaked(wallet string, blockchainID uint64, amount decimal.Decimal) error + + // GetTotalUserStaked returns the total staked amount for a user across all blockchains. + GetTotalUserStaked(wallet string) (decimal.Decimal, error) + + // --- Action Log Operations --- + + // RecordAction inserts a new action log entry for a user. + RecordAction(wallet string, gatedAction core.GatedAction) error + + // GetUserActionCount returns the number of actions matching the given wallet, method, and path + // within the specified time window. + GetUserActionCount(wallet string, gatedAction core.GatedAction, window time.Duration) (uint64, error) + + // GetUserActionCounts returns a map of gated actions to their respective counts for a user within the specified time window. + GetUserActionCounts(userWallet string, window time.Duration) (map[core.GatedAction]uint64, error) + // --- Contract Event Operations --- // StoreContractEvent stores a blockchain event to prevent duplicate processing. diff --git a/clearnode/store/database/test/postgres_integration_test.go b/clearnode/store/database/test/postgres_integration_test.go index 59a44a316..b51b0adad 100644 --- a/clearnode/store/database/test/postgres_integration_test.go +++ b/clearnode/store/database/test/postgres_integration_test.go @@ -254,9 +254,9 @@ func TestPostgres_AppSessionOperations(t *testing.T) { t.Run("Create and retrieve app session", func(t *testing.T) { session := app.AppSessionV1{ - SessionID: "0x7234567890123456789012345678901234567890123456789012345678901234", - Application: "poker", - Nonce: 1, + SessionID: "0x7234567890123456789012345678901234567890123456789012345678901234", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0x7334567890123456789012345678901234567890", @@ -283,7 +283,7 @@ func TestPostgres_AppSessionOperations(t *testing.T) { require.NotNil(t, retrieved) assert.Equal(t, session.SessionID, retrieved.SessionID) - assert.Equal(t, session.Application, retrieved.Application) + assert.Equal(t, session.ApplicationID, retrieved.ApplicationID) assert.Len(t, retrieved.Participants, 2) }) @@ -291,9 +291,9 @@ func TestPostgres_AppSessionOperations(t *testing.T) { participant := "0x7534567890123456789012345678901234567890" session := app.AppSessionV1{ - SessionID: "0x7634567890123456789012345678901234567890123456789012345678901234", - Application: "poker", - Nonce: 1, + SessionID: "0x7634567890123456789012345678901234567890123456789012345678901234", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: participant, @@ -319,9 +319,9 @@ func TestPostgres_AppSessionOperations(t *testing.T) { t.Run("Update app session", func(t *testing.T) { session := app.AppSessionV1{ - SessionID: "0x7734567890123456789012345678901234567890123456789012345678901234", - Application: "poker", - Nonce: 1, + SessionID: "0x7734567890123456789012345678901234567890123456789012345678901234", + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ { WalletAddress: "0x7834567890123456789012345678901234567890", @@ -380,9 +380,9 @@ func TestPostgres_AppLedgerOperations(t *testing.T) { // Create session with participants session := app.AppSessionV1{ - SessionID: sessionID, - Application: "poker", - Nonce: 1, + SessionID: sessionID, + ApplicationID: "poker", + Nonce: 1, Participants: []app.AppParticipantV1{ {WalletAddress: wallet1, SignatureWeight: 50}, {WalletAddress: wallet2, SignatureWeight: 50}, diff --git a/clearnode/store/database/testing.go b/clearnode/store/database/testing.go index ca9d9f8c3..c429c7441 100644 --- a/clearnode/store/database/testing.go +++ b/clearnode/store/database/testing.go @@ -54,7 +54,7 @@ func setupTestSqlite(t testing.TB) *gorm.DB { t.Fatalf("Failed to open SQLite database: %v", err) } - err = database.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &AppSessionV1{}, &AppParticipantV1{}, &BlockchainAction{}, &Channel{}, &ContractEvent{}, &State{}, &Transaction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{}) + err = database.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &AppSessionV1{}, &AppParticipantV1{}, &BlockchainAction{}, &Channel{}, &ContractEvent{}, &State{}, &Transaction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{}, &UserStakedV1{}, &ActionLogEntryV1{}) if err != nil { t.Fatalf("Failed to run migrations: %v", err) } @@ -99,7 +99,7 @@ func setupTestPostgres(ctx context.Context, t testing.TB) (*gorm.DB, testcontain t.Fatalf("Failed to open PostgreSQL database: %v", err) } - err = database.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &Transaction{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{}) + err = database.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &Transaction{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{}, &UserStakedV1{}, &ActionLogEntryV1{}) if err != nil { t.Fatalf("Failed to run migrations: %v", err) } diff --git a/clearnode/store/database/user_staked.go b/clearnode/store/database/user_staked.go new file mode 100644 index 000000000..01db0fe18 --- /dev/null +++ b/clearnode/store/database/user_staked.go @@ -0,0 +1,75 @@ +package database + +import ( + "fmt" + "strings" + "time" + + "github.com/shopspring/decimal" + "gorm.io/gorm/clause" +) + +type UserStakedV1 struct { + UserWallet string `gorm:"column:user_wallet;primaryKey;not null"` + BlockchainID uint64 `gorm:"column:blockchain_id;primaryKey;not null"` + Amount decimal.Decimal `gorm:"column:amount;type:varchar(78);not null"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (UserStakedV1) TableName() string { + return "user_staked_v1" +} + +// UpdateUserStaked upserts the staked amount for a user on a specific blockchain. +func (s *DBStore) UpdateUserStaked(wallet string, blockchainID uint64, amount decimal.Decimal) error { + wallet = strings.ToLower(wallet) + + if wallet == "" { + return fmt.Errorf("wallet address must not be empty") + } + if blockchainID == 0 { + return fmt.Errorf("blockchain ID must not be zero") + } + if amount.IsNegative() { + return fmt.Errorf("staked amount must not be negative") + } + + now := time.Now() + + record := UserStakedV1{ + UserWallet: wallet, + BlockchainID: blockchainID, + Amount: amount, + CreatedAt: now, + UpdatedAt: now, + } + + err := s.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "user_wallet"}, {Name: "blockchain_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"amount", "updated_at"}), + }).Create(&record).Error + if err != nil { + return fmt.Errorf("failed to update user staked amount: %w", err) + } + + return nil +} + +// GetTotalUserStaked returns the total staked amount for a user across all blockchains. +func (s *DBStore) GetTotalUserStaked(wallet string) (decimal.Decimal, error) { + wallet = strings.ToLower(wallet) + + var result struct { + Total decimal.Decimal `gorm:"column:total"` + } + err := s.db.Model(&UserStakedV1{}). + Where("user_wallet = ?", wallet). + Select("COALESCE(SUM(amount), 0) AS total"). + Scan(&result).Error + if err != nil { + return decimal.Zero, fmt.Errorf("failed to get user staked total: %w", err) + } + + return result.Total, nil +} diff --git a/clearnode/store/database/utils.go b/clearnode/store/database/utils.go index ea62d7b37..6bac202b6 100644 --- a/clearnode/store/database/utils.go +++ b/clearnode/store/database/utils.go @@ -38,16 +38,16 @@ func databaseAppSessionToCore(dbSession *AppSessionV1) *app.AppSessionV1 { } return &app.AppSessionV1{ - SessionID: dbSession.ID, - Application: dbSession.ApplicationID, - Participants: participants, - Quorum: dbSession.Quorum, - Nonce: dbSession.Nonce, - Status: dbSession.Status, - Version: dbSession.Version, - SessionData: dbSession.SessionData, - CreatedAt: dbSession.CreatedAt, - UpdatedAt: dbSession.UpdatedAt, + SessionID: dbSession.ID, + ApplicationID: dbSession.ApplicationID, + Participants: participants, + Quorum: dbSession.Quorum, + Nonce: dbSession.Nonce, + Status: dbSession.Status, + Version: dbSession.Version, + SessionData: dbSession.SessionData, + CreatedAt: dbSession.CreatedAt, + UpdatedAt: dbSession.UpdatedAt, } } diff --git a/clearnode/stress/app_session.go b/clearnode/stress/app_session.go index f622e6f87..db19a6f16 100644 --- a/clearnode/stress/app_session.go +++ b/clearnode/stress/app_session.go @@ -72,10 +72,10 @@ func preGenerateSigs(p *pipe, numParticipants, numOperates int, nonce uint64, as } definition := app.AppDefinitionV1{ - Application: "stress-test", - Participants: participants, - Quorum: uint8(numParticipants), - Nonce: nonce, + ApplicationID: "stress-test", + Participants: participants, + Quorum: uint8(numParticipants), + Nonce: nonce, } sessionID, err := app.GenerateAppSessionIDV1(definition) diff --git a/docs/api.yaml b/docs/api.yaml index b25670dfa..6de9fe49b 100644 --- a/docs/api.yaml +++ b/docs/api.yaml @@ -156,7 +156,7 @@ types: - app_definition: description: Definition for an app session fields: - - name: application + - name: application_id type: string description: Application identifier from an app registry - name: participants @@ -338,24 +338,16 @@ types: - name: status type: string description: Session status (open/closed) - - name: participants - type: array - items: - type: app_participant - description: List of participant wallet addresses with weights + - name: app_definition + type: app_definition + description: The application definition for this session - name: session_data type: string description: JSON stringified session data optional: true - - name: quorum - type: integer - description: Quorum required for operations - name: version type: string description: Current version of the session state - - name: nonce - type: string - description: Nonce for the session - name: allocations type: array items: @@ -459,6 +451,22 @@ types: type: string description: Last update timestamp (unix seconds) + - action_allowance: + description: Allowance information for a specific gated action + fields: + - name: gated_action + type: string + description: The specific action being gated (transfer, app_session_deposit, app_session_operation, app_session_withdrawal) + - name: time_window + type: string + description: Time window for which the allowance is valid (e.g. "24h0m0s") + - name: allowance + type: string + description: Total allowance for the action within the time window + - name: used + type: string + description: Amount already used within the time window + - pagination_params: description: Pagination request parameters fields: @@ -984,6 +992,23 @@ api: type: pagination_metadata description: Pagination information optional: true + - name: get_action_allowances + description: Retrieve action allowances for a user based on their staking level + request: + - field_name: wallet + type: string + description: User's wallet address + response: + - field_name: allowances + type: array + items: + type: action_allowance + description: List of action allowances + errors: + - message: wallet_required + description: The wallet address is required + - message: retrieval_failed + description: Failed to retrieve action allowances - name: node description: Utility methods to get node's configuration and check connectivity diff --git a/pkg/app/app_session_v1.go b/pkg/app/app_session_v1.go index effb11875..74b771fca 100644 --- a/pkg/app/app_session_v1.go +++ b/pkg/app/app_session_v1.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/erc7824/nitrolite/pkg/core" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -37,6 +38,19 @@ func (intent AppStateUpdateIntent) String() string { } } +func (intent AppStateUpdateIntent) GatedAction() core.GatedAction { + switch intent { + case AppStateUpdateIntentOperate: + return core.GatedActionAppSessionOperation + case AppStateUpdateIntentDeposit: + return core.GatedActionAppSessionDeposit + case AppStateUpdateIntentWithdraw: + return core.GatedActionAppSessionWithdrawal + default: + return "" + } +} + type AppSessionStatus uint8 const ( @@ -60,16 +74,16 @@ func (status AppSessionStatus) String() string { // AppSessionV1 represents an application session in the V1 API. type AppSessionV1 struct { - SessionID string - Application string - Participants []AppParticipantV1 - Quorum uint8 - Nonce uint64 - Status AppSessionStatus - Version uint64 - SessionData string - CreatedAt time.Time - UpdatedAt time.Time + SessionID string + ApplicationID string + Participants []AppParticipantV1 + Quorum uint8 + Nonce uint64 + Status AppSessionStatus + Version uint64 + SessionData string + CreatedAt time.Time + UpdatedAt time.Time } // AppParticipantV1 represents the definition for an app participant. @@ -80,10 +94,10 @@ type AppParticipantV1 struct { // AppDefinitionV1 represents the definition for an app session. type AppDefinitionV1 struct { - Application string - Participants []AppParticipantV1 - Quorum uint8 - Nonce uint64 + ApplicationID string + Participants []AppParticipantV1 + Quorum uint8 + Nonce uint64 } // AppSessionVersionV1 represents a session ID and version pair for rebalancing operations. @@ -116,14 +130,12 @@ type SignedAppStateUpdateV1 struct { // AppSessionInfoV1 represents information about an application session. type AppSessionInfoV1 struct { - AppSessionID string - IsClosed bool - Participants []AppParticipantV1 - SessionData string - Quorum uint8 - Version uint64 - Nonce uint64 - Allocations []AppAllocationV1 + AppSessionID string + AppDefinition AppDefinitionV1 + IsClosed bool + SessionData string + Version uint64 + Allocations []AppAllocationV1 } // SessionKeyV1 represents a session key with spending allowances. @@ -183,7 +195,7 @@ func PackCreateAppSessionRequestV1(definition AppDefinitionV1, sessionData strin // Pack the data using ABI encoding packed, err := args.Pack( - definition.Application, + definition.ApplicationID, participants, definition.Quorum, definition.Nonce, @@ -294,7 +306,7 @@ func GenerateAppSessionIDV1(definition AppDefinitionV1) (string, error) { // Pack the data using ABI encoding packed, err := args.Pack( - definition.Application, + definition.ApplicationID, participants, definition.Quorum, definition.Nonce, diff --git a/pkg/app/app_session_v1_test.go b/pkg/app/app_session_v1_test.go index d1bc2b0cd..a5aca19d1 100644 --- a/pkg/app/app_session_v1_test.go +++ b/pkg/app/app_session_v1_test.go @@ -11,7 +11,7 @@ import ( func TestPackCreateAppSessionRequestV1(t *testing.T) { t.Parallel() def := AppDefinitionV1{ - Application: "chess-v1", + ApplicationID: "chess-v1", Participants: []AppParticipantV1{ {WalletAddress: "0x1111111111111111111111111111111111111111", SignatureWeight: 1}, {WalletAddress: "0x2222222222222222222222222222222222222222", SignatureWeight: 1}, @@ -48,7 +48,7 @@ func TestPackAppStateUpdateV1(t *testing.T) { func TestGenerateAppSessionIDV1(t *testing.T) { t.Parallel() def := AppDefinitionV1{ - Application: "chess-v1", + ApplicationID: "chess-v1", Participants: []AppParticipantV1{ {WalletAddress: "0x1111111111111111111111111111111111111111", SignatureWeight: 1}, }, diff --git a/pkg/core/types.go b/pkg/core/types.go index 332f93ab8..9d28c8640 100644 --- a/pkg/core/types.go +++ b/pkg/core/types.go @@ -842,6 +842,15 @@ func (t TransitionType) String() string { } } +func (t TransitionType) GatedAction() GatedAction { + switch t { + case TransitionTypeTransferSend: + return GatedActionTransfer + default: + return "" + } +} + // Transition represents a state transition type Transition struct { Type TransitionType `json:"type"` // Type of state transition @@ -903,22 +912,61 @@ type Token struct { Decimals uint8 `json:"decimals"` // Number of decimal places } -// SessionKey represents a session key with spending allowances -type SessionKey struct { - ID uint64 `json:"id"` // Unique identifier for the session key record - SessionKey string `json:"session_key"` // The address of the session key - Application string `json:"application"` // Name of the application authorized for this session key - Allowances []AssetAllowance `json:"allowances"` // Asset allowances with usage tracking - Scope *string `json:"scope,omitempty"` // Permission scope for this session key - ExpiresAt string `json:"expires_at"` // When this session key expires (ISO 8601 format) - CreatedAt string `json:"created_at"` // When the session key was created (ISO 8601 format) +// GatedAction represents an action that can be gated behind certain conditions, such as feature flags or access controls. +type GatedAction string + +var ( + GatedActionTransfer GatedAction = "transfer" + + GatedActionAppSessionCreation GatedAction = "app_session_creation" + GatedActionAppSessionOperation GatedAction = "app_session_operation" + GatedActionAppSessionDeposit GatedAction = "app_session_deposit" + GatedActionAppSessionWithdrawal GatedAction = "app_session_withdrawal" +) + +// ID returns a unique identifier for the GatedAction, which can be used for efficient storage and retrieval in databases or feature flag systems. +func (g GatedAction) ID() uint8 { + switch g { + case GatedActionTransfer: + return 1 + case GatedActionAppSessionCreation: + return 10 + case GatedActionAppSessionOperation: + return 11 + case GatedActionAppSessionDeposit: + return 12 + case GatedActionAppSessionWithdrawal: + return 13 + } + return 0 +} + +// GatedActionFromID returns the GatedAction corresponding to the given uint8 ID. +// Returns an empty GatedAction and false if the ID is unknown. +func GatedActionFromID(id uint8) (GatedAction, bool) { + switch id { + case 1: + return GatedActionTransfer, true + case 10: + return GatedActionAppSessionCreation, true + case 11: + return GatedActionAppSessionOperation, true + case 12: + return GatedActionAppSessionDeposit, true + case 13: + return GatedActionAppSessionWithdrawal, true + default: + return "", false + } } -// AssetAllowance represents asset allowance with usage tracking -type AssetAllowance struct { - Asset string `json:"asset"` // Symbol of the asset - Allowance decimal.Decimal `json:"allowance"` // Maximum amount the session key can spend - Used decimal.Decimal `json:"used"` // Amount already spent by this session key +// ActionAllowance represents the allowance information for a specific gated action, +// including the time window for which the allowance applies, the total allowance, and the amount used. +type ActionAllowance struct { + GatedAction GatedAction + TimeWindow string + Allowance uint64 + Used uint64 } // ========= Blockchain CLient Response Types ========= diff --git a/pkg/rpc/api.go b/pkg/rpc/api.go index d4d310336..5bb5f8194 100644 --- a/pkg/rpc/api.go +++ b/pkg/rpc/api.go @@ -360,6 +360,18 @@ type UserV1GetTransactionsResponse struct { Metadata PaginationMetadataV1 `json:"metadata"` } +// UserV1GetActionAllowancesRequest retrieves the current action allowances for a user. +type UserV1GetActionAllowancesRequest struct { + // Wallet is the user's wallet address + Wallet string `json:"wallet"` +} + +// UserV1GetActionAllowancesResponse returns the list of action allowances for the user. +type UserV1GetActionAllowancesResponse struct { + // Allowances is the list of action allowances for the user + Allowances []ActionAllowanceV1 `json:"allowances"` +} + // ============================================================================ // Node Group - V1 API // ============================================================================ diff --git a/pkg/rpc/client.go b/pkg/rpc/client.go index b3be86ecf..d43aafdd2 100644 --- a/pkg/rpc/client.go +++ b/pkg/rpc/client.go @@ -261,6 +261,15 @@ func (c *Client) UserV1GetTransactions(ctx context.Context, req UserV1GetTransac return resp, nil } +// UserV1GetActionAllowances retrieves the user's current action allowances for channels and app sessions. +func (c *Client) UserV1GetActionAllowances(ctx context.Context, req UserV1GetActionAllowancesRequest) (UserV1GetActionAllowancesResponse, error) { + var resp UserV1GetActionAllowancesResponse + if err := c.call(ctx, UserV1GetActionAllowancesMethod, req, &resp); err != nil { + return resp, err + } + return resp, nil +} + // ============================================================================ // Node Group - V1 API Methods // ============================================================================ diff --git a/pkg/rpc/client_test.go b/pkg/rpc/client_test.go index ef02f6826..ec5b3d319 100644 --- a/pkg/rpc/client_test.go +++ b/pkg/rpc/client_test.go @@ -362,12 +362,15 @@ func TestClientV1_AppSessionsV1GetAppSessions(t *testing.T) { { AppSessionID: testAppSession, Status: "open", - Participants: []rpc.AppParticipantV1{ - {WalletAddress: testWalletV1, SignatureWeight: 1}, + AppDefinitionV1: rpc.AppDefinitionV1{ + Application: "0xapp", + Participants: []rpc.AppParticipantV1{ + {WalletAddress: testWalletV1, SignatureWeight: 1}, + }, + Nonce: "1", + Quorum: 1, }, - Quorum: 1, Version: "1", - Nonce: "1", }, }, Metadata: rpc.PaginationMetadataV1{ diff --git a/pkg/rpc/error.go b/pkg/rpc/error.go index cd18473ef..50aa885ca 100644 --- a/pkg/rpc/error.go +++ b/pkg/rpc/error.go @@ -49,6 +49,24 @@ type Error struct { err error } +// NewError creates a new Error from an existing error. The message from the provided error will be included in the RPC response sent to the client. +// +// Use this function when you have an existing error that contains a user-friendly message +// that you want to send back to the client. If the original error message is not suitable +// for client consumption, consider using Errorf to create a new Error with a custom message. +// +// Example: +// +// // Creating an Error from an existing error +// if err := validateAddress(addr); err != nil { +// return rpc.NewError(err) +// } +// +// // This will send the original error message to the client, so ensure it's appropriate for exposure. +func NewError(err error) Error { + return Error{err: err} +} + // Errorf creates a new Error with a formatted error message that will be sent // to the client in the RPC response. This is the preferred way to create client-facing // errors in RPC handlers. diff --git a/pkg/rpc/methods.go b/pkg/rpc/methods.go index 4d116a599..de8a37df6 100644 --- a/pkg/rpc/methods.go +++ b/pkg/rpc/methods.go @@ -35,9 +35,10 @@ const ( AppsV1SubmitAppVersionMethod Method = "apps.v1.submit_app_version" // User Group - V1 Methods - UserV1Group Group = "user.v1" - UserV1GetBalancesMethod Method = "user.v1.get_balances" - UserV1GetTransactionsMethod Method = "user.v1.get_transactions" + UserV1Group Group = "user.v1" + UserV1GetBalancesMethod Method = "user.v1.get_balances" + UserV1GetTransactionsMethod Method = "user.v1.get_transactions" + UserV1GetActionAllowancesMethod Method = "user.v1.get_action_allowances" // Node Group - V1 Methods NodeV1Group Group = "node.v1" diff --git a/pkg/rpc/types.go b/pkg/rpc/types.go index 5f61f7764..685012d69 100644 --- a/pkg/rpc/types.go +++ b/pkg/rpc/types.go @@ -148,7 +148,7 @@ type AppParticipantV1 struct { // AppDefinitionV1 represents the definition for an app session. type AppDefinitionV1 struct { // Application is the application identifier from an app registry - Application string `json:"application"` + Application string `json:"application_id"` // Participants is the list of participants in the app session Participants []AppParticipantV1 `json:"participants"` // Quorum is the quorum required for the app session @@ -187,17 +187,13 @@ type AppSessionInfoV1 struct { AppSessionID string `json:"app_session_id"` // Status is the session status (open/closed) Status string `json:"status"` - // Participants is the list of participant wallet addresses with weights - Participants []AppParticipantV1 `json:"participants"` + // AppDefinitionV1 contains immutable application fields + AppDefinitionV1 AppDefinitionV1 `json:"app_definition"` // SessionData is the JSON stringified session data SessionData *string `json:"session_data,omitempty"` - // Quorum is the quorum required for operations - Quorum uint8 `json:"quorum"` // Version is the current version of the session state Version string `json:"version"` // Nonce is the nonce for the session - Nonce string `json:"nonce"` - // Allocations is the list of allocations in the app state Allocations []AppAllocationV1 `json:"allocations"` } @@ -324,6 +320,22 @@ type TransactionV1 struct { CreatedAt string `json:"created_at"` } +// ============================================================================ +// Action Gateway Types +// ============================================================================ + +// ActionAllowanceV1 represents the allowance information for a specific gated action. +type ActionAllowanceV1 struct { + // GatedAction is the specific action being gated (e.g. transfer, app_operation) + GatedAction core.GatedAction `json:"gated_action"` + // TimeWindow is the time window for which the allowance is valid (e.g. "1h", "24h") + TimeWindow string `json:"time_window"` + // Allowance is the total allowance for the gated action within the time window + Allowance string `json:"allowance"` + // Used is the amount of the allowance that has already been used within the time window + Used string `json:"used"` +} + // ============================================================================ // Pagination Types // ============================================================================ diff --git a/sdk/go/client_test.go b/sdk/go/client_test.go index 4dbbc7db4..6b7fdcc24 100644 --- a/sdk/go/client_test.go +++ b/sdk/go/client_test.go @@ -168,11 +168,13 @@ func TestClient_GetAppSessions(t *testing.T) { AppSessions: []rpc.AppSessionInfoV1{ { AppSessionID: "0xSessionID", - Participants: []rpc.AppParticipantV1{}, - Allocations: []rpc.AppAllocationV1{}, - Status: "open", - Nonce: "1", - Version: "1", + AppDefinitionV1: rpc.AppDefinitionV1{ + Participants: []rpc.AppParticipantV1{}, + Nonce: "1", + }, + Allocations: []rpc.AppAllocationV1{}, + Status: "open", + Version: "1", }, }, Metadata: rpc.PaginationMetadataV1{TotalCount: 1}, @@ -211,7 +213,7 @@ func TestClient_GetAppDefinition(t *testing.T) { def, err := client.GetAppDefinition(context.Background(), "0xSessionID") require.NoError(t, err) - assert.Equal(t, "0xApp", def.Application) + assert.Equal(t, "0xApp", def.ApplicationID) assert.Equal(t, uint64(1), def.Nonce) } @@ -232,7 +234,7 @@ func TestClient_CreateAppSession(t *testing.T) { } def := app.AppDefinitionV1{ - Application: "chess-v1", + ApplicationID: "chess-v1", Participants: []app.AppParticipantV1{ {WalletAddress: "0xAlice", SignatureWeight: 1}, {WalletAddress: "0xBob", SignatureWeight: 1}, diff --git a/sdk/go/examples/app_sessions/lifecycle.go b/sdk/go/examples/app_sessions/lifecycle.go index a2ffe1d86..c70f160ae 100644 --- a/sdk/go/examples/app_sessions/lifecycle.go +++ b/sdk/go/examples/app_sessions/lifecycle.go @@ -95,7 +95,7 @@ func main() { fmt.Println("=== Step 1: Creating App Session 1 (Wallet 1 only) ===") session1Definition := app.AppDefinitionV1{ - Application: "test-app", + ApplicationID: "test-app", Participants: []app.AppParticipantV1{ {WalletAddress: wallet1Address, SignatureWeight: 100}, }, @@ -186,7 +186,7 @@ func main() { } session2Definition := app.AppDefinitionV1{ - Application: appID, + ApplicationID: appID, Participants: []app.AppParticipantV1{ {WalletAddress: wallet2Address, SignatureWeight: 50}, {WalletAddress: wallet3Address, SignatureWeight: 50}, diff --git a/sdk/go/user.go b/sdk/go/user.go index c356f423a..75850c22c 100644 --- a/sdk/go/user.go +++ b/sdk/go/user.go @@ -3,6 +3,7 @@ package sdk import ( "context" "fmt" + "strconv" "github.com/erc7824/nitrolite/pkg/core" "github.com/erc7824/nitrolite/pkg/rpc" @@ -86,3 +87,46 @@ func (c *Client) GetTransactions(ctx context.Context, wallet string, opts *GetTr } return txs, transformPaginationMetadata(resp.Metadata), nil } + +// GetActionAllowances retrieves the action allowances for a user based on their staking level. +// +// Parameters: +// - wallet: The user's wallet address +// +// Returns: +// - Slice of ActionAllowance containing allowance information per gated action +// - Error if the request fails +func (c *Client) GetActionAllowances(ctx context.Context, wallet string) ([]core.ActionAllowance, error) { + req := rpc.UserV1GetActionAllowancesRequest{Wallet: wallet} + resp, err := c.rpcClient.UserV1GetActionAllowances(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get action allowances: %w", err) + } + allowances, err := transformActionAllowances(resp.Allowances) + if err != nil { + return nil, fmt.Errorf("failed to transform action allowances: %w", err) + } + return allowances, nil +} + +// transformActionAllowances converts RPC ActionAllowanceV1 slice to core.ActionAllowance slice. +func transformActionAllowances(allowances []rpc.ActionAllowanceV1) ([]core.ActionAllowance, error) { + result := make([]core.ActionAllowance, 0, len(allowances)) + for _, a := range allowances { + allowance, err := strconv.ParseUint(a.Allowance, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid allowance value %q for action %q: %w", a.Allowance, a.GatedAction, err) + } + used, err := strconv.ParseUint(a.Used, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid used value %q for action %q: %w", a.Used, a.GatedAction, err) + } + result = append(result, core.ActionAllowance{ + GatedAction: a.GatedAction, + TimeWindow: a.TimeWindow, + Allowance: allowance, + Used: used, + }) + } + return result, nil +} diff --git a/sdk/go/utils.go b/sdk/go/utils.go index d3fea8922..fcb218f77 100644 --- a/sdk/go/utils.go +++ b/sdk/go/utils.go @@ -385,13 +385,9 @@ func transformChannelDefinitionToRPC(def core.ChannelDefinition) rpc.ChannelDefi func transformAppSessions(sessions []rpc.AppSessionInfoV1) ([]app.AppSessionInfoV1, error) { result := make([]app.AppSessionInfoV1, 0, len(sessions)) for _, s := range sessions { - // Transform participants - participants := make([]app.AppParticipantV1, 0, len(s.Participants)) - for _, p := range s.Participants { - participants = append(participants, app.AppParticipantV1{ - WalletAddress: p.WalletAddress, - SignatureWeight: p.SignatureWeight, - }) + appDef, err := transformAppDefinition(s.AppDefinitionV1) + if err != nil { + return nil, fmt.Errorf("failed to transform app definition: %w", err) } // Transform allocations @@ -418,25 +414,18 @@ func transformAppSessions(sessions []rpc.AppSessionInfoV1) ([]app.AppSessionInfo sessionData = *s.SessionData } - nonce, err := strconv.ParseUint(s.Nonce, 10, 64) - if err != nil { - return nil, fmt.Errorf("failed to parse nonce: %w", err) - } - version, err := strconv.ParseUint(s.Version, 10, 64) if err != nil { return nil, fmt.Errorf("failed to parse version: %w", err) } result = append(result, app.AppSessionInfoV1{ - AppSessionID: s.AppSessionID, - IsClosed: isClosed, - Participants: participants, - SessionData: sessionData, - Quorum: s.Quorum, - Version: version, - Nonce: nonce, - Allocations: allocations, + AppSessionID: s.AppSessionID, + AppDefinition: appDef, + IsClosed: isClosed, + SessionData: sessionData, + Version: version, + Allocations: allocations, }) } return result, nil @@ -458,10 +447,10 @@ func transformAppDefinition(def rpc.AppDefinitionV1) (app.AppDefinitionV1, error } return app.AppDefinitionV1{ - Application: def.Application, - Participants: participants, - Quorum: def.Quorum, - Nonce: nonce, + ApplicationID: def.Application, + Participants: participants, + Quorum: def.Quorum, + Nonce: nonce, }, nil } @@ -476,7 +465,7 @@ func transformAppDefinitionToRPC(def app.AppDefinitionV1) rpc.AppDefinitionV1 { } return rpc.AppDefinitionV1{ - Application: def.Application, + Application: def.ApplicationID, Participants: participants, Quorum: def.Quorum, Nonce: strconv.FormatUint(def.Nonce, 10), diff --git a/sdk/go/utils_test.go b/sdk/go/utils_test.go index b1a0f0ae1..1bd46d32e 100644 --- a/sdk/go/utils_test.go +++ b/sdk/go/utils_test.go @@ -119,7 +119,7 @@ func TestTransformPaginationParams(t *testing.T) { limit := uint32(20) offset := uint32(0) sort := "desc" - + params := &core.PaginationParams{ Limit: &limit, Offset: &offset, @@ -226,7 +226,7 @@ func TestTransformState(t *testing.T) { rpcStateBack := transformStateToRPC(state) assert.Equal(t, rpcState.ID, rpcStateBack.ID) assert.Equal(t, rpcState.Epoch, rpcStateBack.Epoch) - + // Test error cases badState := rpcState badState.Epoch = "invalid" @@ -249,15 +249,17 @@ func TestTransformAppSessions(t *testing.T) { rpcSessions := []rpc.AppSessionInfoV1{ { AppSessionID: "0xSessionID", - Participants: []rpc.AppParticipantV1{ - {WalletAddress: "0xA", SignatureWeight: 1}, + AppDefinitionV1: rpc.AppDefinitionV1{ + Participants: []rpc.AppParticipantV1{ + {WalletAddress: "0xA", SignatureWeight: 1}, + }, + Nonce: "1", }, Allocations: []rpc.AppAllocationV1{ {Participant: "0xA", Asset: "USDC", Amount: "10.0"}, }, Status: "open", SessionData: &sessionData, - Nonce: "1", Version: "1", }, } @@ -268,7 +270,7 @@ func TestTransformAppSessions(t *testing.T) { assert.Equal(t, "0xSessionID", sessions[0].AppSessionID) assert.False(t, sessions[0].IsClosed) assert.Equal(t, "data", sessions[0].SessionData) - assert.Equal(t, uint64(1), sessions[0].Nonce) + assert.Equal(t, uint64(1), sessions[0].AppDefinition.Nonce) // Test error cases rpcSessions[0].Allocations[0].Amount = "invalid" @@ -276,7 +278,7 @@ func TestTransformAppSessions(t *testing.T) { assert.Error(t, err) rpcSessions[0].Allocations[0].Amount = "10.0" - rpcSessions[0].Nonce = "invalid" + rpcSessions[0].AppDefinitionV1.Nonce = "invalid" _, err = transformAppSessions(rpcSessions) assert.Error(t, err) } @@ -293,7 +295,7 @@ func TestTransformAppDefinition(t *testing.T) { def, err := transformAppDefinition(rpcDef) require.NoError(t, err) - assert.Equal(t, "0xApp", def.Application) + assert.Equal(t, "0xApp", def.ApplicationID) assert.Equal(t, uint64(1), def.Nonce) // Reverse @@ -351,7 +353,7 @@ func TestTransformChannelSessionKeyState(t *testing.T) { rpcStateBack := transformChannelSessionKeyStateToRPC(state) assert.Equal(t, rpcState.Version, rpcStateBack.Version) // rpcStateBack.UserAddress might be lowercased now - + // Error cases badState := rpcState badState.Version = "invalid" diff --git a/sdk/ts/examples/example-app/src/components/WalletDashboard.tsx b/sdk/ts/examples/example-app/src/components/WalletDashboard.tsx index 07bd46408..f2fa2e95b 100644 --- a/sdk/ts/examples/example-app/src/components/WalletDashboard.tsx +++ b/sdk/ts/examples/example-app/src/components/WalletDashboard.tsx @@ -736,15 +736,18 @@ export default function WalletDashboard({ )} -
+
+
+ App: {session.appDefinition.applicationId} +
v{session.version?.toString()}
- Quorum: {session.quorum} + Quorum: {session.appDefinition.quorum}
- {session.participants?.length || 0} participants + {session.appDefinition.participants?.length || 0} participants
{session.allocations && session.allocations.length > 0 && ( diff --git a/sdk/ts/src/app/packing.ts b/sdk/ts/src/app/packing.ts index c66686273..a382f0afa 100644 --- a/sdk/ts/src/app/packing.ts +++ b/sdk/ts/src/app/packing.ts @@ -37,7 +37,7 @@ export function packCreateAppSessionRequestV1( { type: 'string' }, // sessionData ], [ - definition.application, + definition.applicationId, participants, definition.quorum, definition.nonce, @@ -117,7 +117,7 @@ export function generateAppSessionIDV1(definition: AppDefinitionV1): `0x${string { type: 'uint8' }, // quorum { type: 'uint64' }, // nonce ], - [definition.application, participants, definition.quorum, definition.nonce] + [definition.applicationId, participants, definition.quorum, definition.nonce] ); // Return the Keccak256 hash as hex string diff --git a/sdk/ts/src/app/types.ts b/sdk/ts/src/app/types.ts index 16495fc39..0cfcebf3b 100644 --- a/sdk/ts/src/app/types.ts +++ b/sdk/ts/src/app/types.ts @@ -91,7 +91,7 @@ export interface AppParticipantV1 { * AppDefinitionV1 represents the definition for an app session */ export interface AppDefinitionV1 { - application: string; + applicationId: string; participants: AppParticipantV1[]; quorum: number; // uint8 nonce: bigint; // uint64 @@ -138,12 +138,10 @@ export interface SignedAppStateUpdateV1 { */ export interface AppSessionInfoV1 { appSessionId: string; + appDefinition: AppDefinitionV1; isClosed: boolean; - participants: AppParticipantV1[]; sessionData: string; - quorum: number; // uint8 version: bigint; // uint64 - nonce: bigint; // uint64 allocations: AppAllocationV1[]; } diff --git a/sdk/ts/src/client.ts b/sdk/ts/src/client.ts index 078834ca9..176eef9dd 100644 --- a/sdk/ts/src/client.ts +++ b/sdk/ts/src/client.ts @@ -29,6 +29,7 @@ import { transformSignedAppStateUpdateToRPC, transformAppSessionInfo, transformAppDefinitionFromRPC, + transformActionAllowance, } from './utils'; import * as blockchain from './blockchain'; import { nextState, applyChannelCreation, applyAcknowledgementTransition, applyHomeDepositTransition, applyHomeWithdrawalTransition, applyTransferSendTransition, applyFinalizeTransition, applyCommitTransition } from './core/state'; @@ -1053,6 +1054,26 @@ export class Client { }; } + /** + * GetActionAllowances retrieves the action allowances for a user based on their staking level. + * + * @param wallet - The user's wallet address + * @returns Array of action allowances for each gated action + * + * @example + * ```typescript + * const allowances = await client.getActionAllowances('0x1234...'); + * for (const a of allowances) { + * console.log(`${a.gatedAction}: ${a.used}/${a.allowance} (${a.timeWindow})`); + * } + * ``` + */ + async getActionAllowances(wallet: Address): Promise { + const req: API.UserV1GetActionAllowancesRequest = { wallet }; + const resp = await this.rpcClient.userV1GetActionAllowances(req); + return resp.allowances.map(transformActionAllowance); + } + // ============================================================================ // Channel Query Methods // ============================================================================ diff --git a/sdk/ts/src/core/types.ts b/sdk/ts/src/core/types.ts index 87cc95b3e..71eaab2d6 100644 --- a/sdk/ts/src/core/types.ts +++ b/sdk/ts/src/core/types.ts @@ -209,6 +209,13 @@ export interface BalanceEntry { balance: Decimal; } +export interface ActionAllowance { + gatedAction: string; + timeWindow: string; + allowance: bigint; + used: bigint; +} + // ============================================================================ // Pagination Types // ============================================================================ diff --git a/sdk/ts/src/rpc/api.ts b/sdk/ts/src/rpc/api.ts index 662e8481d..8aa59c34e 100644 --- a/sdk/ts/src/rpc/api.ts +++ b/sdk/ts/src/rpc/api.ts @@ -18,6 +18,7 @@ import { BlockchainInfoV1, AppV1, AppInfoV1, + ActionAllowanceV1, } from './types'; import { AppDefinitionV1, @@ -352,6 +353,16 @@ export interface UserV1GetTransactionsResponse { metadata: PaginationMetadataV1; } +export interface UserV1GetActionAllowancesRequest { + /** User's wallet address */ + wallet: Address; +} + +export interface UserV1GetActionAllowancesResponse { + /** List of action allowances */ + allowances: ActionAllowanceV1[]; +} + // ============================================================================ // Node Group - V1 API // ============================================================================ diff --git a/sdk/ts/src/rpc/client.ts b/sdk/ts/src/rpc/client.ts index 6de5dc2cf..31823f87c 100644 --- a/sdk/ts/src/rpc/client.ts +++ b/sdk/ts/src/rpc/client.ts @@ -234,6 +234,13 @@ export class RPCClient { return this.call(Methods.UserV1GetTransactionsMethod, req, signal); } + async userV1GetActionAllowances( + req: API.UserV1GetActionAllowancesRequest, + signal?: AbortSignal + ): Promise { + return this.call(Methods.UserV1GetActionAllowancesMethod, req, signal); + } + // ============================================================================ // Node Group - V1 API Methods // ============================================================================ diff --git a/sdk/ts/src/rpc/methods.ts b/sdk/ts/src/rpc/methods.ts index e7a33ee34..9fa980569 100644 --- a/sdk/ts/src/rpc/methods.ts +++ b/sdk/ts/src/rpc/methods.ts @@ -49,6 +49,7 @@ export const AppsV1SubmitAppVersionMethod: Method = 'apps.v1.submit_app_version' export const UserV1Group: Group = 'user.v1'; export const UserV1GetBalancesMethod: Method = 'user.v1.get_balances'; export const UserV1GetTransactionsMethod: Method = 'user.v1.get_transactions'; +export const UserV1GetActionAllowancesMethod: Method = 'user.v1.get_action_allowances'; // Node Group - V1 Methods export const NodeV1Group: Group = 'node.v1'; diff --git a/sdk/ts/src/rpc/types.ts b/sdk/ts/src/rpc/types.ts index 77d0d3e27..5d8921f92 100644 --- a/sdk/ts/src/rpc/types.ts +++ b/sdk/ts/src/rpc/types.ts @@ -277,6 +277,24 @@ export interface PaginationParamsV1 { limit?: number; // uint32 } +// ============================================================================ +// Action Allowance Types +// ============================================================================ + +/** + * ActionAllowanceV1 represents the allowance information for a specific gated action + */ +export interface ActionAllowanceV1 { + /** The specific action being gated (transfer, app_session_deposit, app_session_operation, app_session_withdrawal) */ + gated_action: string; + /** Time window for which the allowance is valid (e.g. "24h0m0s") */ + time_window: string; + /** Total allowance for the action within the time window */ + allowance: string; + /** Amount already used within the time window */ + used: string; +} + /** * PaginationMetadataV1 represents pagination information */ diff --git a/sdk/ts/src/utils.ts b/sdk/ts/src/utils.ts index be28d3b8b..95f7cbf85 100644 --- a/sdk/ts/src/utils.ts +++ b/sdk/ts/src/utils.ts @@ -4,7 +4,7 @@ import * as core from './core/types'; import * as API from './rpc/api'; -import { AssetV1, BalanceEntryV1, ChannelV1, LedgerV1, TransitionV1, StateV1, TransactionV1, PaginationMetadataV1 } from './rpc/types'; +import { AssetV1, BalanceEntryV1, ChannelV1, LedgerV1, TransitionV1, StateV1, TransactionV1, PaginationMetadataV1, ActionAllowanceV1 } from './rpc/types'; import Decimal from 'decimal.js'; import { Address } from 'viem'; @@ -277,6 +277,22 @@ export function transformPaginationMetadata( }; } +// ============================================================================ +// Action Allowance Transformations +// ============================================================================ + +/** + * Transform RPC ActionAllowanceV1 to core ActionAllowance + */ +export function transformActionAllowance(a: ActionAllowanceV1): core.ActionAllowance { + return { + gatedAction: a.gated_action, + timeWindow: a.time_window, + allowance: BigInt(a.allowance), + used: BigInt(a.used), + }; +} + // ============================================================================ // App Session Transformations // ============================================================================ @@ -290,7 +306,7 @@ import * as RPCApp from './rpc/api'; */ export function transformAppDefinitionToRPC(def: AppDefinitionV1): any { return { - application: def.application, + application_id: def.applicationId, participants: def.participants.map(p => ({ wallet_address: p.walletAddress, signature_weight: p.signatureWeight, @@ -340,15 +356,10 @@ export function transformSignedAppStateUpdateToRPC(signed: SignedAppStateUpdateV export function transformAppSessionInfo(raw: any): AppSessionInfoV1 { return { appSessionId: raw.app_session_id, + appDefinition: transformAppDefinitionFromRPC(raw.app_definition), isClosed: raw.status === 'closed', - participants: (raw.participants || []).map((p: any) => ({ - walletAddress: p.wallet_address as Address, - signatureWeight: p.signature_weight, - })), sessionData: raw.session_data || '', - quorum: raw.quorum, version: BigInt(raw.version), - nonce: BigInt(raw.nonce), allocations: (raw.allocations || []).map((a: any) => ({ participant: a.participant as Address, asset: a.asset, @@ -362,8 +373,11 @@ export function transformAppSessionInfo(raw: any): AppSessionInfoV1 { * The server returns snake_case JSON that needs conversion to SDK types. */ export function transformAppDefinitionFromRPC(raw: any): AppDefinitionV1 { + if (!raw.application_id || raw.nonce === undefined || raw.nonce === null) { + throw new Error('Invalid app definition: missing required fields (application_id, nonce)'); + } return { - application: raw.application, + applicationId: raw.application_id, participants: (raw.participants || []).map((p: any) => ({ walletAddress: p.wallet_address as Address, signatureWeight: p.signature_weight, From dc2cfaf08a6759b2b7e5a60f5f0b74c133fb66a1 Mon Sep 17 00:00:00 2001 From: Dmytro Steblyna <80773046+dimast-x@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:39:40 +0000 Subject: [PATCH 05/13] YNU-834: Rename erc7824 org to layer-3 & Remove erc7824 docs (#603) --- .dockerignore | 1 - .github/CODEOWNERS | 6 +- .github/dependabot.yml | 1 - .github/workflows/main-pr.yml | 19 - .github/workflows/main-push.yml | 16 - .github/workflows/test-go.yml | 2 +- .github/workflows/v1-push.yml | 17 - LICENSE | 2 +- README.md | 10 +- cerebro/README.md | 2 +- cerebro/commands.go | 8 +- cerebro/operator.go | 6 +- cerebro/operator_test.go | 2 +- clearnode/action_gateway/action_gateway.go | 2 +- .../action_gateway/action_gateway_test.go | 2 +- .../api/app_session_v1/create_app_session.go | 8 +- .../app_session_v1/create_app_session_test.go | 8 +- .../api/app_session_v1/get_app_definition.go | 2 +- .../app_session_v1/get_app_definition_test.go | 8 +- .../api/app_session_v1/get_app_sessions.go | 6 +- .../app_session_v1/get_app_sessions_test.go | 8 +- .../api/app_session_v1/get_last_key_states.go | 6 +- clearnode/api/app_session_v1/handler.go | 12 +- clearnode/api/app_session_v1/interface.go | 6 +- .../app_session_v1/rebalance_app_sessions.go | 8 +- .../rebalance_app_sessions_test.go | 8 +- .../api/app_session_v1/submit_app_state.go | 8 +- .../app_session_v1/submit_app_state_test.go | 8 +- .../app_session_v1/submit_deposit_state.go | 8 +- .../submit_deposit_state_test.go | 8 +- .../submit_session_key_state.go | 8 +- clearnode/api/app_session_v1/testing.go | 8 +- clearnode/api/app_session_v1/utils.go | 6 +- clearnode/api/apps_v1/get_apps.go | 6 +- clearnode/api/apps_v1/get_apps_test.go | 6 +- clearnode/api/apps_v1/interface.go | 6 +- clearnode/api/apps_v1/submit_app_version.go | 6 +- .../api/apps_v1/submit_app_version_test.go | 6 +- clearnode/api/apps_v1/testing.go | 6 +- clearnode/api/channel_v1/get_channels.go | 4 +- clearnode/api/channel_v1/get_channels_test.go | 6 +- .../api/channel_v1/get_escrow_channel.go | 4 +- .../api/channel_v1/get_escrow_channel_test.go | 6 +- clearnode/api/channel_v1/get_home_channel.go | 4 +- .../api/channel_v1/get_home_channel_test.go | 6 +- .../api/channel_v1/get_last_key_states.go | 6 +- clearnode/api/channel_v1/get_latest_state.go | 4 +- .../api/channel_v1/get_latest_state_test.go | 6 +- clearnode/api/channel_v1/handler.go | 8 +- clearnode/api/channel_v1/interface.go | 4 +- clearnode/api/channel_v1/request_creation.go | 6 +- .../api/channel_v1/request_creation_test.go | 6 +- .../channel_v1/submit_session_key_state.go | 6 +- clearnode/api/channel_v1/submit_state.go | 6 +- clearnode/api/channel_v1/submit_state_test.go | 6 +- clearnode/api/channel_v1/testing.go | 6 +- clearnode/api/channel_v1/utils.go | 4 +- clearnode/api/metric_store.go | 8 +- clearnode/api/node_v1/get_assets.go | 2 +- clearnode/api/node_v1/get_assets_test.go | 4 +- clearnode/api/node_v1/get_config.go | 4 +- clearnode/api/node_v1/get_config_test.go | 4 +- clearnode/api/node_v1/interface.go | 2 +- clearnode/api/node_v1/ping.go | 2 +- clearnode/api/node_v1/testing.go | 2 +- clearnode/api/node_v1/utils.go | 4 +- clearnode/api/rate_limits.go | 2 +- clearnode/api/rate_limits_test.go | 2 +- clearnode/api/rpc_router.go | 26 +- .../api/user_v1/get_action_allowances.go | 4 +- .../api/user_v1/get_action_allowances_test.go | 4 +- clearnode/api/user_v1/get_balances.go | 2 +- clearnode/api/user_v1/get_balances_test.go | 4 +- clearnode/api/user_v1/get_transactions.go | 4 +- .../api/user_v1/get_transactions_test.go | 4 +- clearnode/api/user_v1/interface.go | 4 +- clearnode/api/user_v1/testing.go | 4 +- clearnode/api/user_v1/utils.go | 4 +- clearnode/api/utils.go | 2 +- clearnode/blockchain_worker.go | 6 +- clearnode/chart/README.md | 6 +- clearnode/chart/README.md.gotmpl | 4 +- clearnode/event_handlers/interface.go | 2 +- clearnode/event_handlers/service.go | 4 +- clearnode/event_handlers/service_test.go | 4 +- clearnode/event_handlers/testing.go | 2 +- clearnode/main.go | 14 +- clearnode/metrics/exporter.go | 6 +- clearnode/metrics/interface.go | 6 +- clearnode/runtime.go | 20 +- clearnode/runtime_test.go | 2 +- clearnode/store/database/action_log.go | 2 +- clearnode/store/database/action_log_test.go | 2 +- clearnode/store/database/app.go | 4 +- clearnode/store/database/app_ledger_test.go | 2 +- clearnode/store/database/app_session.go | 4 +- .../store/database/app_session_key_state.go | 2 +- .../database/app_session_key_state_test.go | 2 +- clearnode/store/database/app_session_test.go | 4 +- clearnode/store/database/app_test.go | 4 +- .../store/database/blockchain_action_test.go | 2 +- clearnode/store/database/channel.go | 2 +- .../database/channel_session_key_state.go | 2 +- .../channel_session_key_state_test.go | 2 +- clearnode/store/database/channel_test.go | 2 +- clearnode/store/database/contract_event.go | 2 +- .../store/database/contract_event_test.go | 2 +- clearnode/store/database/db_store.go | 2 +- clearnode/store/database/db_store_test.go | 2 +- clearnode/store/database/interface.go | 4 +- clearnode/store/database/state.go | 2 +- clearnode/store/database/state_test.go | 2 +- .../test/postgres_integration_test.go | 6 +- clearnode/store/database/transaction.go | 2 +- clearnode/store/database/transaction_test.go | 2 +- clearnode/store/database/utils.go | 4 +- clearnode/store/memory/interface.go | 2 +- clearnode/store/memory/memory_store.go | 2 +- clearnode/stress/app_session.go | 4 +- clearnode/stress/config.go | 2 +- clearnode/stress/methods.go | 4 +- clearnode/stress/pool.go | 6 +- clearnode/stress/runner.go | 2 +- clearnode/stress/transfer.go | 6 +- clearnode/stress/types.go | 2 +- erc7824-docs/.firebaserc | 5 - erc7824-docs/.gitignore | 23 - erc7824-docs/README.md | 42 - erc7824-docs/blog/authors.yml | 23 - erc7824-docs/blog/tags.yml | 19 - erc7824-docs/docs/_legacy/examples/index.md | 13 - .../docs/_legacy/examples/tictactoe.md | 167 - erc7824-docs/docs/_legacy/faq.md | 128 - erc7824-docs/docs/_legacy/index.md | 69 - erc7824-docs/docs/_legacy/protocol.md | 430 - erc7824-docs/docs/_legacy/resources.md | 32 - erc7824-docs/docs/_legacy/spec.md | 355 - erc7824-docs/docs/erc-7824.md | 498 - erc7824-docs/docs/guides/index.md | 42 - erc7824-docs/docs/guides/migration-guide.md | 930 - erc7824-docs/docs/index.md | 62 - .../advanced/abstract-accounts.md | 427 - .../advanced/erc20-service.md | 218 - .../docs/nitrolite_client/advanced/index.md | 150 - .../advanced/nitrolite-service.md | 202 - .../advanced/supported-sig-formats.md | 86 - erc7824-docs/docs/nitrolite_client/index.mdx | 122 - .../docs/nitrolite_client/methods.mdx | 236 - erc7824-docs/docs/nitrolite_client/types.md | 371 - erc7824-docs/docs/nitrolite_rpc/index.md | 177 - .../nitrolite_rpc/message_creation_api.md | 403 - erc7824-docs/docs/nitrolite_rpc/rpc_types.md | 296 - .../docs/quick_start/application_session.md | 1071 - erc7824-docs/docs/quick_start/balances.md | 396 - .../docs/quick_start/close_session.md | 1197 - .../quick_start/connect_to_the_clearnode.md | 1791 -- erc7824-docs/docs/quick_start/index.mdx | 86 - .../docs/quick_start/initializing_channel.md | 39 - erc7824-docs/docusaurus.config.js | 276 - erc7824-docs/firebase.json | 10 - erc7824-docs/package-lock.json | 19685 ---------------- erc7824-docs/package.json | 45 - erc7824-docs/sidebars.js | 35 - erc7824-docs/src/components/Card/index.js | 31 - .../src/components/Card/styles.module.css | 79 - .../src/components/HomepageFeatures/index.js | 64 - .../HomepageFeatures/styles.module.css | 11 - erc7824-docs/src/components/MethodCard.tsx | 81 - erc7824-docs/src/components/MethodDetails.tsx | 148 - erc7824-docs/src/css/custom.css | 213 - erc7824-docs/src/pages/index.module.css | 23 - erc7824-docs/src/pages/markdown-page.md | 7 - erc7824-docs/static/.nojekyll | 0 erc7824-docs/static/img/erc7824.png | Bin 3592 -> 0 bytes .../static/img/erc7824_social_card.png | Bin 7762 -> 0 bytes erc7824-docs/static/img/favicon.ico | Bin 7991 -> 0 bytes erc7824-docs/static/img/logo.svg | 9 - erc7824-docs/static/img/logo_dark.svg | 9 - .../static/img/undraw_docusaurus_mountain.svg | 171 - .../static/img/undraw_docusaurus_react.svg | 170 - .../static/img/undraw_docusaurus_tree.svg | 40 - go.mod | 2 +- pkg/app/app_session_v1.go | 2 +- pkg/app/session_key_v1.go | 2 +- pkg/app/session_key_v1_test.go | 2 +- pkg/blockchain/evm/client.go | 4 +- pkg/blockchain/evm/client_test.go | 2 +- pkg/blockchain/evm/interface.go | 2 +- pkg/blockchain/evm/listener.go | 4 +- pkg/blockchain/evm/listener_test.go | 4 +- pkg/blockchain/evm/reactor.go | 4 +- pkg/blockchain/evm/utils.go | 6 +- pkg/blockchain/evm/utils_test.go | 2 +- pkg/core/README.md | 2 +- pkg/core/channel_signer.go | 2 +- pkg/core/session_key.go | 2 +- pkg/core/session_key_test.go | 2 +- pkg/log/context_test.go | 2 +- pkg/log/mock_logger_test.go | 2 +- pkg/log/span_logger_test.go | 2 +- pkg/log/zap_logger_test.go | 2 +- pkg/rpc/README.md | 10 +- pkg/rpc/api.go | 2 +- pkg/rpc/client_test.go | 4 +- pkg/rpc/connection.go | 2 +- pkg/rpc/connection_test.go | 2 +- pkg/rpc/dialer.go | 2 +- pkg/rpc/dialer_test.go | 2 +- pkg/rpc/doc.go | 2 +- pkg/rpc/mock_dialer_test.go | 2 +- pkg/rpc/node.go | 2 +- pkg/rpc/payload_test.go | 2 +- pkg/rpc/types.go | 4 +- pkg/sign/README.md | 2 +- pkg/sign/example_test.go | 2 +- pkg/sign/kms/gcp/gcp_test.go | 2 +- pkg/sign/kms/gcp/signer.go | 4 +- pkg/sign/kms/gcp/signer_test.go | 2 +- sdk/go/README.md | 8 +- sdk/go/app_registry.go | 8 +- sdk/go/app_session.go | 6 +- sdk/go/asset_cache.go | 2 +- sdk/go/asset_cache_test.go | 2 +- sdk/go/channel.go | 6 +- sdk/go/client.go | 8 +- sdk/go/client_test.go | 8 +- sdk/go/doc.go | 4 +- sdk/go/examples/app_sessions/lifecycle.go | 8 +- sdk/go/examples/challenge/main.go | 6 +- sdk/go/mock_dialer_test.go | 2 +- sdk/go/node.go | 4 +- sdk/go/user.go | 4 +- sdk/go/utils.go | 6 +- sdk/go/utils_test.go | 6 +- sdk/ts-compat/README.md | 2 +- sdk/ts-compat/docs/migration-offchain.md | 2 +- sdk/ts-compat/docs/migration-overview.md | 10 +- sdk/ts-compat/examples/lifecycle.ts | 4 +- sdk/ts-compat/examples/tsconfig.json | 4 +- sdk/ts-compat/package.json | 4 +- sdk/ts-compat/src/index.ts | 8 +- sdk/ts-compat/src/types.ts | 1 - sdk/ts/README.md | 8 +- sdk/ts/docs/TYPECHAIN_AUTOMATION.md | 2 +- .../examples/app_sessions/package-lock.json | 2 +- sdk/ts/examples/example-app/README.md | 17 +- sdk/ts/examples/example-app/src/App.tsx | 2 +- sdk/ts/package.json | 3 +- sdk/ts/scripts/generate-docs.ts | 6 +- sdk/ts/scripts/generate-example-tutorials.ts | 5 +- test/integration/README.md | 2 +- test/integration/common/auth.ts | 2 +- test/integration/common/blockchainUtils.ts | 2 +- test/integration/common/databaseUtils.ts | 2 +- test/integration/common/identity.ts | 4 +- test/integration/common/nitroliteClient.ts | 2 +- .../common/testAppSessionHelpers.ts | 2 +- test/integration/common/testHelpers.ts | 2 +- test/integration/common/testSetup.ts | 2 +- test/integration/common/ws.ts | 8 +- test/integration/package-lock.json | 6 +- test/integration/package.json | 2 +- .../lifecycle_nitrorpc_v02.test.ts | 383 - .../nitrorpc_v02.test.ts | 121 - .../onchain_ops_with_sk.test.ts | 460 - .../tests/challenge_channel.test.ts | 2 +- test/integration/tests/clearnode_auth.test.ts | 2 +- .../tests/clearnode_connect.test.ts | 32 - test/integration/tests/close_channel.test.ts | 2 +- test/integration/tests/create_channel.test.ts | 2 +- test/integration/tests/get_user_tag.test.ts | 78 - .../tests/ledger_transactions.test.ts | 2 +- .../tests/lifecycle_nitrorpc_v04.test.ts | 2 +- test/integration/tests/nitrorpc_v04.test.ts | 2 +- test/integration/tests/resize_channel.test.ts | 432 - test/integration/tests/session_key.test.ts | 2 +- test/integration/tests/transfer.test.ts | 2 +- test/integration/tsconfig.json | 2 +- 278 files changed, 449 insertions(+), 33228 deletions(-) delete mode 100644 erc7824-docs/.firebaserc delete mode 100644 erc7824-docs/.gitignore delete mode 100644 erc7824-docs/README.md delete mode 100644 erc7824-docs/blog/authors.yml delete mode 100644 erc7824-docs/blog/tags.yml delete mode 100644 erc7824-docs/docs/_legacy/examples/index.md delete mode 100644 erc7824-docs/docs/_legacy/examples/tictactoe.md delete mode 100644 erc7824-docs/docs/_legacy/faq.md delete mode 100644 erc7824-docs/docs/_legacy/index.md delete mode 100644 erc7824-docs/docs/_legacy/protocol.md delete mode 100644 erc7824-docs/docs/_legacy/resources.md delete mode 100644 erc7824-docs/docs/_legacy/spec.md delete mode 100644 erc7824-docs/docs/erc-7824.md delete mode 100644 erc7824-docs/docs/guides/index.md delete mode 100644 erc7824-docs/docs/guides/migration-guide.md delete mode 100644 erc7824-docs/docs/index.md delete mode 100644 erc7824-docs/docs/nitrolite_client/advanced/abstract-accounts.md delete mode 100644 erc7824-docs/docs/nitrolite_client/advanced/erc20-service.md delete mode 100644 erc7824-docs/docs/nitrolite_client/advanced/index.md delete mode 100644 erc7824-docs/docs/nitrolite_client/advanced/nitrolite-service.md delete mode 100644 erc7824-docs/docs/nitrolite_client/advanced/supported-sig-formats.md delete mode 100644 erc7824-docs/docs/nitrolite_client/index.mdx delete mode 100644 erc7824-docs/docs/nitrolite_client/methods.mdx delete mode 100644 erc7824-docs/docs/nitrolite_client/types.md delete mode 100644 erc7824-docs/docs/nitrolite_rpc/index.md delete mode 100644 erc7824-docs/docs/nitrolite_rpc/message_creation_api.md delete mode 100644 erc7824-docs/docs/nitrolite_rpc/rpc_types.md delete mode 100644 erc7824-docs/docs/quick_start/application_session.md delete mode 100644 erc7824-docs/docs/quick_start/balances.md delete mode 100644 erc7824-docs/docs/quick_start/close_session.md delete mode 100644 erc7824-docs/docs/quick_start/connect_to_the_clearnode.md delete mode 100644 erc7824-docs/docs/quick_start/index.mdx delete mode 100644 erc7824-docs/docs/quick_start/initializing_channel.md delete mode 100644 erc7824-docs/docusaurus.config.js delete mode 100644 erc7824-docs/firebase.json delete mode 100644 erc7824-docs/package-lock.json delete mode 100644 erc7824-docs/package.json delete mode 100644 erc7824-docs/sidebars.js delete mode 100644 erc7824-docs/src/components/Card/index.js delete mode 100644 erc7824-docs/src/components/Card/styles.module.css delete mode 100644 erc7824-docs/src/components/HomepageFeatures/index.js delete mode 100644 erc7824-docs/src/components/HomepageFeatures/styles.module.css delete mode 100644 erc7824-docs/src/components/MethodCard.tsx delete mode 100644 erc7824-docs/src/components/MethodDetails.tsx delete mode 100644 erc7824-docs/src/css/custom.css delete mode 100644 erc7824-docs/src/pages/index.module.css delete mode 100644 erc7824-docs/src/pages/markdown-page.md delete mode 100644 erc7824-docs/static/.nojekyll delete mode 100644 erc7824-docs/static/img/erc7824.png delete mode 100644 erc7824-docs/static/img/erc7824_social_card.png delete mode 100644 erc7824-docs/static/img/favicon.ico delete mode 100644 erc7824-docs/static/img/logo.svg delete mode 100644 erc7824-docs/static/img/logo_dark.svg delete mode 100644 erc7824-docs/static/img/undraw_docusaurus_mountain.svg delete mode 100644 erc7824-docs/static/img/undraw_docusaurus_react.svg delete mode 100644 erc7824-docs/static/img/undraw_docusaurus_tree.svg delete mode 100644 test/integration/tests/backward_compatibility/lifecycle_nitrorpc_v02.test.ts delete mode 100644 test/integration/tests/backward_compatibility/nitrorpc_v02.test.ts delete mode 100644 test/integration/tests/backward_compatibility/onchain_ops_with_sk.test.ts delete mode 100644 test/integration/tests/clearnode_connect.test.ts delete mode 100644 test/integration/tests/get_user_tag.test.ts delete mode 100644 test/integration/tests/resize_channel.test.ts diff --git a/.dockerignore b/.dockerignore index d3be7426b..72c49dfbb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,7 +7,6 @@ docker-compose.yml LICENSE contracts/ docs/ -erc7824-docs/ sdk/ts/ test/ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4e19d7391..365492e92 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,8 @@ # CODEOWNERS: https://help.github.com/articles/about-codeowners/ # Yellow Network - Research and Development -* @erc7824/yellow-network +* @alessio +* @dimast-x +* @nksazonov +* @philanton +* @ihsraham diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f3edff838..65d4f6ba0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,7 +8,6 @@ updates: - package-ecosystem: "npm" # See documentation for possible values directories: - "/sdk/ts" - - "/erc7824-docs" - "/sdk/compat" - "/test/integration" - "/sdk/ts/examples/*app*" diff --git a/.github/workflows/main-pr.yml b/.github/workflows/main-pr.yml index 6a7afc431..96d4b3ab3 100644 --- a/.github/workflows/main-pr.yml +++ b/.github/workflows/main-pr.yml @@ -57,22 +57,3 @@ jobs: build-args: | VERSION=${{ steps.sha.outputs.short_sha }} - # TODO: Enable this job if docs preview are needed (do not forget to provide GITHUB_TOKEN and GH access to receive preview URLs). - # https://stackoverflow.com/questions/75514653/firebase-action-hosting-deploy-fails-with-requesterror-resource-not-accessible - - # build-and-preview-docs-firebase: - # name: Deploy to Firebase Hosting on PR - # if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - - # - run: npm install && npm run build - # working-directory: erc7824-docs - - # - uses: FirebaseExtended/action-hosting-deploy@v0 - # with: - # # repoToken: ${{ secrets.GITHUB_TOKEN }} - # firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_ERC7824 }} - # projectId: erc7824 - # entryPoint: ./erc7824-docs diff --git a/.github/workflows/main-push.yml b/.github/workflows/main-push.yml index 370ce4fd9..901393272 100644 --- a/.github/workflows/main-push.yml +++ b/.github/workflows/main-push.yml @@ -117,19 +117,3 @@ jobs: # ${{github.event.head_commit.message}} # SLACK_FOOTER: 'Nitrolite CI/CD Pipeline' - # build-and-deploy-docs-firebase: - # name: Deploy to Firebase Hosting on merge - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v6 - - # - run: npm install && npm run build - # working-directory: erc7824-docs - - # - uses: FirebaseExtended/action-hosting-deploy@v0 - # with: - # # repoToken: ${{ secrets.GITHUB_TOKEN }} - # firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_ERC7824 }} - # channelId: live - # projectId: erc7824 - # entryPoint: ./erc7824-docs diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml index a302e7266..55ce9da2a 100644 --- a/.github/workflows/test-go.yml +++ b/.github/workflows/test-go.yml @@ -36,4 +36,4 @@ jobs: - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} - slug: erc7824/nitrolite + slug: layer-3/nitrolite diff --git a/.github/workflows/v1-push.yml b/.github/workflows/v1-push.yml index 532debf23..93af694f7 100644 --- a/.github/workflows/v1-push.yml +++ b/.github/workflows/v1-push.yml @@ -160,20 +160,3 @@ jobs: # ⚠️ RC build or deployment was cancelled! # ${{github.event.head_commit.message}} # SLACK_FOOTER: 'Nitrolite CI/CD Pipeline' - - # build-and-deploy-docs-firebase: - # name: Deploy to Firebase Hosting on merge - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - - # - run: npm install && npm run build - # working-directory: erc7824-docs - - # - uses: FirebaseExtended/action-hosting-deploy@v0 - # with: - # # repoToken: ${{ secrets.GITHUB_TOKEN }} - # firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_ERC7824 }} - # channelId: live - # projectId: erc7824 - # entryPoint: ./erc7824-docs diff --git a/LICENSE b/LICENSE index 36f6d597a..20b1a81cf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025, 2026 erc7824 +Copyright (c) 2025, 2026 layer-3 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 62256e191..627a21c2b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Release Policy](https://img.shields.io/badge/release%20policy-v1.0-blue)](https://github.com/layer-3/release-process/blob/master/README.md) -[![codecov](https://codecov.io/github/erc7824/nitrolite/graph/badge.svg?token=XASM4CIEFO)](https://codecov.io/github/erc7824/nitrolite) -[![Go Reference](https://pkg.go.dev/badge/github.com/erc7824/nitrolite.svg)](https://pkg.go.dev/github.com/erc7824/nitrolite) -[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/erc7824/nitrolite)](https://github.com/erc7824/nitrolite/releases) +[![codecov](https://codecov.io/github/layer-3/nitrolite/graph/badge.svg?token=XASM4CIEFO)](https://codecov.io/github/layer-3/nitrolite) +[![Go Reference](https://pkg.go.dev/badge/github.com/layer-3/nitrolite.svg)](https://pkg.go.dev/github.com/layer-3/nitrolite) +[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/layer-3/nitrolite)](https://github.com/layer-3/nitrolite/releases) # Nitrolite: State Channel Framework @@ -87,7 +87,7 @@ See the [Clearnode Documentation](/clearnode/README.md) for more details. The official Go SDK for building performant backend integrations or CLI tools. ```bash -go get github.com/erc7824/nitrolite/sdk/go +go get github.com/layer-3/nitrolite/sdk/go ``` See [SDK Go README](/sdk/go/README.md). @@ -96,7 +96,7 @@ See [SDK Go README](/sdk/go/README.md). The official TypeScript SDK for web-based applications. ```bash -npm install @erc7824/nitrolite +npm install @layer-3/nitrolite ``` See [SDK TS README](/sdk/ts/README.md). diff --git a/cerebro/README.md b/cerebro/README.md index bb330feed..e1f2622ac 100644 --- a/cerebro/README.md +++ b/cerebro/README.md @@ -12,7 +12,7 @@ go build -o clearnode-cli Or install directly: ```bash -go install github.com/erc7824/nitrolite/sdk/go/examples/cli@latest +go install github.com/layer-3/nitrolite/sdk/go/examples/cli@latest ``` ## Quick Start diff --git a/cerebro/commands.go b/cerebro/commands.go index 78ce6fa6e..695620df8 100644 --- a/cerebro/commands.go +++ b/cerebro/commands.go @@ -9,12 +9,12 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/sign" - sdk "github.com/erc7824/nitrolite/sdk/go" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/sign" + sdk "github.com/layer-3/nitrolite/sdk/go" ) // ============================================================================ diff --git a/cerebro/operator.go b/cerebro/operator.go index bb7a3f7e5..68ddb2761 100644 --- a/cerebro/operator.go +++ b/cerebro/operator.go @@ -8,9 +8,9 @@ import ( "time" "github.com/c-bata/go-prompt" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/sign" - sdk "github.com/erc7824/nitrolite/sdk/go" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/sign" + sdk "github.com/layer-3/nitrolite/sdk/go" "github.com/shopspring/decimal" ) diff --git a/cerebro/operator_test.go b/cerebro/operator_test.go index d11decb39..69f8d1275 100644 --- a/cerebro/operator_test.go +++ b/cerebro/operator_test.go @@ -5,8 +5,8 @@ import ( "strings" "testing" - "github.com/erc7824/nitrolite/pkg/core" "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/nitrolite/pkg/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/clearnode/action_gateway/action_gateway.go b/clearnode/action_gateway/action_gateway.go index a0b19e280..1eecbf519 100644 --- a/clearnode/action_gateway/action_gateway.go +++ b/clearnode/action_gateway/action_gateway.go @@ -8,7 +8,7 @@ import ( "slices" "time" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" "go.yaml.in/yaml/v2" ) diff --git a/clearnode/action_gateway/action_gateway_test.go b/clearnode/action_gateway/action_gateway_test.go index d9486d094..dad63ad5a 100644 --- a/clearnode/action_gateway/action_gateway_test.go +++ b/clearnode/action_gateway/action_gateway_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/clearnode/api/app_session_v1/create_app_session.go b/clearnode/api/app_session_v1/create_app_session.go index 3fbe49e77..1db8d74dd 100644 --- a/clearnode/api/app_session_v1/create_app_session.go +++ b/clearnode/api/app_session_v1/create_app_session.go @@ -4,11 +4,11 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" - "github.com/erc7824/nitrolite/pkg/rpc" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/rpc" ) // CreateAppSession creates a new application session between participants. diff --git a/clearnode/api/app_session_v1/create_app_session_test.go b/clearnode/api/app_session_v1/create_app_session_test.go index 4f9cc436a..ccbab0721 100644 --- a/clearnode/api/app_session_v1/create_app_session_test.go +++ b/clearnode/api/app_session_v1/create_app_session_test.go @@ -8,10 +8,10 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func TestCreateAppSession_Success(t *testing.T) { diff --git a/clearnode/api/app_session_v1/get_app_definition.go b/clearnode/api/app_session_v1/get_app_definition.go index 32c4f0f10..849b21a05 100644 --- a/clearnode/api/app_session_v1/get_app_definition.go +++ b/clearnode/api/app_session_v1/get_app_definition.go @@ -3,7 +3,7 @@ package app_session_v1 import ( "strconv" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/rpc" ) // GetAppDefinition retrieves the application definition for a specific app session. diff --git a/clearnode/api/app_session_v1/get_app_definition_test.go b/clearnode/api/app_session_v1/get_app_definition_test.go index 6ca17b7d7..8f3fb7231 100644 --- a/clearnode/api/app_session_v1/get_app_definition_test.go +++ b/clearnode/api/app_session_v1/get_app_definition_test.go @@ -8,10 +8,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func TestGetAppDefinition_Success(t *testing.T) { diff --git a/clearnode/api/app_session_v1/get_app_sessions.go b/clearnode/api/app_session_v1/get_app_sessions.go index bdb953128..2c5d1c196 100644 --- a/clearnode/api/app_session_v1/get_app_sessions.go +++ b/clearnode/api/app_session_v1/get_app_sessions.go @@ -3,9 +3,9 @@ package app_session_v1 import ( "github.com/shopspring/decimal" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) // GetAppSessions retrieves application sessions with optional filtering. diff --git a/clearnode/api/app_session_v1/get_app_sessions_test.go b/clearnode/api/app_session_v1/get_app_sessions_test.go index ddf1428dd..1367c81b8 100644 --- a/clearnode/api/app_session_v1/get_app_sessions_test.go +++ b/clearnode/api/app_session_v1/get_app_sessions_test.go @@ -10,10 +10,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func TestGetAppSessions_SuccessWithParticipant(t *testing.T) { diff --git a/clearnode/api/app_session_v1/get_last_key_states.go b/clearnode/api/app_session_v1/get_last_key_states.go index e741b9deb..83976a1c5 100644 --- a/clearnode/api/app_session_v1/get_last_key_states.go +++ b/clearnode/api/app_session_v1/get_last_key_states.go @@ -1,9 +1,9 @@ package app_session_v1 import ( - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/log" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/rpc" ) // GetLastKeyStates retrieves the latest session key states for a user with optional filtering by session key. diff --git a/clearnode/api/app_session_v1/handler.go b/clearnode/api/app_session_v1/handler.go index 672273367..bfac5079b 100644 --- a/clearnode/api/app_session_v1/handler.go +++ b/clearnode/api/app_session_v1/handler.go @@ -7,12 +7,12 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/shopspring/decimal" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" - "github.com/erc7824/nitrolite/pkg/rpc" - "github.com/erc7824/nitrolite/pkg/sign" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/sign" ) // Handler manages app session operations and provides RPC endpoints for app session management. diff --git a/clearnode/api/app_session_v1/interface.go b/clearnode/api/app_session_v1/interface.go index 611695cf3..5c13464b9 100644 --- a/clearnode/api/app_session_v1/interface.go +++ b/clearnode/api/app_session_v1/interface.go @@ -1,9 +1,9 @@ package app_session_v1 import ( - "github.com/erc7824/nitrolite/clearnode/action_gateway" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/clearnode/action_gateway" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" ) diff --git a/clearnode/api/app_session_v1/rebalance_app_sessions.go b/clearnode/api/app_session_v1/rebalance_app_sessions.go index e151a0dcb..f066c6cf1 100644 --- a/clearnode/api/app_session_v1/rebalance_app_sessions.go +++ b/clearnode/api/app_session_v1/rebalance_app_sessions.go @@ -6,10 +6,10 @@ import ( "github.com/shopspring/decimal" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/rpc" ) // RebalanceAppSessions processes multi-session rebalancing operations atomically. diff --git a/clearnode/api/app_session_v1/rebalance_app_sessions_test.go b/clearnode/api/app_session_v1/rebalance_app_sessions_test.go index 073792a7e..aef9cb2e9 100644 --- a/clearnode/api/app_session_v1/rebalance_app_sessions_test.go +++ b/clearnode/api/app_session_v1/rebalance_app_sessions_test.go @@ -9,10 +9,10 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) // assertSuccess checks if the RPC context has a successful response diff --git a/clearnode/api/app_session_v1/submit_app_state.go b/clearnode/api/app_session_v1/submit_app_state.go index d62bfbc00..e5c1290c5 100644 --- a/clearnode/api/app_session_v1/submit_app_state.go +++ b/clearnode/api/app_session_v1/submit_app_state.go @@ -4,10 +4,10 @@ import ( "context" "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/rpc" "github.com/shopspring/decimal" ) diff --git a/clearnode/api/app_session_v1/submit_app_state_test.go b/clearnode/api/app_session_v1/submit_app_state_test.go index a0bb83866..3638e600b 100644 --- a/clearnode/api/app_session_v1/submit_app_state_test.go +++ b/clearnode/api/app_session_v1/submit_app_state_test.go @@ -5,10 +5,10 @@ import ( "errors" "testing" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" diff --git a/clearnode/api/app_session_v1/submit_deposit_state.go b/clearnode/api/app_session_v1/submit_deposit_state.go index 0fdc96a02..c273c608e 100644 --- a/clearnode/api/app_session_v1/submit_deposit_state.go +++ b/clearnode/api/app_session_v1/submit_deposit_state.go @@ -3,11 +3,11 @@ package app_session_v1 import ( "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" - "github.com/erc7824/nitrolite/pkg/rpc" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/rpc" "github.com/shopspring/decimal" ) diff --git a/clearnode/api/app_session_v1/submit_deposit_state_test.go b/clearnode/api/app_session_v1/submit_deposit_state_test.go index 0612ec9a0..539667ae9 100644 --- a/clearnode/api/app_session_v1/submit_deposit_state_test.go +++ b/clearnode/api/app_session_v1/submit_deposit_state_test.go @@ -13,10 +13,10 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func TestSubmitDepositState_Success(t *testing.T) { diff --git a/clearnode/api/app_session_v1/submit_session_key_state.go b/clearnode/api/app_session_v1/submit_session_key_state.go index 18dd1c313..c92970a93 100644 --- a/clearnode/api/app_session_v1/submit_session_key_state.go +++ b/clearnode/api/app_session_v1/submit_session_key_state.go @@ -7,10 +7,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/log" - "github.com/erc7824/nitrolite/pkg/rpc" - "github.com/erc7824/nitrolite/pkg/sign" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/sign" ) // SubmitSessionKeyState processes session key state submissions for registration and updates. diff --git a/clearnode/api/app_session_v1/testing.go b/clearnode/api/app_session_v1/testing.go index 0756b2505..a1a220f3f 100644 --- a/clearnode/api/app_session_v1/testing.go +++ b/clearnode/api/app_session_v1/testing.go @@ -11,10 +11,10 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/clearnode/action_gateway" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/sign" + "github.com/layer-3/nitrolite/clearnode/action_gateway" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/sign" ) // MockStore is a mock implementation of the Store interface diff --git a/clearnode/api/app_session_v1/utils.go b/clearnode/api/app_session_v1/utils.go index 400e3703e..4381b2d62 100644 --- a/clearnode/api/app_session_v1/utils.go +++ b/clearnode/api/app_session_v1/utils.go @@ -7,9 +7,9 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" "github.com/shopspring/decimal" ) diff --git a/clearnode/api/apps_v1/get_apps.go b/clearnode/api/apps_v1/get_apps.go index 95d36e631..3ab03b4e5 100644 --- a/clearnode/api/apps_v1/get_apps.go +++ b/clearnode/api/apps_v1/get_apps.go @@ -3,9 +3,9 @@ package apps_v1 import ( "strconv" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) // GetApps retrieves registered applications with optional filtering. diff --git a/clearnode/api/apps_v1/get_apps_test.go b/clearnode/api/apps_v1/get_apps_test.go index 206202c90..aaeb8ed52 100644 --- a/clearnode/api/apps_v1/get_apps_test.go +++ b/clearnode/api/apps_v1/get_apps_test.go @@ -4,9 +4,9 @@ import ( "context" "testing" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/clearnode/api/apps_v1/interface.go b/clearnode/api/apps_v1/interface.go index 4a5fa21a5..06c9d94c2 100644 --- a/clearnode/api/apps_v1/interface.go +++ b/clearnode/api/apps_v1/interface.go @@ -1,9 +1,9 @@ package apps_v1 import ( - "github.com/erc7824/nitrolite/clearnode/action_gateway" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/clearnode/action_gateway" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" ) // StoreTxHandler is a function that executes Store operations within a transaction. diff --git a/clearnode/api/apps_v1/submit_app_version.go b/clearnode/api/apps_v1/submit_app_version.go index c96df82b7..c604e76fe 100644 --- a/clearnode/api/apps_v1/submit_app_version.go +++ b/clearnode/api/apps_v1/submit_app_version.go @@ -6,9 +6,9 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/rpc" - "github.com/erc7824/nitrolite/pkg/sign" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/sign" ) // SubmitAppVersion updates an entry in the app registry. diff --git a/clearnode/api/apps_v1/submit_app_version_test.go b/clearnode/api/apps_v1/submit_app_version_test.go index 840bca5ca..5486fec86 100644 --- a/clearnode/api/apps_v1/submit_app_version_test.go +++ b/clearnode/api/apps_v1/submit_app_version_test.go @@ -5,11 +5,11 @@ import ( "strings" "testing" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/rpc" - "github.com/erc7824/nitrolite/pkg/sign" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/sign" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/clearnode/api/apps_v1/testing.go b/clearnode/api/apps_v1/testing.go index ed77642ff..e3cf9a4cf 100644 --- a/clearnode/api/apps_v1/testing.go +++ b/clearnode/api/apps_v1/testing.go @@ -3,9 +3,9 @@ package apps_v1 import ( "time" - "github.com/erc7824/nitrolite/clearnode/action_gateway" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/clearnode/action_gateway" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" ) diff --git a/clearnode/api/channel_v1/get_channels.go b/clearnode/api/channel_v1/get_channels.go index 9c751de07..f51d11043 100644 --- a/clearnode/api/channel_v1/get_channels.go +++ b/clearnode/api/channel_v1/get_channels.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func channelStatusFromString(s string) (core.ChannelStatus, error) { diff --git a/clearnode/api/channel_v1/get_channels_test.go b/clearnode/api/channel_v1/get_channels_test.go index e10cf449f..05dc01c8d 100644 --- a/clearnode/api/channel_v1/get_channels_test.go +++ b/clearnode/api/channel_v1/get_channels_test.go @@ -8,9 +8,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func newGetChannelsHandler(mockTxStore *MockStore) *Handler { diff --git a/clearnode/api/channel_v1/get_escrow_channel.go b/clearnode/api/channel_v1/get_escrow_channel.go index 1866508ca..351e566c4 100644 --- a/clearnode/api/channel_v1/get_escrow_channel.go +++ b/clearnode/api/channel_v1/get_escrow_channel.go @@ -1,8 +1,8 @@ package channel_v1 import ( - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) // GetEscrowChannel retrieves current on-chain escrow channel information. diff --git a/clearnode/api/channel_v1/get_escrow_channel_test.go b/clearnode/api/channel_v1/get_escrow_channel_test.go index 087cff6cc..16ae76e61 100644 --- a/clearnode/api/channel_v1/get_escrow_channel_test.go +++ b/clearnode/api/channel_v1/get_escrow_channel_test.go @@ -7,9 +7,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func TestGetEscrowChannel_Success(t *testing.T) { diff --git a/clearnode/api/channel_v1/get_home_channel.go b/clearnode/api/channel_v1/get_home_channel.go index 392e44d2e..18b4d6935 100644 --- a/clearnode/api/channel_v1/get_home_channel.go +++ b/clearnode/api/channel_v1/get_home_channel.go @@ -1,8 +1,8 @@ package channel_v1 import ( - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) // GetHomeChannel retrieves current on-chain home channel information. diff --git a/clearnode/api/channel_v1/get_home_channel_test.go b/clearnode/api/channel_v1/get_home_channel_test.go index cd5a31f9c..aec4fb3dd 100644 --- a/clearnode/api/channel_v1/get_home_channel_test.go +++ b/clearnode/api/channel_v1/get_home_channel_test.go @@ -7,9 +7,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func TestGetHomeChannel_Success(t *testing.T) { diff --git a/clearnode/api/channel_v1/get_last_key_states.go b/clearnode/api/channel_v1/get_last_key_states.go index 37836d759..001d96008 100644 --- a/clearnode/api/channel_v1/get_last_key_states.go +++ b/clearnode/api/channel_v1/get_last_key_states.go @@ -1,9 +1,9 @@ package channel_v1 import ( - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/rpc" ) // GetLastKeyStates retrieves the latest channel session key states for a user with optional filtering by session key. diff --git a/clearnode/api/channel_v1/get_latest_state.go b/clearnode/api/channel_v1/get_latest_state.go index 0d9b2a49b..80000a38c 100644 --- a/clearnode/api/channel_v1/get_latest_state.go +++ b/clearnode/api/channel_v1/get_latest_state.go @@ -1,8 +1,8 @@ package channel_v1 import ( - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) // GetLatestState retrieves the current state of the user stored on the Node. diff --git a/clearnode/api/channel_v1/get_latest_state_test.go b/clearnode/api/channel_v1/get_latest_state_test.go index 8392f853b..922bca4c8 100644 --- a/clearnode/api/channel_v1/get_latest_state_test.go +++ b/clearnode/api/channel_v1/get_latest_state_test.go @@ -8,9 +8,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func TestGetLatestState_Success(t *testing.T) { diff --git a/clearnode/api/channel_v1/handler.go b/clearnode/api/channel_v1/handler.go index 4e2058522..f5912303c 100644 --- a/clearnode/api/channel_v1/handler.go +++ b/clearnode/api/channel_v1/handler.go @@ -3,10 +3,10 @@ package channel_v1 import ( "context" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/rpc" ) // Handler manages channel state transitions and provides RPC endpoints for state submission. diff --git a/clearnode/api/channel_v1/interface.go b/clearnode/api/channel_v1/interface.go index 61f55860b..3817e275a 100644 --- a/clearnode/api/channel_v1/interface.go +++ b/clearnode/api/channel_v1/interface.go @@ -1,8 +1,8 @@ package channel_v1 import ( - "github.com/erc7824/nitrolite/clearnode/action_gateway" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/clearnode/action_gateway" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" ) diff --git a/clearnode/api/channel_v1/request_creation.go b/clearnode/api/channel_v1/request_creation.go index 81a419e68..99fd79fa1 100644 --- a/clearnode/api/channel_v1/request_creation.go +++ b/clearnode/api/channel_v1/request_creation.go @@ -4,11 +4,11 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" - "github.com/erc7824/nitrolite/pkg/rpc" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/rpc" ) // RequestCreation processes channel creation requests from users. diff --git a/clearnode/api/channel_v1/request_creation_test.go b/clearnode/api/channel_v1/request_creation_test.go index c324d92cc..1db939081 100644 --- a/clearnode/api/channel_v1/request_creation_test.go +++ b/clearnode/api/channel_v1/request_creation_test.go @@ -10,9 +10,9 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func TestRequestCreation_Success(t *testing.T) { diff --git a/clearnode/api/channel_v1/submit_session_key_state.go b/clearnode/api/channel_v1/submit_session_key_state.go index 16eb612ea..3f464d367 100644 --- a/clearnode/api/channel_v1/submit_session_key_state.go +++ b/clearnode/api/channel_v1/submit_session_key_state.go @@ -5,9 +5,9 @@ import ( "github.com/ethereum/go-ethereum/common" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/rpc" ) // SubmitSessionKeyState processes channel session key state submissions for registration and updates. diff --git a/clearnode/api/channel_v1/submit_state.go b/clearnode/api/channel_v1/submit_state.go index 11b144f8b..2daf5867d 100644 --- a/clearnode/api/channel_v1/submit_state.go +++ b/clearnode/api/channel_v1/submit_state.go @@ -1,10 +1,10 @@ package channel_v1 import ( - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" - "github.com/erc7824/nitrolite/pkg/rpc" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/rpc" ) // SubmitState processes user-submitted state transitions, validates them against the current state, diff --git a/clearnode/api/channel_v1/submit_state_test.go b/clearnode/api/channel_v1/submit_state_test.go index b6086305d..75407f1d1 100644 --- a/clearnode/api/channel_v1/submit_state_test.go +++ b/clearnode/api/channel_v1/submit_state_test.go @@ -10,9 +10,9 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func TestSubmitState_TransferSend_Success(t *testing.T) { diff --git a/clearnode/api/channel_v1/testing.go b/clearnode/api/channel_v1/testing.go index c8ba6da70..78c3ab245 100644 --- a/clearnode/api/channel_v1/testing.go +++ b/clearnode/api/channel_v1/testing.go @@ -8,9 +8,9 @@ import ( "github.com/shopspring/decimal" "github.com/stretchr/testify/mock" - "github.com/erc7824/nitrolite/clearnode/action_gateway" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/sign" + "github.com/layer-3/nitrolite/clearnode/action_gateway" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/sign" ) // MockStore is a mock implementation of the Store interface diff --git a/clearnode/api/channel_v1/utils.go b/clearnode/api/channel_v1/utils.go index 7346365fa..d7e85bb72 100644 --- a/clearnode/api/channel_v1/utils.go +++ b/clearnode/api/channel_v1/utils.go @@ -8,8 +8,8 @@ import ( "github.com/shopspring/decimal" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func toCoreState(state rpc.StateV1) (core.State, error) { diff --git a/clearnode/api/metric_store.go b/clearnode/api/metric_store.go index 5c6ee9b1d..2ea3e7601 100644 --- a/clearnode/api/metric_store.go +++ b/clearnode/api/metric_store.go @@ -1,10 +1,10 @@ package api import ( - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/clearnode/store/database" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/clearnode/store/database" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" ) // metricStore wraps a DatabaseStore to buffer metric callbacks during a DB transaction. diff --git a/clearnode/api/node_v1/get_assets.go b/clearnode/api/node_v1/get_assets.go index 392fb6ea3..290af89d6 100644 --- a/clearnode/api/node_v1/get_assets.go +++ b/clearnode/api/node_v1/get_assets.go @@ -3,7 +3,7 @@ package node_v1 import ( "strconv" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/rpc" ) // GetAssets retrieves the current assets of the Node. diff --git a/clearnode/api/node_v1/get_assets_test.go b/clearnode/api/node_v1/get_assets_test.go index 4e5495116..caae73999 100644 --- a/clearnode/api/node_v1/get_assets_test.go +++ b/clearnode/api/node_v1/get_assets_test.go @@ -7,8 +7,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func TestGetAssets_Success(t *testing.T) { diff --git a/clearnode/api/node_v1/get_config.go b/clearnode/api/node_v1/get_config.go index 91c80011f..050333c82 100644 --- a/clearnode/api/node_v1/get_config.go +++ b/clearnode/api/node_v1/get_config.go @@ -1,8 +1,8 @@ package node_v1 import ( - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) // GetConfig retrieves the current configuration of the Node. diff --git a/clearnode/api/node_v1/get_config_test.go b/clearnode/api/node_v1/get_config_test.go index dacae7233..c113a4a1d 100644 --- a/clearnode/api/node_v1/get_config_test.go +++ b/clearnode/api/node_v1/get_config_test.go @@ -7,8 +7,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func TestGetConfig_Success(t *testing.T) { diff --git a/clearnode/api/node_v1/interface.go b/clearnode/api/node_v1/interface.go index 140da7077..34f3f7074 100644 --- a/clearnode/api/node_v1/interface.go +++ b/clearnode/api/node_v1/interface.go @@ -1,7 +1,7 @@ package node_v1 import ( - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" ) // MemoryStore defines an in-memory data store interface for retrieving diff --git a/clearnode/api/node_v1/ping.go b/clearnode/api/node_v1/ping.go index 1a0a70e94..8de7c358e 100644 --- a/clearnode/api/node_v1/ping.go +++ b/clearnode/api/node_v1/ping.go @@ -1,7 +1,7 @@ package node_v1 import ( - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/rpc" ) // Ping handles the ping request and responds with the ping method. diff --git a/clearnode/api/node_v1/testing.go b/clearnode/api/node_v1/testing.go index f02577d70..cf114d19f 100644 --- a/clearnode/api/node_v1/testing.go +++ b/clearnode/api/node_v1/testing.go @@ -3,7 +3,7 @@ package node_v1 import ( "github.com/stretchr/testify/mock" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" ) // MockMemoryStore is a mock implementation of the MemoryStore interface diff --git a/clearnode/api/node_v1/utils.go b/clearnode/api/node_v1/utils.go index 2d3a3800d..fb6ed7041 100644 --- a/clearnode/api/node_v1/utils.go +++ b/clearnode/api/node_v1/utils.go @@ -4,8 +4,8 @@ import ( "fmt" "strconv" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func mapBlockchainV1(blockchain core.Blockchain) rpc.BlockchainInfoV1 { diff --git a/clearnode/api/rate_limits.go b/clearnode/api/rate_limits.go index 28088bafe..55041e2c4 100644 --- a/clearnode/api/rate_limits.go +++ b/clearnode/api/rate_limits.go @@ -3,7 +3,7 @@ package api import ( "time" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/rpc" ) const ( diff --git a/clearnode/api/rate_limits_test.go b/clearnode/api/rate_limits_test.go index bc631fb85..45cb7c299 100644 --- a/clearnode/api/rate_limits_test.go +++ b/clearnode/api/rate_limits_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/clearnode/api/rpc_router.go b/clearnode/api/rpc_router.go index 1f362e330..73a4e2274 100644 --- a/clearnode/api/rpc_router.go +++ b/clearnode/api/rpc_router.go @@ -3,19 +3,19 @@ package api import ( "time" - "github.com/erc7824/nitrolite/clearnode/action_gateway" - "github.com/erc7824/nitrolite/clearnode/api/app_session_v1" - "github.com/erc7824/nitrolite/clearnode/api/apps_v1" - "github.com/erc7824/nitrolite/clearnode/api/channel_v1" - "github.com/erc7824/nitrolite/clearnode/api/node_v1" - "github.com/erc7824/nitrolite/clearnode/api/user_v1" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/clearnode/store/database" - "github.com/erc7824/nitrolite/clearnode/store/memory" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" - "github.com/erc7824/nitrolite/pkg/rpc" - "github.com/erc7824/nitrolite/pkg/sign" + "github.com/layer-3/nitrolite/clearnode/action_gateway" + "github.com/layer-3/nitrolite/clearnode/api/app_session_v1" + "github.com/layer-3/nitrolite/clearnode/api/apps_v1" + "github.com/layer-3/nitrolite/clearnode/api/channel_v1" + "github.com/layer-3/nitrolite/clearnode/api/node_v1" + "github.com/layer-3/nitrolite/clearnode/api/user_v1" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/clearnode/store/database" + "github.com/layer-3/nitrolite/clearnode/store/memory" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/sign" ) type RPCRouter struct { diff --git a/clearnode/api/user_v1/get_action_allowances.go b/clearnode/api/user_v1/get_action_allowances.go index bbeb23fd5..5fa193a33 100644 --- a/clearnode/api/user_v1/get_action_allowances.go +++ b/clearnode/api/user_v1/get_action_allowances.go @@ -3,8 +3,8 @@ package user_v1 import ( "strconv" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) // GetActionAllowances retrieves the action allowances for a user. diff --git a/clearnode/api/user_v1/get_action_allowances_test.go b/clearnode/api/user_v1/get_action_allowances_test.go index 04113ee28..37cbcfac3 100644 --- a/clearnode/api/user_v1/get_action_allowances_test.go +++ b/clearnode/api/user_v1/get_action_allowances_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func TestGetActionAllowances_Success(t *testing.T) { diff --git a/clearnode/api/user_v1/get_balances.go b/clearnode/api/user_v1/get_balances.go index b85d9a9c7..44efc99f3 100644 --- a/clearnode/api/user_v1/get_balances.go +++ b/clearnode/api/user_v1/get_balances.go @@ -1,7 +1,7 @@ package user_v1 import ( - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/rpc" ) // GetBalances retrieves the balances of the user. diff --git a/clearnode/api/user_v1/get_balances_test.go b/clearnode/api/user_v1/get_balances_test.go index 4a8613eee..c07b08ccb 100644 --- a/clearnode/api/user_v1/get_balances_test.go +++ b/clearnode/api/user_v1/get_balances_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func TestGetBalances_Success(t *testing.T) { diff --git a/clearnode/api/user_v1/get_transactions.go b/clearnode/api/user_v1/get_transactions.go index 75b690002..9c7c1bd59 100644 --- a/clearnode/api/user_v1/get_transactions.go +++ b/clearnode/api/user_v1/get_transactions.go @@ -1,8 +1,8 @@ package user_v1 import ( - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) // GetTransactions retrieves transaction history for a user with optional filters. diff --git a/clearnode/api/user_v1/get_transactions_test.go b/clearnode/api/user_v1/get_transactions_test.go index 83b76d8c7..ed41441fa 100644 --- a/clearnode/api/user_v1/get_transactions_test.go +++ b/clearnode/api/user_v1/get_transactions_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func TestGetTransactions_Success(t *testing.T) { diff --git a/clearnode/api/user_v1/interface.go b/clearnode/api/user_v1/interface.go index 6caeb4eee..06f085a06 100644 --- a/clearnode/api/user_v1/interface.go +++ b/clearnode/api/user_v1/interface.go @@ -1,8 +1,8 @@ package user_v1 import ( - "github.com/erc7824/nitrolite/clearnode/action_gateway" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/clearnode/action_gateway" + "github.com/layer-3/nitrolite/pkg/core" ) // StoreTxHandler is a function that executes Store operations within a transaction. diff --git a/clearnode/api/user_v1/testing.go b/clearnode/api/user_v1/testing.go index 17b02ae96..be69f2c72 100644 --- a/clearnode/api/user_v1/testing.go +++ b/clearnode/api/user_v1/testing.go @@ -6,8 +6,8 @@ import ( "github.com/shopspring/decimal" "github.com/stretchr/testify/mock" - "github.com/erc7824/nitrolite/clearnode/action_gateway" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/clearnode/action_gateway" + "github.com/layer-3/nitrolite/pkg/core" ) // MockStore is a mock implementation of the Store interface diff --git a/clearnode/api/user_v1/utils.go b/clearnode/api/user_v1/utils.go index 226186dbd..c8a5f0930 100644 --- a/clearnode/api/user_v1/utils.go +++ b/clearnode/api/user_v1/utils.go @@ -1,8 +1,8 @@ package user_v1 import ( - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) func mapTransactionV1(tx core.Transaction) rpc.TransactionV1 { diff --git a/clearnode/api/utils.go b/clearnode/api/utils.go index 4aa3d4b84..dfbc54469 100644 --- a/clearnode/api/utils.go +++ b/clearnode/api/utils.go @@ -1,6 +1,6 @@ package api -import "github.com/erc7824/nitrolite/pkg/rpc" +import "github.com/layer-3/nitrolite/pkg/rpc" func getMethodPath(c *rpc.Context) string { switch c.Request.Method { diff --git a/clearnode/blockchain_worker.go b/clearnode/blockchain_worker.go index 2c7c518d0..a30d34b54 100644 --- a/clearnode/blockchain_worker.go +++ b/clearnode/blockchain_worker.go @@ -6,9 +6,9 @@ import ( "sync" "time" - "github.com/erc7824/nitrolite/clearnode/store/database" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/clearnode/store/database" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" ) type BlockchainWorkerStore interface { diff --git a/clearnode/chart/README.md b/clearnode/chart/README.md index 1656ae503..7ee043e87 100644 --- a/clearnode/chart/README.md +++ b/clearnode/chart/README.md @@ -17,7 +17,7 @@ Clearnode Helm chart To install the chart with the release name `my-release`: ```bash -helm install my-release git+https://github.com/erc7824/clearnode@chart?ref=main +helm install my-release git+https://github.com/layer-3/clearnode@chart?ref=main ``` The command deploys Clearnode on the Kubernetes cluster with default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation. @@ -54,7 +54,7 @@ helm delete my-release | config.secretEnvs | object | `{}` | Additional environment variables to be stored in a secret | | extraLabels | object | `{}` | Additional labels to add to all resources | | fullnameOverride | string | `""` | Override the full name | -| image.repository | string | `"ghcr.io/erc7824/clearnode"` | Docker image repository | +| image.repository | string | `"ghcr.io/layer-3/clearnode"` | Docker image repository | | image.tag | string | `"0.0.1"` | Docker image tag | | imagePullSecret | string | `""` | Image pull secret name | | metrics.enabled | bool | `true` | Enable Prometheus metrics | @@ -114,7 +114,7 @@ For managing sensitive values like API keys and credentials, you can use `helm-s 3. When deploying or upgrading, reference your secrets file with the `secrets://` prefix: ```bash - helm upgrade --install my-release git+https://github.com/erc7824/clearnode@chart?ref=main \ + helm upgrade --install my-release git+https://github.com/layer-3/clearnode@chart?ref=main \ -f values.yaml \ -f secrets://secrets.yaml ``` diff --git a/clearnode/chart/README.md.gotmpl b/clearnode/chart/README.md.gotmpl index 96c24668b..352b5f66e 100644 --- a/clearnode/chart/README.md.gotmpl +++ b/clearnode/chart/README.md.gotmpl @@ -17,7 +17,7 @@ To install the chart with the release name `my-release`: ```bash -helm install my-release git+https://github.com/erc7824/clearnode@chart?ref=main +helm install my-release git+https://github.com/layer-3/clearnode@chart?ref=main ``` The command deploys Clearnode on the Kubernetes cluster with default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation. @@ -61,7 +61,7 @@ For managing sensitive values like API keys and credentials, you can use `helm-s 3. When deploying or upgrading, reference your secrets file with the `secrets://` prefix: ```bash - helm upgrade --install my-release git+https://github.com/erc7824/clearnode@chart?ref=main \ + helm upgrade --install my-release git+https://github.com/layer-3/clearnode@chart?ref=main \ -f values.yaml \ -f secrets://secrets.yaml ``` diff --git a/clearnode/event_handlers/interface.go b/clearnode/event_handlers/interface.go index 40167fb07..c053fd9cf 100644 --- a/clearnode/event_handlers/interface.go +++ b/clearnode/event_handlers/interface.go @@ -1,7 +1,7 @@ package event_handlers import ( - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" ) // StoreTxHandler is a function that executes Store operations within a transaction. diff --git a/clearnode/event_handlers/service.go b/clearnode/event_handlers/service.go index 46e87d6b8..159d1c201 100644 --- a/clearnode/event_handlers/service.go +++ b/clearnode/event_handlers/service.go @@ -4,8 +4,8 @@ import ( "context" "time" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" ) var _ core.BlockchainEventHandler = &EventHandlerService{} diff --git a/clearnode/event_handlers/service_test.go b/clearnode/event_handlers/service_test.go index 2c3fed384..63fc1b934 100644 --- a/clearnode/event_handlers/service_test.go +++ b/clearnode/event_handlers/service_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" ) func TestHandleHomeChannelCreated_Success(t *testing.T) { diff --git a/clearnode/event_handlers/testing.go b/clearnode/event_handlers/testing.go index 6a87572ce..5d18d64b8 100644 --- a/clearnode/event_handlers/testing.go +++ b/clearnode/event_handlers/testing.go @@ -3,7 +3,7 @@ package event_handlers import ( "github.com/stretchr/testify/mock" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" ) // MockStore is a mock implementation of the Store interface for testing diff --git a/clearnode/main.go b/clearnode/main.go index 1e834277f..03086dbc6 100644 --- a/clearnode/main.go +++ b/clearnode/main.go @@ -12,13 +12,13 @@ import ( "github.com/ethereum/go-ethereum/ethclient" "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/erc7824/nitrolite/clearnode/api" - "github.com/erc7824/nitrolite/clearnode/event_handlers" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/clearnode/store/database" - "github.com/erc7824/nitrolite/clearnode/stress" - "github.com/erc7824/nitrolite/pkg/blockchain/evm" - "github.com/erc7824/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/clearnode/api" + "github.com/layer-3/nitrolite/clearnode/event_handlers" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/clearnode/store/database" + "github.com/layer-3/nitrolite/clearnode/stress" + "github.com/layer-3/nitrolite/pkg/blockchain/evm" + "github.com/layer-3/nitrolite/pkg/log" ) func main() { diff --git a/clearnode/metrics/exporter.go b/clearnode/metrics/exporter.go index d6dd0e313..6610888cc 100644 --- a/clearnode/metrics/exporter.go +++ b/clearnode/metrics/exporter.go @@ -8,9 +8,9 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/shopspring/decimal" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) const ( diff --git a/clearnode/metrics/interface.go b/clearnode/metrics/interface.go index c103e8ba2..49335459e 100644 --- a/clearnode/metrics/interface.go +++ b/clearnode/metrics/interface.go @@ -5,9 +5,9 @@ import ( "github.com/shopspring/decimal" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) // RuntimeMetricExporter defines the interface for recording runtime metrics across various components of the system. diff --git a/clearnode/runtime.go b/clearnode/runtime.go index c8df85a83..c65733b59 100644 --- a/clearnode/runtime.go +++ b/clearnode/runtime.go @@ -15,16 +15,16 @@ import ( "github.com/joho/godotenv" "github.com/prometheus/client_golang/prometheus" - "github.com/erc7824/nitrolite/clearnode/action_gateway" - "github.com/erc7824/nitrolite/clearnode/metrics" - "github.com/erc7824/nitrolite/clearnode/store/database" - "github.com/erc7824/nitrolite/clearnode/store/memory" - "github.com/erc7824/nitrolite/pkg/blockchain/evm" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" - "github.com/erc7824/nitrolite/pkg/rpc" - "github.com/erc7824/nitrolite/pkg/sign" - "github.com/erc7824/nitrolite/pkg/sign/kms/gcp" + "github.com/layer-3/nitrolite/clearnode/action_gateway" + "github.com/layer-3/nitrolite/clearnode/metrics" + "github.com/layer-3/nitrolite/clearnode/store/database" + "github.com/layer-3/nitrolite/clearnode/store/memory" + "github.com/layer-3/nitrolite/pkg/blockchain/evm" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/sign" + "github.com/layer-3/nitrolite/pkg/sign/kms/gcp" ) //go:embed config/migrations/*/*.sql diff --git a/clearnode/runtime_test.go b/clearnode/runtime_test.go index 0fd342827..2b66d9917 100644 --- a/clearnode/runtime_test.go +++ b/clearnode/runtime_test.go @@ -6,7 +6,7 @@ import ( "github.com/ethereum/go-ethereum/common" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" ) func TestCheckChannelHubVersion_Manual(t *testing.T) { diff --git a/clearnode/store/database/action_log.go b/clearnode/store/database/action_log.go index 01de38444..d6e331cad 100644 --- a/clearnode/store/database/action_log.go +++ b/clearnode/store/database/action_log.go @@ -5,8 +5,8 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/core" "github.com/google/uuid" + "github.com/layer-3/nitrolite/pkg/core" ) type ActionLogEntryV1 struct { diff --git a/clearnode/store/database/action_log_test.go b/clearnode/store/database/action_log_test.go index b416135c2..a0a5cf262 100644 --- a/clearnode/store/database/action_log_test.go +++ b/clearnode/store/database/action_log_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/clearnode/store/database/app.go b/clearnode/store/database/app.go index 9e38bce21..5df3757f8 100644 --- a/clearnode/store/database/app.go +++ b/clearnode/store/database/app.go @@ -5,8 +5,8 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" "gorm.io/gorm" ) diff --git a/clearnode/store/database/app_ledger_test.go b/clearnode/store/database/app_ledger_test.go index 9ff2f2443..afc318d7d 100644 --- a/clearnode/store/database/app_ledger_test.go +++ b/clearnode/store/database/app_ledger_test.go @@ -3,7 +3,7 @@ package database import ( "testing" - "github.com/erc7824/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/app" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/clearnode/store/database/app_session.go b/clearnode/store/database/app_session.go index f7500a710..225932fb0 100644 --- a/clearnode/store/database/app_session.go +++ b/clearnode/store/database/app_session.go @@ -5,8 +5,8 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" "gorm.io/gorm" ) diff --git a/clearnode/store/database/app_session_key_state.go b/clearnode/store/database/app_session_key_state.go index aa5e3fbae..76bcdd597 100644 --- a/clearnode/store/database/app_session_key_state.go +++ b/clearnode/store/database/app_session_key_state.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/app" "gorm.io/gorm" ) diff --git a/clearnode/store/database/app_session_key_state_test.go b/clearnode/store/database/app_session_key_state_test.go index 22dab70ce..823f76049 100644 --- a/clearnode/store/database/app_session_key_state_test.go +++ b/clearnode/store/database/app_session_key_state_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/app" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/clearnode/store/database/app_session_test.go b/clearnode/store/database/app_session_test.go index 239745cc4..0d8746b92 100644 --- a/clearnode/store/database/app_session_test.go +++ b/clearnode/store/database/app_session_test.go @@ -4,8 +4,8 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/clearnode/store/database/app_test.go b/clearnode/store/database/app_test.go index e392bc1d6..2293ddada 100644 --- a/clearnode/store/database/app_test.go +++ b/clearnode/store/database/app_test.go @@ -4,8 +4,8 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/clearnode/store/database/blockchain_action_test.go b/clearnode/store/database/blockchain_action_test.go index a1a09cff1..c94e24be7 100644 --- a/clearnode/store/database/blockchain_action_test.go +++ b/clearnode/store/database/blockchain_action_test.go @@ -3,8 +3,8 @@ package database import ( "testing" - "github.com/erc7824/nitrolite/pkg/core" "github.com/ethereum/go-ethereum/common" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/clearnode/store/database/channel.go b/clearnode/store/database/channel.go index a407ea7c8..01cb21d54 100644 --- a/clearnode/store/database/channel.go +++ b/clearnode/store/database/channel.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" "gorm.io/gorm" ) diff --git a/clearnode/store/database/channel_session_key_state.go b/clearnode/store/database/channel_session_key_state.go index 6eb23e179..aa41159a7 100644 --- a/clearnode/store/database/channel_session_key_state.go +++ b/clearnode/store/database/channel_session_key_state.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" "gorm.io/gorm" ) diff --git a/clearnode/store/database/channel_session_key_state_test.go b/clearnode/store/database/channel_session_key_state_test.go index a6f670105..4263cc63d 100644 --- a/clearnode/store/database/channel_session_key_state_test.go +++ b/clearnode/store/database/channel_session_key_state_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/clearnode/store/database/channel_test.go b/clearnode/store/database/channel_test.go index 2e13dbe6d..0be5d2559 100644 --- a/clearnode/store/database/channel_test.go +++ b/clearnode/store/database/channel_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/clearnode/store/database/contract_event.go b/clearnode/store/database/contract_event.go index b11465ece..955feb58d 100644 --- a/clearnode/store/database/contract_event.go +++ b/clearnode/store/database/contract_event.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" "gorm.io/gorm" ) diff --git a/clearnode/store/database/contract_event_test.go b/clearnode/store/database/contract_event_test.go index 090dd17f4..72e054d30 100644 --- a/clearnode/store/database/contract_event_test.go +++ b/clearnode/store/database/contract_event_test.go @@ -3,7 +3,7 @@ package database import ( "testing" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/clearnode/store/database/db_store.go b/clearnode/store/database/db_store.go index 21fe52a91..c87ae2331 100644 --- a/clearnode/store/database/db_store.go +++ b/clearnode/store/database/db_store.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" "gorm.io/gorm" "gorm.io/gorm/clause" diff --git a/clearnode/store/database/db_store_test.go b/clearnode/store/database/db_store_test.go index 3a819ded0..e5b113d10 100644 --- a/clearnode/store/database/db_store_test.go +++ b/clearnode/store/database/db_store_test.go @@ -3,7 +3,7 @@ package database import ( "testing" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/clearnode/store/database/interface.go b/clearnode/store/database/interface.go index 68b87a147..0961ffedb 100644 --- a/clearnode/store/database/interface.go +++ b/clearnode/store/database/interface.go @@ -3,8 +3,8 @@ package database import ( "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" ) diff --git a/clearnode/store/database/state.go b/clearnode/store/database/state.go index 9f62911ab..b4191a7e1 100644 --- a/clearnode/store/database/state.go +++ b/clearnode/store/database/state.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" "gorm.io/gorm" ) diff --git a/clearnode/store/database/state_test.go b/clearnode/store/database/state_test.go index aab572bd7..08e6e5ddc 100644 --- a/clearnode/store/database/state_test.go +++ b/clearnode/store/database/state_test.go @@ -3,7 +3,7 @@ package database import ( "testing" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/clearnode/store/database/test/postgres_integration_test.go b/clearnode/store/database/test/postgres_integration_test.go index b51b0adad..fb6a01f42 100644 --- a/clearnode/store/database/test/postgres_integration_test.go +++ b/clearnode/store/database/test/postgres_integration_test.go @@ -6,9 +6,9 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/clearnode/store/database" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/clearnode/store/database" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/clearnode/store/database/transaction.go b/clearnode/store/database/transaction.go index fa74ffb77..dbd1c8daa 100644 --- a/clearnode/store/database/transaction.go +++ b/clearnode/store/database/transaction.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" ) diff --git a/clearnode/store/database/transaction_test.go b/clearnode/store/database/transaction_test.go index 1f3c68109..f0a604f93 100644 --- a/clearnode/store/database/transaction_test.go +++ b/clearnode/store/database/transaction_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/clearnode/store/database/utils.go b/clearnode/store/database/utils.go index 6bac202b6..5f7af224e 100644 --- a/clearnode/store/database/utils.go +++ b/clearnode/store/database/utils.go @@ -5,8 +5,8 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" ) // databaseChannelToCore converts database.Channel to core.Channel diff --git a/clearnode/store/memory/interface.go b/clearnode/store/memory/interface.go index be05d9f67..eb80cbe44 100644 --- a/clearnode/store/memory/interface.go +++ b/clearnode/store/memory/interface.go @@ -1,7 +1,7 @@ package memory import ( - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" ) // MemoryStore defines an in-memory data store interface for retrieving diff --git a/clearnode/store/memory/memory_store.go b/clearnode/store/memory/memory_store.go index d4e27c9a1..707207edf 100644 --- a/clearnode/store/memory/memory_store.go +++ b/clearnode/store/memory/memory_store.go @@ -5,7 +5,7 @@ import ( "slices" "strings" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" ) type MemoryStoreV1 struct { diff --git a/clearnode/stress/app_session.go b/clearnode/stress/app_session.go index db19a6f16..085515445 100644 --- a/clearnode/stress/app_session.go +++ b/clearnode/stress/app_session.go @@ -13,8 +13,8 @@ import ( "github.com/shopspring/decimal" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/sign" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/sign" ) type pipe struct { diff --git a/clearnode/stress/config.go b/clearnode/stress/config.go index 6a1c64833..d9241baee 100644 --- a/clearnode/stress/config.go +++ b/clearnode/stress/config.go @@ -10,7 +10,7 @@ import ( ethcrypto "github.com/ethereum/go-ethereum/crypto" "github.com/ilyakaznacheev/cleanenv" - "github.com/erc7824/nitrolite/pkg/sign" + "github.com/layer-3/nitrolite/pkg/sign" ) // Config holds all stress test settings, read from environment variables. diff --git a/clearnode/stress/methods.go b/clearnode/stress/methods.go index 11ea73f25..6aca3e709 100644 --- a/clearnode/stress/methods.go +++ b/clearnode/stress/methods.go @@ -5,8 +5,8 @@ import ( "fmt" "strconv" - "github.com/erc7824/nitrolite/pkg/core" - sdk "github.com/erc7824/nitrolite/sdk/go" + "github.com/layer-3/nitrolite/pkg/core" + sdk "github.com/layer-3/nitrolite/sdk/go" ) // MethodRegistry returns all available stress test methods. diff --git a/clearnode/stress/pool.go b/clearnode/stress/pool.go index cd70071d9..a84050f39 100644 --- a/clearnode/stress/pool.go +++ b/clearnode/stress/pool.go @@ -4,9 +4,9 @@ import ( "fmt" "time" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/sign" - sdk "github.com/erc7824/nitrolite/sdk/go" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/sign" + sdk "github.com/layer-3/nitrolite/sdk/go" ) // CreateClientPool opens up to n WebSocket connections to the clearnode. diff --git a/clearnode/stress/runner.go b/clearnode/stress/runner.go index 457900b8d..7bc26ed1b 100644 --- a/clearnode/stress/runner.go +++ b/clearnode/stress/runner.go @@ -7,7 +7,7 @@ import ( "sync/atomic" "time" - sdk "github.com/erc7824/nitrolite/sdk/go" + sdk "github.com/layer-3/nitrolite/sdk/go" ) // RunTest executes totalReqs calls of fn distributed across the client pool. diff --git a/clearnode/stress/transfer.go b/clearnode/stress/transfer.go index 97df7ae57..d75467d07 100644 --- a/clearnode/stress/transfer.go +++ b/clearnode/stress/transfer.go @@ -13,9 +13,9 @@ import ( "github.com/shopspring/decimal" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/sign" - sdk "github.com/erc7824/nitrolite/sdk/go" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/sign" + sdk "github.com/layer-3/nitrolite/sdk/go" ) type wallet struct { diff --git a/clearnode/stress/types.go b/clearnode/stress/types.go index 9891b36e7..912b1e663 100644 --- a/clearnode/stress/types.go +++ b/clearnode/stress/types.go @@ -4,7 +4,7 @@ import ( "context" "time" - sdk "github.com/erc7824/nitrolite/sdk/go" + sdk "github.com/layer-3/nitrolite/sdk/go" ) // Runner is the unified signature for executing a stress test. diff --git a/erc7824-docs/.firebaserc b/erc7824-docs/.firebaserc deleted file mode 100644 index 40c8735e8..000000000 --- a/erc7824-docs/.firebaserc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "projects": { - "default": "erc7824" - } -} diff --git a/erc7824-docs/.gitignore b/erc7824-docs/.gitignore deleted file mode 100644 index d3e664850..000000000 --- a/erc7824-docs/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# Dependencies -/node_modules - -# Production -/build - -# Generated files -.docusaurus -.cache-loader - -# Misc -.firebase -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* -sdk -frontend \ No newline at end of file diff --git a/erc7824-docs/README.md b/erc7824-docs/README.md deleted file mode 100644 index 1a5805b96..000000000 --- a/erc7824-docs/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Website - -This repository hosts the source files from which the [erc7284.org website](https://erc7284.org) is built -using [Docusaurus](https://docusaurus.io/), a modern static website generator. - -### Installation - -``` -$ yarn -``` - -### Local Development - -``` -$ yarn start -``` - -This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. - -### Build - -``` -$ yarn build -``` - -This command generates static content into the `build` directory and can be served using any static contents hosting service. - -### Deployment - -Using SSH: - -``` -$ USE_SSH=true yarn deploy -``` - -Not using SSH: - -``` -$ GIT_USER= yarn deploy -``` - -If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. diff --git a/erc7824-docs/blog/authors.yml b/erc7824-docs/blog/authors.yml deleted file mode 100644 index 8bfa5c7c4..000000000 --- a/erc7824-docs/blog/authors.yml +++ /dev/null @@ -1,23 +0,0 @@ -yangshun: - name: Yangshun Tay - title: Front End Engineer @ Facebook - url: https://github.com/yangshun - image_url: https://github.com/yangshun.png - page: true - socials: - x: yangshunz - github: yangshun - -slorber: - name: Sébastien Lorber - title: Docusaurus maintainer - url: https://sebastienlorber.com - image_url: https://github.com/slorber.png - page: - # customize the url of the author page at /blog/authors/ - permalink: '/all-sebastien-lorber-articles' - socials: - x: sebastienlorber - linkedin: sebastienlorber - github: slorber - newsletter: https://thisweekinreact.com diff --git a/erc7824-docs/blog/tags.yml b/erc7824-docs/blog/tags.yml deleted file mode 100644 index bfaa778fb..000000000 --- a/erc7824-docs/blog/tags.yml +++ /dev/null @@ -1,19 +0,0 @@ -facebook: - label: Facebook - permalink: /facebook - description: Facebook tag description - -hello: - label: Hello - permalink: /hello - description: Hello tag description - -docusaurus: - label: Docusaurus - permalink: /docusaurus - description: Docusaurus tag description - -hola: - label: Hola - permalink: /hola - description: Hola tag description diff --git a/erc7824-docs/docs/_legacy/examples/index.md b/erc7824-docs/docs/_legacy/examples/index.md deleted file mode 100644 index 0cd8cd5ca..000000000 --- a/erc7824-docs/docs/_legacy/examples/index.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -sidebar_position: 5 -title: Examples -description: State Channels tutorials, application examples in solidity and golang -keywords: [erc7824, statechannels, state channels, ethereum scaling, layer 2, off-chain, tutorial, tictactoe, gaming] -tags: - - erc7824 - - games - - golang - - docs ---- - -# Example Apps diff --git a/erc7824-docs/docs/_legacy/examples/tictactoe.md b/erc7824-docs/docs/_legacy/examples/tictactoe.md deleted file mode 100644 index bb9eefc0a..000000000 --- a/erc7824-docs/docs/_legacy/examples/tictactoe.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -sidebar_position: 3 -title: Gaming with TicTacToe -description: Example of out to manage nitro state using go-nitro SDK -keywords: [erc7824, statechannels, nitro, sdk, development, state channels, ethereum scaling, L2] -tags: - - erc7824 - - golang - - go-nitro - - nitro - - docs ---- -# TicTacToe Offchain - -Below an example in go lang of usage of our upcoming go lang SDK -Typescript will be also available soon. - -```go -package main - -import ( - "errors" - "testing" - - "github.com/ethereum/go-ethereum/common" - ecrypto "github.com/ethereum/go-ethereum/crypto" - "github.com/layer-3/clearsync/pkg/signer" - "github.com/layer-3/neodax/internal/nitro" - "github.com/stretchr/testify/require" -) - -// TicTacToe represents a simple Nitro application implementing a Tic-Tac-Toe game. -type TicTacToe struct { - players []signer.Signer - grid [3][3]byte // 3x3 grid for TicTacToe - - ch nitro.Channel -} - -// NewTicTacToe initializes a new TicTacToe instance. -func NewTicTacToe(players []signer.Signer) *TicTacToe { - fp := nitro.FixedPart{ - Participants: []common.Address{players[0].CommonAddress(), players[1].CommonAddress()}, - } - vp := nitro.VariablePart{} - s := nitro.StateFromFixedAndVariablePart(fp, vp) - - return &TicTacToe{ - players: players, - grid: [3][3]byte{}, - ch: *nitro.NewChannel(s), - } -} - -// Definition returns the FixedPart of the Nitro state. -func (t *TicTacToe) Definition() nitro.FixedPart { - return t.ch.FixedPart -} - -// Data returns the VariablePart representing the current game state. -func (t *TicTacToe) Data(turn uint64) nitro.VariablePart { - return nitro.VariablePart{ - AppData: encodeGrid(t.grid), - Outcome: nitro.Exit{}, - } -} - -func (t *TicTacToe) State(turn uint64) nitro.SignedState { - return t.ch.SignedStateForTurnNum[turn] -} - -// Validate checks if the current state is valid. -func (t *TicTacToe) Validate(turn uint64) bool { - // Validate state can be done using the contract artifact - return true -} - -func (t *TicTacToe) LatestSupportedState() (nitro.SignedState, error) { - return t.ch.LatestSupportedState() -} - -// encodeGrid converts the grid to a byte slice for storage in AppData. -func encodeGrid(grid [3][3]byte) []byte { - var data []byte - // Encode using ABI - for _, row := range grid { - data = append(data, row[:]...) - } - return data -} - -// MakeMove updates the game state with a player's move. -func (t *TicTacToe) MakeMove(player byte, x, y int) error { - if x < 0 || x > 2 || y < 0 || y > 2 { - return errors.New("invalid move: out of bounds") - } - if t.grid[x][y] != 0 { - return errors.New("invalid move: cell already occupied") - } - t.grid[x][y] = player - return nil -} - -func (t *TicTacToe) StartGame() error { - preFundState, err := t.ch.State(nitro.PreFundTurnNum) - if err != nil { - return err - } - - for _, p := range t.players { - if _, err := t.ch.SignAndAddState(preFundState.State(), p); err != nil { - return err - } - } - - return nil -} - -func (t *TicTacToe) FinishGame() error { - lss, err := t.ch.LatestSignedState() - if err != nil { - return err - } - - lastState := lss.State().Clone() - lastState.IsFinal = true - lastState.TurnNum += 1 - - for _, p := range t.players { - if _, err := t.ch.SignAndAddState(lastState, p); err != nil { - return err - } - } - - return nil -} - -func TestTicTacToe(t *testing.T) { - // Create player signers - var alice, bob signer.Signer - - app := NewTicTacToe([]signer.Signer{alice, bob}) - nch, err := nitro.NewClient() // Implements channel.Client - require.NoError(t, err, "Error creating client") - - err = app.StartGame() - require.NoError(t, err, "Error starting game") - - // Open the channel - _, err = nch.Open(app) - require.NoError(t, err, "Error opening channel") - - // Simulate moves - err = app.MakeMove('X', 0, 0) - require.NoError(t, err, "Invalid move") - - err = app.MakeMove('O', 1, 1) - require.NoError(t, err, "Invalid move") - - err = app.FinishGame() - require.NoError(t, err, "Error finishing game") - - // Close the channel at turn 2 - err = nch.Close(app) - require.NoError(t, err, "Error closing channel") -} -``` diff --git a/erc7824-docs/docs/_legacy/faq.md b/erc7824-docs/docs/_legacy/faq.md deleted file mode 100644 index a547155a5..000000000 --- a/erc7824-docs/docs/_legacy/faq.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -sidebar_position: 7 -title: FAQ -description: State channels frequently asked questions -keywords: [erc7824, statechannels, nitro, sdk, faq, state channels, ethereum scaling, L2] -tags: - - erc7824 - - nitro - - faq ---- -# FAQ - -### State channels FAQ - -
- What is ERC-7824? -

- ERC-7824 is a proposed standard for cross-chain trade execution systems that use state channels. It defines structures and interfaces to enable efficient, secure, and scalable off-chain interactions while leveraging the blockchain for finality and dispute resolution. -

-
- -
- What is a state channel? -

- A state channel can be thought of as an account with multiple balances (often just two). The owners of that account can update those balances according to predefined rules, which are enforceable on a blockchain. This enables peer-to-peer games, payments, and other few-user applications to safely trade blockchain assets with extremely low latency, low cost, and high throughput without requiring trust in a third party. -

-
- -
- How do state channels work? -

- 1. Setup: Participants lock assets into a blockchain-based smart contract.
- 2. Off-Chain Updates: Transactions or updates occur off-chain through cryptographically signed messages.
- 3. Finalization: The final state is submitted on-chain for settlement, or disputes are resolved if necessary. -

-
- -
- What are the benefits of state channels? -

- - High Performance: Transactions are processed off-chain, providing low latency and high throughput.
- - Cost Efficiency: Minimal blockchain interactions significantly reduce gas fees.
- - Privacy: Off-chain interactions keep intermediate states confidential.
- - Flexibility: Supports a wide range of applications, including multi-chain trading. -

-
- -
- What kind of applications use state channels? -

- State channels enable the redistribution of assets according to arbitrary logic, making them suitable for: -

    -
  • Games: Peer-to-peer poker or other interactive games.
  • -
  • Payments: Microtransactions and conditional payments.
  • -
  • Swaps: Atomic swaps between assets.
  • -
  • Decentralized Trading: Real-time, high-frequency trading applications.
  • -
-

-
- -
- How is Nitro Protocol implemented? -

- - On-Chain Components: Implemented in Solidity and included in the npm package @statechannels/nitro-protocol.
- - Off-Chain Components: A reference implementation provided through go-nitro, a lightweight client written in Go. -

-
- -
- Where is Nitro Protocol being used? -

- The maintainers of Nitro Protocol are actively integrating it into the Filecoin Retrieval Market and the Filecoin Virtual Machine, enabling decentralized and efficient content distribution. -

-
- -
- What is the structure of a state in state channels? -

- A state consists of: -

    -
  1. Fixed Part: Immutable properties like participants, nonce, app definition, and challenge duration.
  2. -
  3. Variable Part: Changeable properties like outcomes, application data, and turn numbers.
  4. -
- In Nitro, participants sign a keccak256 hash of both these parts to commit to a particular state. The turnNum determines the version of the state, while isFinal can trigger an instant finalization when fully countersigned. -

-
- -
- What is a challenge duration? -

- The challenge duration is a time window during which disputes can be raised on-chain. If no disputes are raised, the state channel finalizes according to its latest agreed state. In Nitro, it is set at channel creation and cannot be changed later. During this period, an unresponsive or dishonest participant can be forced to progress the channel state via on-chain transactions. -

-
- -
- How do disputes get resolved in state channels? -

- Participants can: -

    -
  1. Submit signed updates to the blockchain as evidence.
  2. -
  3. Resolve disputes based on turn numbers and application-specific rules stored in an on-chain appDefinition.
  4. -
  5. Finalize the channel after the challenge duration if no valid disputes arise.
  6. -
- Nitro Protocol introduces a challenge mechanism, enabling any participant to push the channel state on-chain, forcing the other side to respond. This ensures that unresponsive or malicious actors cannot stall the channel indefinitely. -

-
- -
- What is the typical channel lifecycle in Nitro Protocol? -

- A direct channel often follows these stages: -

- 1. Proposed: A participant signs the initial (prefund) state with turnNum=0.
- 2. ReadyToFund: All participants countersign the prefund state, ensuring it is safe to deposit on-chain.
- 3. Funded: Deposits appear on-chain, and participants exchange a postfund state (turnNum=1).
- 4. Running: The channel can be updated off-chain by incrementing turnNum and exchanging signatures.
- 5. Finalized: A state with isFinal=true is fully signed. No more updates are possible; the channel can pay out according to the final outcome. -

-
- -
- How do I finalize and withdraw funds from a direct channel in Nitro? -

- - Finalization (Happy Path): If a fully signed state with isFinal=true exists off-chain, any participant can call conclude on the Nitro Adjudicator to finalize instantly.
- - Finalization (Dispute Path): If participants are unresponsive or disagree, one party can challenge with the latest supported state. After the challenge window, the channel is finalized if unchallenged or out-of-date states are resolved.
- - Withdrawing: Once finalized, participants use the transfer or concludeAndTransferAllAssets method to claim their allocations on-chain. -

-
diff --git a/erc7824-docs/docs/_legacy/index.md b/erc7824-docs/docs/_legacy/index.md deleted file mode 100644 index 9680d9e2c..000000000 --- a/erc7824-docs/docs/_legacy/index.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -sidebar_position: 5 -slug: /legacy -title: Legacy -description: Create high-performance chain agnostic dApps using state channels -keywords: [erc7824, statechannels, chain abstraction, chain agnostic, state channels, ethereum scaling, layer 2, layer 3, nitro, trading, high-speed] -tags: - - erc7824 - - docs ---- - -# Inject Nitro in your Stack - -Blockchain technology has introduced a paradigm shift in how digital assets and decentralized applications (dApps) operate. However, scalability, transaction costs, and cross-chain interoperability remain significant challenges. **ERC-7824** and the **Nitro Protocol** provide a solution by enabling **off-chain state channels**, allowing seamless and efficient transactions without compromising on security or performance. - -ERC-7824 defines a minimal, universal interface for state channel interactions without assuming any specific underlying chain, making it naturally chain agnostic. It abstracts away chain-specific details by specifying data structures and messaging formats that can be verified on any blockchain with basic smart contract capabilities. As a result, developers can rely on ERC-7824’s standard approach for off-chain interactions and dispute resolution across multiple L1 or L2 networks, preserving interoperability and reusability of state channel components in a wide range of environments. - -## Supercharging Web2/3 Applications - -The Nitro Protocol is designed to integrate effortlessly with **both Web2 and Web3 applications**. Whether operating in traditional infrastructure or leveraging blockchain ecosystems, developers can **enhance their software stack** with blockchain-grade security and efficiency without sacrificing speed or requiring major architectural changes. - -```mermaid -quadrantChart - title Distributed system scaling - x-axis Low Speed --> High Speed - y-axis Low Trust --> High Trust - quadrant-1 Network Overlay - quadrant-2 Decentralized Systems - quadrant-3 Cross-chain Systems - quadrant-4 Centralized Systems - Bitcoin: [0.20, 0.8] - L2 Chains: [0.57, 0.70] - L3 Nitro: [0.75, 0.60] - ClearSync: [0.80, 0.85] - Solana: [0.60, 0.42] - TradFi: [0.85, 0.34] - CEX: [0.70, 0.20] - Bridges: [0.24, 0.24] - Cross-chain Messaging: [0.15, 0.40] - Ethereum: [0.35, 0.78] -``` - -## Key Benefits of ERC-7824 and Nitro Protocol - -1. **Scalability Without Congestion** - By moving most interactions off-chain and leveraging **state channels**, Nitro enables **near-instant, gas-free transactions**, reducing load on the base layer blockchain. - -2. **Cost Efficiency** - Users and applications **avoid high gas fees** by settling transactions off-chain while maintaining an on-chain fallback for dispute resolution. - -3. **Chain Agnosticism & Interoperability** - ERC-7824 is designed to be **cross-chain compatible**, allowing seamless **interactions between different blockchain ecosystems**. This enables **cross-chain asset transfers, atomic swaps, and multi-chain smart contract execution**. - -4. **Security & Trustless Execution** - - Transactions occur off-chain but remain **cryptographically signed and enforceable** on-chain. - - **ForceMove** dispute resolution mechanism ensures that **malicious actors cannot manipulate state transitions**. - -5. **Modular & Flexible** - The protocol provides **plug-and-play interfaces**, allowing developers to build **custom business logic** while ensuring compatibility with the ERC-7824 framework. - -6. **Real-World Applications** - - **DeFi Scaling:** Enables off-chain transactions and batch settlements for **DEXs and lending protocols**. - - **Gaming & Metaverse:** Supports **high-speed microtransactions** for in-game economies. - - **Cross-Chain Payments:** Facilitates **low-cost, instant remittances and global transactions**. - - **Enterprise & Supply Chain:** Enhances **multi-party agreements and digital asset settlement**. - -## Summary - -ERC-7824 and the Nitro Protocol provide the **next-generation infrastructure** for decentralized applications, bridging the gap between **legacy systems and blockchain**. By combining **off-chain speed** with **on-chain security**, this framework allows developers to build high-performance, scalable solutions without being tied to a single blockchain network. diff --git a/erc7824-docs/docs/_legacy/protocol.md b/erc7824-docs/docs/_legacy/protocol.md deleted file mode 100644 index 6f638dca8..000000000 --- a/erc7824-docs/docs/_legacy/protocol.md +++ /dev/null @@ -1,430 +0,0 @@ ---- -sidebar_position: 3 -title: Protocol -description: Interfaces and nitro types for NitroApp development -keywords: [erc7824, statechannels, nitro, protocol, sdk, development, state channels, ethereum scaling, L2] -tags: - - erc7824 - - protocol - - nitro - - sdk - - docs ---- - -# Nitro Protocol - -**Nitro Protocol** is a framework for building fast, secure, and flexible state channels on Ethereum. It enables developers to keep most transactions off-chain for near-instant updates while retaining the option to settle disputes on-chain as a last resort. At the heart of Nitro are well-defined states and transition rules enforced by the on-chain Adjudicator contract. The application-specific logic (“NitroApp”) is encapsulated in an on-chain contract, which the Nitro Adjudicator references during disputes to verify that a proposed state transition is valid. - -Off-chain, developers write the code responsible for constructing and signing new states, managing state progression, and orchestrating deposits and withdrawals. This off-chain logic ensures quick state updates without paying on-chain gas fees, since new states only require on-chain submission when there is a disagreement or final payout. By following Nitro’s API and implementing these components carefully, developers can build robust applications that benefit from low latency and trust-minimized on-chain settlements. - -## Overview - -```mermaid -graph TD - subgraph Nitro Protocol - INitroAdjudicator[INitroAdjudicator] -->|Interacts with| IForceMove[IForceMove] - INitroAdjudicator -->|Manages outcomes| IMultiAssetHolder[IMultiAssetHolder] - INitroAdjudicator -->|Handles states| IStatusManager[IStatusManager] - - IForceMove -->|Implements state transitions| IForceMoveApp[IForceMoveApp] - IForceMoveApp -->|Validates states| INitroTypes[INitroTypes] - end - - subgraph NitroApps - CountingApp[CountingApp] -->|Implement| IForceMoveApp - EscrowApp[VEscrowApp] -->|Implement| IForceMoveApp - end -``` - -## 1. States & Channels - -A state channel can be thought of as a private ledger containing balances and other arbitrary data housed in a data structure called a “state.” The state of the channel is updated, committed to (via signatures), and exchanged between a fixed set of actors (participants). A state channel controls funds which are locked — typically on an L1 blockchain — and those locked funds are unlocked according to the channel’s final state when the channel finishes. - -### States - -In Nitro protocol, a state is broken up into fixed and variable parts: - -
-Solidity - -```solidity -import {ExitFormat as Outcome} from '@statechannels/exit-format/contracts/ExitFormat.sol'; - -struct FixedPart { - address[] participants; - uint48 channelNonce; - address appDefinition; - uint48 challengeDuration; -} - -struct VariablePart { - Outcome.SingleAssetExit[] outcome; // (1) - bytes appData; - uint48 turnNum; - bool isFinal; -} -``` - -
- -
-TypeScript - -```typescript -import * as ExitFormat from '@statechannels/exit-format'; -// (1) -import {Address, Bytes, Bytes32, Uint256, Uint48, Uint64} from '@statechannels/nitro-protocol'; - -export interface FixedPart { - participants: Address[]; - channelNonce: Uint64; - appDefinition: Address; - challengeDuration: Uint48; -} - -export interface VariablePart { - outcome: ExitFormat.Exit; // (2) - appData: Bytes; - turnNum: Uint48; - isFinal: boolean; -} -``` - -
- -
-Go - -```go -import ( - "github.com/statechannels/go-nitro/channel/state/outcome" - "github.com/statechannels/go-nitro/types" // (1) -) - -type ( - FixedPart struct { - Participants []types.Address - ChannelNonce uint64 - AppDefinition types.Address - ChallengeDuration uint32 - } - - VariablePart struct { - AppData types.Bytes - Outcome outcome.Exit // (2) - TurnNum uint64 - IsFinal bool - } -) -``` - -
- -1. `Bytes32`, `Bytes`, `Address`, `Uint256`, `Uint64` are aliases for hex-encoded strings. `Uint48` is aliased to a `number`. -2. This composite type is explained in more detail in the “Outcomes” section. - -Each state has: - -- **Fixed Part**: - - `participants`: The list of addresses (one per participant). - - `channelNonce`: A unique identifier so that re-using the same participants and app definition does not cause replay attacks. - - `appDefinition`: Address of the application contract (on-chain code) that enforces application-specific rules. - - `challengeDuration`: The duration (in seconds) of the challenge window in case of on-chain disputes. - -- **Variable Part**: - - `outcome`: The distribution of funds if the channel were finalized in that state. - - `appData`: Extra data interpreted by the application (e.g. game state). - - `turnNum`: A version counter for the state updates. - - `isFinal`: A boolean that triggers instant finalization when fully signed. - -#### Channel IDs - -Channels are identified by the hash of the fixed part: - -```solidity -bytes32 channelId = keccak256( - abi.encode( - fixedPart.participants, - fixedPart.channelNonce, - fixedPart.appDefinition, - fixedPart.challengeDuration - ) -); -``` - -#### State Commitments - -To commit to a state, we take the keccak256 hash of a combination of the channel ID and the variable part: - -```solidity -bytes32 stateHash = keccak256(abi.encode( - channelId, - variablePart.appData, - variablePart.outcome, - variablePart.turnNum, - variablePart.isFinal -)); -``` - -Participants sign this `stateHash` using their private key. A signature has the form `(v, r, s)` in ECDSA. - -#### Signed Variable Parts and Support Proofs - -A “signed variable part” is simply the variable part plus the signatures from participants. Submitting such bundles to the chain allows the chain to verify that a given state is “supported” off-chain. Typically, a single channel update is accompanied by a single “candidate” state plus (optionally) a sequence of older states that prove the channel progressed correctly. This bundle is called a “support proof.” - ---- - -## 2. Execution Rules - -A channel’s execution rules dictate which state updates can be considered valid and which are invalid. Once a state is considered “supported” (i.e., it has enough signatures to pass on-chain checks), it can become the channel’s final state if the channel is later finalized. - -### Core Protocol Rules - -1. **Higher Turn Number**: Among multiple states, the one with the highest turn number supersedes the rest. -2. **Instant Finalization**: If a state has `isFinal = true` and is fully signed, it can finalize the channel without needing a challenge phase. - -### Application Rules - -Each channel references an on-chain contract (the `appDefinition`) that encodes custom logic: - -```solidity -interface IForceMoveApp is INitroTypes { - function stateIsSupported( - FixedPart calldata fixedPart, - RecoveredVariablePart[] calldata proof, - RecoveredVariablePart calldata candidate - ) external view returns (bool, string memory); -} -``` - -When on-chain disputes occur, the chain calls this function to confirm that a “candidate” state is valid relative to the “proof” states. - ---- - -## 3. Outcomes - -The **outcome** of a state is the piece that dictates how funds in the channel will be disbursed once the channel is finalized. Nitro uses the [L2 exit format](https://github.com/statechannels/exit-format): - -- **Outcome**: An array of `SingleAssetExit`, each describing: - - An `asset` (e.g. the zero address for native ETH, or an ERC20 contract address), - - Optional `assetMetadata`, - - A list of **allocations**. - -Example: - -```ts -import { - Exit, - SingleAssetExit, - NullAssetMetadata, - AllocationType, -} from "@statechannels/exit-format"; - -const ethExit: SingleAssetExit = { - asset: "0x0", // native token (ETH) - assetMetadata: NullAssetMetadata, - allocations: [ - { - destination: "0x00000000000000000000000096f7123E3A80C9813eF50213ADEd0e4511CB820f", - amount: "0x05", - allocationType: AllocationType.simple, - metadata: "0x", - }, - { - destination: "0x0000000000000000000000000737369d5F8525D039038Da1EdBAC4C4f161b949", - amount: "0x05", - allocationType: AllocationType.simple, - metadata: "0x", - }, - ], -}; - -const exit = [ethExit]; -``` - -### Allocations - -Allocations specify a `destination` and an `amount`. For **direct channels**, these allocations are typically of `allocationType = 0` (simple). Each entry is the portion of the channel’s asset that the channel participant can claim once the channel is finalized on-chain. - -### Destinations - -A destination is a 32-byte value that may represent: - -- A channel ID, or -- An “external destination” (an Ethereum address left-padded with zeros). - -For **direct** channels, the relevant destinations are ordinarily external addresses corresponding to participant wallets. - ---- - -## 4. Lifecycle of a Channel - -Below we describe the lifecycle of a **direct** channel. - -### Off-Chain Lifecycle (High-Level) - -1. **Proposed**: A participant signs the prefund state (`turnNum = 0`) and shares it with others. -2. **ReadyToFund**: Once all have countersigned, the channel can safely be funded on-chain. -3. **Funded**: After the postfund state (`turnNum = 1`) is signed, the channel is recognized as funded. -4. **Running**: States with `turnNum > 1` can be signed as the channel operates. -5. **Finalized**: A state with `isFinal = true` is fully signed. - -A diagram (off-chain states): - -```mermaid -stateDiagram-v2 - [*] --> Proposed - Proposed --> ReadyToFund - ReadyToFund --> Funded - Funded --> Running - Running --> Finalized -``` - -### Funding Lifecycle (On-Chain) - -A channel is “funded” on-chain once participants’ deposits have been made to the adjudicator contract. Eventually, once the channel is finished, those funds are withdrawn to participants’ external addresses. - -```mermaid -stateDiagram-v2 -state OnChain { - state Funding { - [*]-->NotFundedOnChain - NotFundedOnChain --> FundedOnChain - FundedOnChain --> NotFundedOnChain - } -} -``` - -- **NotFundedOnChain** → channel has 0 or insufficient deposit in the adjudicator. -- **FundedOnChain** → channel has the full deposit allocated. - -### Adjudication Lifecycle (On-Chain) - -Nitro’s `NitroAdjudicator` contract tracks the channel’s status on-chain: - -1. **Open**: No challenge is in progress. -2. **Challenge**: A challenge is ongoing; participants must respond or let it expire. -3. **Finalized**: The channel has either concluded with a `conclude` transaction or the challenge timed out. - -```mermaid -stateDiagram-v2 -state OnChain { -state Adjudication { -Open -->Challenge: challenge -Open --> Open: checkpoint -Open--> Finalized: conclude -Challenge--> Challenge: challenge -Challenge--> Open: checkpoint -Challenge--> Finalized: conclude -Challenge--> Finalized': timeout -} -} -``` - ---- - -## 5. Precautions and Limits - -### Precautions - -As a participant, you should verify: - -- The number of participants is correct. -- Your own address is in `participants`. -- The `channelNonce` is unique. -- `challengeDuration` is acceptable. - -### Limits - -There are upper bounds to how large a state can be before it becomes impractical to finalize on-chain: - -- `MAX_TX_DATA_SIZE`: ~128 KB typical limit for Ethereum transaction data. -- `NITRO_MAX_GAS`: ~6M gas, a safe upper bound for some Nitro operations. -- `MAX_OUTCOME_ITEMS`: The protocol recommends limiting the number of allocation items to at most 2000 to remain under `MAX_TX_DATA_SIZE` and `NITRO_MAX_GAS`. - ---- - -## 6. Funding a Channel - -This section covers **on-chain** depositing for direct channels. - -### Fund with an On-Chain `deposit` - -When participants want to stake funds, they each perform a deposit transaction to the `NitroAdjudicator`: - -```solidity -function deposit( - address asset, - bytes32 destination, - uint256 expectedHeld, - uint256 amount -) public payable -``` - -- `asset`: zero address if depositing ETH, or an ERC20 token address if depositing tokens. -- `destination`: must be the channel’s `channelId` (not an external address). -- `expectedHeld`: ensures the holdings so far match what you expect, preventing over-funding or front-running. -- `amount`: how much is being deposited in this transaction. - -If depositing ETH, include `{value: amount}` in the transaction call. If depositing ERC20 tokens, you must first `approve` the `NitroAdjudicator` for `amount`. - -#### Outcome Priority - -It is possible for a channel to be underfunded if not all participants have deposited yet. If a channel finalizes underfunded, the distribution is paid in priority order (the topmost allocation entries get paid first). Thus, participants commonly wait until they each see the correct deposit events on-chain before signing postfund states. - ---- - -## 7. Finalizing a Channel - -A channel is **finalized** either off-chain with unanimous agreement or via an on-chain transaction (the “happy path” or the “sad path”). - -### Happy Path (Off-Chain) - -- One participant proposes a state with `isFinal = true`. -- All participants sign it. -- No on-chain action is strictly needed to *logically* finalize the channel (they can defund off-chain if no on-chain deposit was used). - -### On-Chain – Calling `conclude` - -When the channel was funded on-chain, any participant (or anyone with the finalization proof) can call: - -```solidity -function conclude( - FixedPart memory fixedPart, - SignedVariablePart memory candidate -) external -``` - -This finalizes the channel’s outcome **instantly** on-chain (no challenge period). Afterward, participants can withdraw or `transfer` funds out of the adjudicator contract. - ---- - -## 8. Defunding a Channel - -Once a channel is **finalized**, its locked funds can be released. For **direct** channels, this typically means: - -### On-Chain Defunding Using `transfer` - -The on-chain `transfer` method in the `NitroAdjudicator` contract moves funds from the channel’s escrow to participant addresses: - -```solidity -function transfer( - uint256 assetIndex, - bytes32 channelId, - bytes memory outcomeBytes, - bytes32 stateHash, - uint256[] memory indices -) public -``` - -- `assetIndex`: Which asset in the channel’s outcome you want to pay out. -- `channelId`: The identifier of the channel. -- `outcomeBytes`: The encoded outcome (list of allocations). -- `stateHash`: If finalization was done via `conclude`, you may pass `bytes32(0)`. -- `indices`: Which allocations to pay out in this transaction (`[]` means “all”). - -Once called, the relevant allocations are paid out to the listed `destination`s, and the contract’s tracked holdings are reduced accordingly. Multiple calls may be required to pay out all allocations, especially if there are many recipients. - -#### concludeAndTransferAllAssets - -The contract also offers a “batched” convenience method `concludeAndTransferAllAssets` that finalizes and transfers in a single transaction. diff --git a/erc7824-docs/docs/_legacy/resources.md b/erc7824-docs/docs/_legacy/resources.md deleted file mode 100644 index 263e6b44a..000000000 --- a/erc7824-docs/docs/_legacy/resources.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -sidebar_position: 4 -title: Resources -description: Learn more about state channels -keywords: [erc7824, statechannels, state channels, awesome state channels, ethereum scaling, layer 2, layer 3, nitro, trading, high-speed] -tags: - - erc7824 - - links - - docs ---- - -# Resources - -### General Overviews - -- [What are State Channels? (Ethereum Foundation)](https://ethereum.org/en/developers/docs/scaling/state-channels/) -- [State Channels vs. Rollups (Vitalik Buterin)](https://vitalik.eth.limo/general/2021/01/05/rollup.html) - -### Technical Papers - -- [(2018) ForceMove: An n-party state channel protocol](https://magmo.com/force-move-games.pdf) -- [(2019) Nitro Protocol: Virtual channels](https://magmo.com/nitro-protocol.pdf) -- [(2022) Stateful Asset Transfer Protocol](https://statechannels.github.io/satp_paper/satp.pdf) -- [Nitro Protocol GitHub Repository](https://github.com/statechannels/go-nitro) - -### Videos and Tutorials - -- [State Channels Explained (YouTube)](https://www.youtube.com/watch?v=p1OTfvCbRFo) -- [Nitro Protocol Walkthrough (YouTube)](https://www.youtube.com/watch?v=Z5dcPAfIzP4) -- [Ethereum Magicians State Channel Discussion](https://ethereum-magicians.org/c/scaling/state-channels) -- [Protocol Tutorial](https://docs.statechannels.org/protocol-tutorial/0010-states-channels/) -- [State Channels Blog](https://blog.statechannels.org/) diff --git a/erc7824-docs/docs/_legacy/spec.md b/erc7824-docs/docs/_legacy/spec.md deleted file mode 100644 index 0c79e4e62..000000000 --- a/erc7824-docs/docs/_legacy/spec.md +++ /dev/null @@ -1,355 +0,0 @@ ---- -sidebar_position: 2 -title: Specification -description: Request for Comment on statechannels framework -keywords: [erc7824, ERC, Ethereum, statechannels, nitro, sdk, development, state channels, ethereum scaling, L2] -tags: - - erc7824 - - nitro - - docs ---- - -# ERC-7824 - -## Abstract - -State Channels is allowing participants to perform off-chain transactions while maintaining the security guarantees of the Ethereum blockchain. The goal is to enhance scalability and reduce transaction costs for decentralized applications. - -This standard defines a framework for implementing state channel systems, enabling efficient off-chain transaction execution and dispute resolution. It provides interfaces for managing channel states and an example implementation. - -## Motivation - -The Ethereum network faces challenges in scalability and transaction costs, making it less feasible for high-frequency interactions. State Channels provide a mechanism to perform most interactions off-chain, reserving on-chain operations for dispute resolution or final settlement. This ERC facilitate the adoption of state channels by standardizing essential interfaces. - -## Specification - -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. - -### Glossary of Terms - -- **Channel**: A construct where participants interact off-chain using signed messages. -- **Channel ID**: A unique identifier for a channel, derived from the channel's fixed part. -- **Participant**: An entity (EOA) involved in a state channel. -- **Turn Number**: A counter indicating the sequence of moves in the channel. -- **State**: A representation of the channel's condition, updated via signed messages. -- **Fixed Part**: Immutable channel parameters. -- **Variable Part**: Mutable channel parameters that evolve as states are updated. -- **ForceMove**: A procedure for resolving disputes by transitioning the state on-chain. -- **Outcome**: The final result of a channel state used for settling balances. -- **Nitro**: Historial name of the initial protocol - -### Data Structures - -#### ExitFormat - -Standard data structure for exiting an EVM based state channel. - -```solidity -/// @title ExitFormat -/// @notice Standard for encoding channel outcomes -library ExitFormat { - struct SingleAssetExit { - address asset; - AssetMetadata assetMetadata; - Allocation[] allocations; - } - - // AssetMetadata allows for different token standards - struct AssetMetadata { - AssetType assetType; - bytes metadata; - } - - enum AssetType {Default, ERC721, ERC1155, Qualified} - - enum AllocationType {simple, withdrawHelper, guarantee} - - struct Allocation { - bytes32 destination; - uint256 amount; - uint8 allocationType; - bytes metadata; - } -} -``` - -#### NitroTypes - -The `FixedPart`, `VariablePart` compose a "state". The state of the channel is updated, committed to and exchanged between a fixed set of participants. - -```solidity -/// @title NitroTypes -/// @notice Defines the core data structures used in state channels. -interface INitroTypes { - struct FixedPart { - address[] participants; - uint64 channelNonce; // This is a unique number used to differentiate channels - address appDefinition; // This is an Ethereum address where a ForceMoveApp has been deployed - uint48 challengeDuration; // This is duration in seconds of the challenge-response window - } - - struct VariablePart { - Outcome.SingleAssetExit[] outcome; - bytes appData; - uint48 turnNum; - bool isFinal; // This is a boolean flag which allows the channel to be finalized "instantly" - } - - struct SignedVariablePart { - VariablePart variablePart; - Signature[] sigs; - } - - struct RecoveredVariablePart { - VariablePart variablePart; - uint256 signedBy; // bitmask - } -} -``` - -#### Channel ID - -A `channelId` is derived using: - -```solidity -bytes32 channelId = keccak256( - abi.encode( - fixedPart.participants, - fixedPart.channelNonce, - fixedPart.appDefinition, - fixedPart.challengeDuration - ) -); -``` - -## State commitments - -To commit to a state, a hash is formed as follows: - -```solidity -bytes32 stateHash = keccak256(abi.encode( - channelId, - variablePart.appData, - variablePart.outcome, - variablePart.turnNum, - variablePart.isFinal -)); -``` - -### Interfaces - -#### IForceMoveApp - -Define the state machine of a ForceMove state channel protocol - -```solidity -/// @title IForceMoveApp -/// @notice Interface for implementing protocol rules in state channels -interface IForceMoveApp is INitroTypes { - // @notice Encodes application-specific rules for a particular ForceMove-compliant state channel. Must revert or return false when invalid support proof and a candidate are supplied. - // @dev Depending on the application, it might be desirable to narrow the state mutability of an implementation to 'pure' to make security analysis easier. - // @param fixedPart Fixed part of the state channel. - // @param proof Array of recovered variable parts which constitutes a support proof for the candidate. May be omitted when `candidate` constitutes a support proof itself. - // @param candidate Recovered variable part the proof was supplied for. Also may constitute a support proof itself. - function stateIsSupported( - FixedPart calldata fixedPart, - RecoveredVariablePart[] calldata proof, - RecoveredVariablePart calldata candidate - ) external view returns (bool, string memory); -} -``` - -#### IForceMove - -The IForceMove interface defines the interface that an implementation of ForceMove should implement. - -```solidity -interface IForceMove is INitroTypes { - /** - * @notice Registers a challenge against a state channel. A challenge will either prompt another participant into clearing the challenge (via one of the other methods), or cause the channel to finalize at a specific time. - * @dev Registers a challenge against a state channel. A challenge will either prompt another participant into clearing the challenge (via one of the other methods), or cause the channel to finalize at a specific time. - * @param fixedPart Data describing properties of the state channel that do not change with state updates. - * @param proof Additional proof material (in the form of an array of signed states) which completes the support proof. - * @param candidate A candidate state (along with signatures) which is being claimed to be supported. - * @param challengerSig The signature of a participant on the keccak256 of the abi.encode of (supportedStateHash, 'forceMove'). - */ - function challenge( - FixedPart memory fixedPart, - SignedVariablePart[] memory proof, - SignedVariablePart memory candidate, - Signature memory challengerSig - ) external; - - /** - * @notice Overwrites the `turnNumRecord` stored against a channel by providing a candidate with higher turn number. - * @dev Overwrites the `turnNumRecord` stored against a channel by providing a candidate with higher turn number. - * @param fixedPart Data describing properties of the state channel that do not change with state updates. - * @param proof Additional proof material (in the form of an array of signed states) which completes the support proof. - * @param candidate A candidate state (along with signatures) which is being claimed to be supported. - */ - function checkpoint( - FixedPart memory fixedPart, - SignedVariablePart[] memory proof, - SignedVariablePart memory candidate - ) external; - - /** - * @notice Finalizes a channel according to the given candidate. External wrapper for _conclude. - * @dev Finalizes a channel according to the given candidate. External wrapper for _conclude. - * @param fixedPart Data describing properties of the state channel that do not change with state updates. - * @param candidate A candidate state (along with signatures) to change to. - */ - function conclude(FixedPart memory fixedPart, SignedVariablePart memory candidate) external; - - // events - - /** - * @dev Indicates that a challenge has been registered against `channelId`. - * @param channelId Unique identifier for a state channel. - * @param finalizesAt The unix timestamp when `channelId` will finalize. - * @param proof Additional proof material (in the form of an array of signed states) which completes the support proof. - * @param candidate A candidate state (along with signatures) which is being claimed to be supported. - */ - event ChallengeRegistered( - bytes32 indexed channelId, - uint48 finalizesAt, - SignedVariablePart[] proof, - SignedVariablePart candidate - ); - - /** - * @dev Indicates that a challenge, previously registered against `channelId`, has been cleared. - * @param channelId Unique identifier for a state channel. - * @param newTurnNumRecord A turnNum that (the adjudicator knows) is supported by a signature from each participant. - */ - event ChallengeCleared(bytes32 indexed channelId, uint48 newTurnNumRecord); - - /** - * @dev Indicates that an on-chain channel data was successfully updated and now has `newTurnNumRecord` as the latest turn number. - * @param channelId Unique identifier for a state channel. - * @param newTurnNumRecord A latest turnNum that (the adjudicator knows) is supported by adhering to channel application rules. - */ - event Checkpointed(bytes32 indexed channelId, uint48 newTurnNumRecord); - - /** - * @dev Indicates that a challenge has been registered against `channelId`. - * @param channelId Unique identifier for a state channel. - * @param finalizesAt The unix timestamp when `channelId` finalized. - */ - event Concluded(bytes32 indexed channelId, uint48 finalizesAt); -} -``` - -### Workflows - -1. **Opening a Channel**: Participants agree on initial states and open a channel on-chain. -2. **Funding a Channel**: Participants transfer the agreed amount of funds. -3. **Off-Chain Interaction**: Participants exchange signed state updates off-chain. -4. **Dispute Resolution**: If a disagreement arises, a participant can force a state on-chain. -5. **Finalization**: Upon agreement or after a timeout, the channel is finalized, and the outcome is settled. - -#### Example Application - -The CountingApp contract complies with the ForceMoveApp interface and strict turn taking logic and allows only for a simple counter to be incremented. - -```solidity -contract CountingApp is IForceMoveApp { - struct CountingAppData { - uint256 counter; - } - - /** - * @notice Decodes the appData. - * @dev Decodes the appData. - * @param appDataBytes The abi.encode of a CountingAppData struct describing the application-specific data. - * @return A CountingAppData struct containing the application-specific data. - */ - function appData(bytes memory appDataBytes) internal pure returns (CountingAppData memory) { - return abi.decode(appDataBytes, (CountingAppData)); - } - - /** - * @notice Encodes application-specific rules for a particular ForceMove-compliant state channel. - * @dev Encodes application-specific rules for a particular ForceMove-compliant state channel. - * @param fixedPart Fixed part of the state channel. - * @param proof Array of recovered variable parts which constitutes a support proof for the candidate. - * @param candidate Recovered variable part the proof was supplied for. - */ - function stateIsSupported( - FixedPart calldata fixedPart, - RecoveredVariablePart[] calldata proof, - RecoveredVariablePart calldata candidate - ) external pure override returns (bool, string memory) { - StrictTurnTaking.requireValidTurnTaking(fixedPart, proof, candidate); - - require(proof.length != 0, '|proof| = 0'); - - // validate the proof - for (uint256 i = 1; i < proof.length; i++) { - _requireIncrementedCounter(proof[i], proof[i - 1]); - _requireEqualOutcomes(proof[i], proof[i - 1]); - } - - _requireIncrementedCounter(candidate, proof[proof.length - 1]); - _requireEqualOutcomes(candidate, proof[proof.length - 1]); - - return (true, ''); - } - - /** - * @notice Checks that counter encoded in first variable part equals an incremented counter in second variable part. - * @dev Checks that counter encoded in first variable part equals an incremented counter in second variable part. - * @param b RecoveredVariablePart with incremented counter. - * @param a RecoveredVariablePart with counter before incrementing. - */ - function _requireIncrementedCounter( - RecoveredVariablePart memory b, - RecoveredVariablePart memory a - ) internal pure { - require( - appData(b.variablePart.appData).counter == appData(a.variablePart.appData).counter + 1, - 'Counter must be incremented' - ); - } - - /** - * @notice Checks that supplied signed variable parts contain the same outcome. - * @dev Checks that supplied signed variable parts contain the same outcome. - * @param a First RecoveredVariablePart. - * @param b Second RecoveredVariablePart. - */ - function _requireEqualOutcomes( - RecoveredVariablePart memory a, - RecoveredVariablePart memory b - ) internal pure { - require( - Outcome.exitsEqual(a.variablePart.outcome, b.variablePart.outcome), - 'Outcome must not change' - ); - } -} -``` - -## Rationale - -State channels offer significant scalability improvements by minimizing on-chain transactions. This standard provides clear interfaces to ensure interoperability and efficiency while retaining flexibility for custom protocol rules. - -**Scalability**: State channels alleviate these concerns by moving most interactions off-chain, while the blockchain serves as a settlement layer. This construction enable high frequency applications. - -**Modular**: Interfaces act as contracts that specify what functions must be implemented without dictating how, allowing developers to create custom implementations suited to specific use cases while maintaining compatibility with the framework. - -**Interoperability**: Enable cross-chain interactions, participants can be using two differents chains for opening their channels and perform for example cross-chain atomic swaps. - -**Security**: The standard would enable to build an audited framework of primitive which is flexible to accommodate a large number of use cases. ERC-7824 is designed to accommodate this diversity by normalizing common protocol patterns. - -## Backwards Compatibility - -No backward compatibility issues found. This ERC is designed to coexist with existing standards and can integrate with [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) and [ERC-4337](https://www.erc4337.io/) - -## Security Considerations - -This ERC is agnostic of the protocol rules that must be implemented using IForceMoveApp. While the smart-contract framework is a simple set of convention, much of the security risk is moved off-chain. Protocols using State channels must perform security audit of their client and server backend implementation. - -## Copyright - -Copyright and related rights waived via [CC0](https://github.com/ethereum/ERCs/blob/master/LICENSE.md). diff --git a/erc7824-docs/docs/erc-7824.md b/erc7824-docs/docs/erc-7824.md deleted file mode 100644 index f58d4542d..000000000 --- a/erc7824-docs/docs/erc-7824.md +++ /dev/null @@ -1,498 +0,0 @@ ---- -sidebar_position: 2 -title: ERC-7824 -description: Interfaces and data types for cross-chain stateful asset transfer -keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, javascript, typescript, sdk] ---- -# ERC-7824 - -## Abstract - -Nitrolite is a lightweight, efficient state channel framework for Ethereum and other EVM-compatible blockchains, enabling off-chain interactions while maintaining on-chain security guarantees. The framework allows participants to perform instant transactions with reduced gas costs while preserving the security of the underlying blockchain. - -This standard defines a framework for implementing state channel systems through the Nitrolite protocol, providing interfaces for channel creation, state management, dispute resolution, and fund custody. It enables high-throughput applications with minimal on-chain footprint. - -## Motivation - -The Ethereum network faces challenges in scalability and transaction costs, making it less feasible for high-frequency interactions. The Nitrolite framework addresses these challenges by providing a lightweight state channel solution that enables: - -- **Instant Finality**: Transactions settle immediately between parties -- **Reduced Gas Costs**: Most interactions happen off-chain, with minimal on-chain footprint -- **High Throughput**: Support for thousands of transactions per second -- **Security Guarantees**: Same security as on-chain, with cryptographic proofs -- **Chain Agnostic**: Works with any EVM-compatible blockchain - -This ERC standardizes the Nitrolite protocol interfaces to facilitate widespread adoption of state channels. - -## Specification - -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. - -### Glossary of Terms - -- **Channel**: A relationship between participants that allows off-chain state updates with on-chain settlement -- **Channel ID**: A unique identifier derived from channel configuration (participants, adjudicator, challenge period, nonce) -- **Participant**: An entity (EOA) involved in a state channel -- **State**: A signed data structure containing version, allocations, and application data -- **Allocation**: Specification of token distribution to destinations -- **Adjudicator**: Contract that validates state transitions according to application rules -- **Challenge Period**: Duration for dispute resolution before finalization -- **Status**: Channel lifecycle stage (VOID, INITIAL, ACTIVE, DISPUTE, FINAL) -- **State Intent**: Purpose of a state (OPERATE, INITIALIZE, RESIZE, FINALIZE) -- **Custody**: On-chain contract holding locked funds for channels -- **Checkpoint**: Recording a valid state on-chain without closing the channel - -### Data Structures - -The Nitrolite protocol defines the following core data structures: - -#### Basic Types - -```solidity -struct Amount { - address token; // ERC-20 token address (address(0) for native tokens) - uint256 amount; // Token amount -} - -struct Allocation { - address destination; // Where funds are sent on channel closure - address token; // ERC-20 token contract address (address(0) for native tokens) - uint256 amount; // Token amount allocated -} -``` - -#### Channel Configuration - -```solidity -struct Channel { - address[] participants; // List of participants in the channel - address adjudicator; // Address of the contract that validates state transitions - uint64 challenge; // Duration in seconds for dispute resolution period - uint64 nonce; // Unique per channel with same participants and adjudicator -} -``` - -#### State Structure - -```solidity -struct State { - StateIntent intent; // Intent of the state - uint256 version; // State version incremental number to compare most recent - bytes data; // Application data encoded, decoded by the adjudicator - Allocation[] allocations; // Asset allocation and destination for each participant - bytes[] sigs; // stateHash signatures from participants -} - -enum StateIntent { - OPERATE, // Normal operation state - INITIALIZE, // Initial funding state - RESIZE, // Resize allocations state - FINALIZE // Final closing state -} -``` - -#### Channel Status - -```solidity -enum Status { - VOID, // Channel was not created, State.version must be 0 - INITIAL, // Channel is created and in funding process, State.version must be 0 - ACTIVE, // Channel fully funded and operational, State.version > 0 - DISPUTE, // Challenge period is active - FINAL // Final state, channel can be closed -} -``` - -#### Channel ID - -The channel ID is computed as: - -```solidity -bytes32 channelId = keccak256( - abi.encode( - channel.participants, - channel.adjudicator, - channel.challenge, - channel.nonce - ) -); -``` - -#### State Hash - -For signature verification, the state hash is computed as: - -```solidity -bytes32 stateHash = keccak256( - abi.encode( - channelId, - state.intent, - state.version, - state.data, - state.allocations - ) -); -``` - -Note: The smart contract supports all popular signature formats, specifically: raw ECDSA, EIP-191, EIP-712, EIP-1271, and EIP-6492. - -### Interfaces - -#### IAdjudicator - -Defines the interface for contracts that validate state transitions according to application-specific rules: - -```solidity -interface IAdjudicator { - /** - * @notice Validates a candidate state based on application-specific rules - * @dev Used to determine if a state is valid during challenges or checkpoints - * @param chan The channel configuration - * @param candidate The proposed state to be validated - * @param proofs Array of previous states that provide context for validation - * @return valid True if the candidate state is valid according to application rules - */ - function adjudicate( - Channel calldata chan, - State calldata candidate, - State[] calldata proofs - ) external returns (bool valid); -} -``` - -#### IComparable - -Interface for determining the ordering between states: - -```solidity -interface IComparable { - /** - * @notice Compares two states to determine their relative ordering - * @dev Returns: -1 if candidate < previous, 0 if equal, 1 if candidate > previous - * @param candidate The state being evaluated - * @param previous The reference state to compare against - * @return result The comparison result - */ - function compare( - State calldata candidate, - State calldata previous - ) external view returns (int8 result); -} -``` - -#### IChannel - -The main state channel interface that manages the channel lifecycle: - -```solidity -interface IChannel { - // Events - event Created(bytes32 indexed channelId, Channel channel, State initial); - event Joined(bytes32 indexed channelId, uint256 index); - event Opened(bytes32 indexed channelId); - event Challenged(bytes32 indexed channelId, uint256 expiration); - event Checkpointed(bytes32 indexed channelId); - event Resized(bytes32 indexed channelId, int256[] deltaAllocations); - event Closed(bytes32 indexed channelId); - - /** - * @notice Creates a new channel and initializes funding - * @dev Creator must sign the funding state with StateIntent.INITIALIZE. - * If both participants sign the initial state, channel opens immediately in ACTIVE status. - * If only creator signs, channel enters INITIAL status awaiting join(). - * @param ch Channel configuration - * @param initial Initial state with StateIntent.INITIALIZE and expected allocations - * @return channelId Unique identifier for the created channel - */ - function create(Channel calldata ch, State calldata initial) - external returns (bytes32 channelId); - - /** - * @notice Allows a participant to join a channel by signing the funding state - * @dev Only needed when channel was created with single signature. - * Participant must provide signature on the same funding state. - * @param channelId Unique identifier for the channel - * @param index Index of the participant in the channel's participants array - * @param sig Signature of the participant on the funding state - * @return channelId Unique identifier for the joined channel - */ - function join(bytes32 channelId, uint256 index, bytes calldata sig) - external returns (bytes32); - - /** - * @notice Finalizes a channel with a mutually signed closing state - * @dev Requires all participants' signatures on a state with StateIntent.FINALIZE - * @param channelId Unique identifier for the channel - * @param candidate The latest known valid state to be finalized - * @param proofs Additional states required by the adjudicator - */ - function close( - bytes32 channelId, - State calldata candidate, - State[] calldata proofs - ) external; - - /** - * @notice Resizes channel allocations with participant agreement - * @dev Used for adjusting channel allocations without withdrawing funds - * @param channelId Unique identifier for the channel - * @param candidate The state with new allocations - * @param proofs Supporting states for validation - */ - function resize( - bytes32 channelId, - State calldata candidate, - State[] calldata proofs - ) external; - - /** - * @notice Initiates or updates a challenge with a signed state - * @dev Starts a challenge period during which participants can respond - * @param channelId Unique identifier for the channel - * @param candidate The state being submitted as the latest valid state - * @param proofs Additional states required by the adjudicator - * @param challengerSig Signature of the challenger on the candidate state. Must be signed by one of the participants - */ - function challenge( - bytes32 channelId, - State calldata candidate, - State[] calldata proofs, - bytes calldata challengerSig - ) external; - - /** - * @notice Records a valid state on-chain without initiating a challenge - * @dev Used to establish on-chain proof of the latest state - * @param channelId Unique identifier for the channel - * @param candidate The state to checkpoint - * @param proofs Additional states required by the adjudicator - */ - function checkpoint( - bytes32 channelId, - State calldata candidate, - State[] calldata proofs - ) external; -} -``` - -#### IDeposit - -Interface for managing token deposits and withdrawals: - -```solidity -interface IDeposit { - /** - * @notice Deposits tokens into the contract - * @dev For native tokens, the value should be sent with the transaction - * @param wallet Address of the account whose ledger is changed - * @param token Token address (use address(0) for native tokens) - * @param amount Amount of tokens to deposit - */ - function deposit(address wallet, address token, uint256 amount) external payable; - - /** - * @notice Withdraws tokens from the contract - * @dev Can only withdraw available (not locked in channels) funds - * @param wallet Address of the account whose ledger is changed - * @param token Token address (use address(0) for native tokens) - * @param amount Amount of tokens to withdraw - */ - function withdraw(address wallet, address token, uint256 amount) external; -} -``` - -### Channel Lifecycle - -1. **Creation**: Creator constructs channel config and signs initial state with `StateIntent.INITIALIZE`. The channel can be opened immediately in one transaction if both participants provide signatures over the initial state, with both participants' funds being deducted from their available balances. -2. **Active**: Once fully funded (either through single-transaction creation or separate join), the channel transitions to active state for off-chain operation -3. **Off-chain Updates**: Participants exchange and sign state updates according to application logic -4. **Resolution**: - - **Cooperative Close**: All parties sign a final state with `StateIntent.FINALIZE` - - **Challenge-Response**: Participant can post a state on-chain and initiate challenge period - - **Checkpoint**: Record valid state on-chain without closing for future dispute resolution - - **Resize**: Adjust allocations by agreement without closing the channel - -#### Two-Phase Opening (Legacy) - -For scenarios requiring separate funding from external accounts: - -1. **Creation**: Creator calls `create()` with single signature, channel enters INITIAL status -2. **Joining**: Second participant calls `join()` with their signature, transitioning channel to ACTIVE status - -### Example Implementation: Remittance Adjudicator - -The Remittance adjudicator validates payment transfers between participants: - -```solidity -contract Remittance is IAdjudicator, IComparable { - struct Intent { - uint8 payer; // Index of the paying participant - Amount transfer; // Amount and token being transferred - } - - /** - * @notice Validates a payment state transition - * @dev Checks that the payer has signed and allocations are correct - */ - function adjudicate( - Channel calldata chan, - State calldata candidate, - State[] calldata proofs - ) external returns (bool valid) { - // Decode the payment intent - Intent memory intent = abi.decode(candidate.data, (Intent)); - - // For first state (version 1), need funding state as proof - if (candidate.version == 1) { - require(proofs.length >= 1, "Missing funding state"); - State memory funding = proofs[0]; - require(funding.intent == StateIntent.INITIALIZE, "Invalid funding state"); - } - - // For subsequent states, need previous state - if (candidate.version > 1) { - require(proofs.length >= 2, "Missing previous state"); - State memory previous = proofs[1]; - - // Verify state transition - require(candidate.version == previous.version + 1, "Invalid version"); - - // Verify allocations match the intent - require( - candidate.allocations[intent.payer].amount == - previous.allocations[intent.payer].amount - intent.transfer.amount, - "Invalid payer allocation" - ); - - uint8 payee = intent.payer == 0 ? 1 : 0; - require( - candidate.allocations[payee].amount == - previous.allocations[payee].amount + intent.transfer.amount, - "Invalid payee allocation" - ); - } - - // Verify payer has signed - require(candidate.sigs[intent.payer].v != 0, "Missing payer signature"); - - return true; - } - - /** - * @notice Compares states by version number - */ - function compare( - State calldata candidate, - State calldata previous - ) external view returns (int8 result) { - if (candidate.version < previous.version) return -1; - if (candidate.version > previous.version) return 1; - return 0; - } -} -``` - -### Example Usage - -```solidity -// Create a channel between Alice and Bob -Channel memory channel = Channel({ - participants: [alice, bob], - adjudicator: address(remittanceAdjudicator), - challenge: 3600, // 1 hour challenge period - nonce: 1 -}); - -// Create initial funding state -State memory fundingState = State({ - intent: StateIntent.INITIALIZE, - version: 0, - data: "", - allocations: [ - Allocation(alice, tokenAddress, 100 ether), - Allocation(bob, tokenAddress, 50 ether) - ], - sigs: [aliceSignature, bobSignature] -}); - -// Alice creates and funds the channel - opens immediately since both signatures provided -bytes32 channelId = custody.create(channel, fundingState); - -// Alternative: Single-signature creation followed by join -State memory fundingStatePartial = State({ - intent: StateIntent.INITIALIZE, - version: 0, - data: "", - allocations: [ - Allocation(alice, tokenAddress, 100 ether), - Allocation(bob, tokenAddress, 50 ether) - ], - sigs: [aliceSignature] // Only Alice's signature -}); -bytes32 channelId = custody.create(channel, fundingStatePartial); -custody.join(channelId, 1, bobSignature); - -// Off-chain: Alice pays Bob 10 tokens -Intent memory paymentIntent = Intent({ - payer: 0, // Alice is payer - transfer: Amount(tokenAddress, 10 ether) -}); - -State memory paymentState = State({ - intent: StateIntent.OPERATE, - version: 1, - data: abi.encode(paymentIntent), - allocations: [ - Allocation(alice, tokenAddress, 90 ether), - Allocation(bob, tokenAddress, 60 ether) - ], - sigs: [aliceSignature, bobSignature] -}); - -// Either party can checkpoint this state on-chain -custody.checkpoint(channelId, paymentState, [fundingState]); -``` - -## Rationale - -The Nitrolite framework addresses critical blockchain scalability challenges through a lightweight state channel design. This standard provides clear interfaces to ensure interoperability while maintaining flexibility for diverse applications. - -**Efficiency**: Nitrolite minimizes on-chain footprint by requiring only essential operations (create, join, close) to occur on-chain, with all application logic executed off-chain. This enables instant finality and dramatically reduces gas costs. - -**Modularity**: The separation of concerns between the custody contract (IChannel), state validation (IAdjudicator), and fund management (IDeposit) allows developers to implement custom application logic while leveraging battle-tested infrastructure. - -**Flexibility**: The adjudicator pattern supports any application logic - from simple payments to complex multi-step protocols. The state intent system (INITIALIZE, OPERATE, RESIZE, FINALIZE) provides clear semantics for different operation types. - -**Security**: The challenge-response mechanism ensures that participants can always recover their funds by posting the latest valid state on-chain. The checkpoint feature allows proactive security by recording states without closing channels. - -**Chain Agnostic**: The protocol design avoids chain-specific features, enabling deployment across any EVM-compatible blockchain and facilitating cross-chain applications through the clearnode architecture. - -## Backwards Compatibility - -No backward compatibility issues found. This ERC is designed to coexist with existing standards and can integrate with [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) and [ERC-4337](https://www.erc4337.io/) - -## Security Considerations - -### On-Chain Security - -- **Signature Verification**: All state transitions require valid signatures from participants. The protocol supports signatures of all popular formats, including EIP-191 and EIP-712. -- **Challenge Period**: The configurable challenge duration provides time for honest participants to respond to invalid states. -- **Adjudicator Validation**: Custom adjudicators must be carefully audited as they control state transition rules. -- **Reentrancy Protection**: Implementation should follow checks-effects-interactions pattern, especially in fund distribution. - -### Off-Chain Security - -- **State Storage**: Participants must securely store all signed states as they may need them for disputes. -- **Signature Security**: Private keys used for state signing must be protected as compromise allows unauthorized state transitions. -- **Availability**: Participants must monitor the chain for challenges during the challenge period. -- **Front-running**: Challenge transactions may be front-run; implementations should consider commit-reveal schemes if needed. - -### Implementation Considerations - -- The custody contract strictly enforces 2-participant channels in the reference implementation. -- Adjudicators should validate all state transitions according to application rules. -- Proper nonce management prevents replay attacks across different channels. - -## Copyright - -Copyright and related rights waived via [CC0](https://github.com/ethereum/ERCs/blob/master/LICENSE.md). diff --git a/erc7824-docs/docs/guides/index.md b/erc7824-docs/docs/guides/index.md deleted file mode 100644 index aab9eff73..000000000 --- a/erc7824-docs/docs/guides/index.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -sidebar_position: 1 -title: Guides -description: Comprehensive guides for working with Nitrolite and the ERC-7824 standard -keywords: [guides, tutorials, migration, best practices, nitrolite, erc7824] ---- - -import { Card, CardGrid } from '@site/src/components/Card'; - -# Guides - -Welcome to the Nitrolite guides section. These comprehensive resources will help you understand and implement various aspects of the ERC-7824 protocol and Nitrolite SDK. - - - - - -## Guide Categories - -### Getting Started -For developers new to Nitrolite, we recommend starting with our [Quick Start](/quick_start) guide, which covers the fundamental concepts and basic implementation. - -### Migration & Upgrades -The [Migration Guide](migration-guide) provides detailed information about breaking changes between versions and how to update your code accordingly. This is essential reading when upgrading your Nitrolite dependencies. - -### Coming Soon - -We're continuously expanding our documentation. Future guides will include: - -- **Best Practices**: Optimization techniques and design patterns for state channel applications -- **Security Guide**: Security considerations and audit recommendations -- **Performance Tuning**: Maximizing throughput and minimizing latency -- **Integration Examples**: Real-world examples of integrating Nitrolite with popular frameworks -- **Troubleshooting**: Common issues and their solutions - -## Contributing - -Have a guide idea or found an issue? We welcome contributions! Please visit our [GitHub repository](https://github.com/erc7824/nitrolite) to submit suggestions or improvements. \ No newline at end of file diff --git a/erc7824-docs/docs/guides/migration-guide.md b/erc7824-docs/docs/guides/migration-guide.md deleted file mode 100644 index 39c8bf862..000000000 --- a/erc7824-docs/docs/guides/migration-guide.md +++ /dev/null @@ -1,930 +0,0 @@ ---- -sidebar_position: 2 -title: Migration Guide -description: Guide to migrate to newer versions of Nitrolite -keywords: [migration, upgrade, breaking changes, nitrolite, erc7824] ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Migration Guide - -If you are coming from an earlier version of Nitrolite, you will need to account for the following breaking changes. - -## 0.5.x Breaking changes - -The 0.5.x release includes fundamental protocol changes affecting session keys, channel operations, state signatures, and channel resize rules. The main objective of these changes is to enhance security, and provide better experience for developers and users by ability to limit allowances for specific applications. - -**Not ready to migrate?** Unfortunately, at this time Yellow Network does not provide ClearNodes running the previous version of the protocol, so you will need to migrate to the latest version to continue using the Network. - -### Protocol Changes - -These protocol-level changes affect all implementations and integrations with the Yellow Network. - -#### Session Keys: Applications, Allowances, and Expiration - -Session keys now have enhanced properties that define their access levels and capabilities: - -- **Application field**: Determines the scope of session key permissions. Setting this to an application name (e.g., "My Trading App") grants application-scoped access with enforced allowances. Setting it to "clearnode" grants root access equivalent to the wallet itself. - -- **Allowances field**: Defines spending limits for application-scoped session keys. These limits are tracked cumulatively across all operations and are enforced by the protocol. - -- **Expires_at field**: Uses a bigint timestamp (seconds since epoch). Once expired, session keys are permanently frozen and cannot be reactivated. This is particularly critical for root access keys (application set to "clearnode") - if they expire, you lose the ability to perform channel operations. - -#### Channel Creation: Separate Create and Fund Steps - -Clearnode no longer supports creating channels with an initial deposit. All channels must be created with zero balance and funded separately through a resize operation. This two-step process ensures cleaner state management and prevents edge cases in channel initialization. - -#### State Signatures: Wallet vs Session Key Signing - -A fundamental change in how channel states are signed: - -- **Channels created before v0.5.0**: The participant address is the session key, and all states must be signed by that session key. - -- **Channels created after v0.5.0**: The participant address is the wallet address, and all states must be signed by the wallet. - -This change improves security and aligns with standard practices, but requires careful handling during the transition period. - -#### Resize Operations: Strict Channel Balance Rules - -The protocol now enforces strict rules about channel balances and their impact on other operations: - -- **Blocked operations**: Users with any channel containing non-zero amounts cannot perform transfers, submit app states with deposit intent, or create app sessions with non-zero allocations. - -- **Resizing state**: After a resize request, channels enter a "resizing" state with locked funds until the on-chain transaction is confirmed. If a channel remains stuck in this state for an extended period, the recommended action is to close the channel and create a new one. - -- **Allocate amount semantics**: The resize operation uses `allocate_amount` where negative values withdraw from the channel to unified balance, and positive values deposit to the channel. - -:::warning -**Legacy channel migration**: Users with existing channels containing non-zero amounts must either resize them to zero (by providing "resize_amount" as 0 and "allocate_amount" as your **negative** on-chain balance) or close them to enable full protocol functionality. If you are unsure how to adjust resize parameters, the safe option is to close the old on-chain channel entirely, and open a new one. -::: - -#### Non-Zero Channel Allocations: Operation Restrictions - -The following operations will return errors if the user has any channel with non-zero amount: - -- **Transfer**: Returns error code indicating blocked due to non-zero channel balance -- **Submit App State** (with deposit intent): Rejected if attempting to deposit -- **Create App Session** (with allocations): Rejected if attempting to allocate - -The returned error has the following format: `operation denied: non-zero allocation in channel(s) detected owned by wallet
"` - -### Nitrolite SDK - -You should definitely read this section if you are using the Nitrolite SDK. - -#### Update Authentication - -Implementing the new session key protocol changes: - - - - - ```typescript - const authRequest = { - address: '0x...', - session_key: '0x...', - application: 'My Trading App', // Application name for confined access - allowances: [ - { asset: 'usdc', amount: '1000.0' }, - { asset: 'eth', amount: '0.5' } - ], - scope: 'app.create', - expires_at: BigInt(Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60) // 7 days - }; - ``` - - - - - ```typescript - const authRequest = { - address: '0x...', - session_key: '0x...', - application: 'clearnode', // Special value for root access - allowances: [], // Not enforced for root access - scope: 'app.create', - expires_at: BigInt(Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60) // Long expiration recommended - }; - ``` - - - - -**Important considerations:** -- Root access keys (application: "clearnode") cannot perform channel operations after expiration -- Plan expiration times based on your operational needs -- Application-scoped keys track cumulative spending against allowances - -#### Migrate Channel Creation - -Channels must now be created with zero initial deposit and funded separately via the `resizeChannel` method: - -```typescript -const { channelId } = await client.createChannel({ - chain_id: 1, - token: tokenAddress, - // remove-next-line - amount: BigInt(1000000), // Initial deposit - // remove-next-line - session_key: '0x...' // Optional -}); - -// add-start -// Step 2: Fund the channel separately -await client.resizeChannel({ - channel_id: channelId, - amount: BigInt(1000000), -}); -// add-end -``` - -#### Resize correctly - -Channel resizing must be negotiated with the ClearNode through WebSocket. Use `resize_amount` and `allocate_amount` with correct sign convention (`resize_amount = -allocate_amount`) and help users with non-zero channel balances migrate by resizing to zero or reopening channels. - -Channel resize can be requested as follows: - -```typescript -const resizeMessage = await createResizeChannelMessage(messageSigner, { - channel_id: channelId, - resize_amount: BigInt(50), // Positive = deposit to channel, negative = withdraw from channel to custody ledger - allocate_amount: BigInt(-50), // Negative = deposit to unified balance, negative = withdraw from unified balance to channel - funds_destination: walletAddress, -}); - -const resizeResponse = {}; // send the message and wait for Clearnode's response - -const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); -const resizeParams = { - resizeState: { - channelId, - ...resizeResponseParams.state, - serverSignature: resizeResponseParams.serverSignature, - data: resizeResponseParams.state.stateData as Hex, - version: BigInt(resizeResponseParams.state.version), - }, - // `previousState` is either initial or previous resizing state, depending on which has higher version number - // can be obtained with `await (client.getChannelData(channelId)).lastValidState` - proofStates: [previousState], -} - -const {txHash} = await client.resizeChannel(resizeParams); -``` - -Here is how you can migrate your channels: - -```typescript -// Check and migrate channels with non-zero amounts -const channels = await client.getOpenChannels(); - -for (const channel of channels) { - if (channel.amount > 0) { - // Must empty channel to enable transfers/app operations - const resizeMessage = await createResizeChannelMessage(messageSigner, { - channel_id: channel.channelId, - resize_amount: BigInt(0), - allocate_amount: -BigInt(channel.amount), - funds_destination: walletAddress, - }); - - // perform the resize as shown above - } -} -``` - - -**Critical:** Operations blocked when any channel has non-zero amount: -- Off-chain transfers -- App state submissions with deposit intent -- Creating app sessions with allocations - -#### Test State Signatures - -If you plan to work with on-chain channels opened PRIOR to v0.5.0, then on NitroliteClient initialization the `stateSigner` you specify must be based on a Session Key used in the channel as participant. Even if this session key is or will expire, you still need to provide a `stateSigner` based on it. - -On the other hand, if you plan to work with channels created SINCE v0.5.0, you can specify the `stateSigner` based on the `walletClient` you have specified. - -#### Manage Session Keys - -New methods have been added for comprehensive session key management, including retrieval and revocation. - -```typescript -// Get all active session keys -const sessionKeys = await client.getSessionKeys(); - -// Revoke a specific session key -await client.revokeSessionKey({ - session_key: '0x...' -}); - -// Session key data structure -interface RPCSessionKey { - id: string; - sessionKey: Address; - application: string; - allowances: RPCAllowanceUsage[]; // Includes usage tracking - scope: string; - expiresAt: bigint; - createdAt: bigint; -} -``` - -#### EIP-712 Signatures: String-based Amounts - -EIP-712 signature types now use string values for amounts instead of numeric types to support better precision with decimal values. - -```typescript -const types = { - Allowance: [ - { name: 'asset', type: 'string' }, - // remove-next-line - { name: 'amount', type: 'uint256' }, - // add-next-line - { name: 'amount', type: 'string' }, - ] -}; -``` - -### ClearNode API - -You should read this section only if you are using the ClearNode API directly. - -#### Update Authentication - -Use the new session key parameters with proper `application`, `allowances`, and `expires_at` fields: - - - - - ```json - { - "req": [1, "auth_request", { - "address": "0x1234567890abcdef...", - "session_key": "0x9876543210fedcba...", - "application": "My Trading App", - "allowances": [ - { "asset": "usdc", "amount": "1000.0" }, - { "asset": "eth", "amount": "0.5" } - ], - "scope": "app.create", - "expires_at": 1719123456789 - }, 1619123456789], - "sig": ["0x..."] - } - ``` - - - - - ```json - { - "req": [1, "auth_request", { - "address": "0x1234567890abcdef...", - "session_key": "0x9876543210fedcba...", - "application": "clearnode", - "allowances": [], - "scope": "app.create", - "expires_at": 1750659456789 - }, 1619123456789], - "sig": ["0x..."] - } - ``` - - - - -#### Migrate Channel Creation - -Implement the two-step process (create empty, then resize to fund) - -The `create_channel` method no longer accepts `amount` and `session_key` parameters: - -```json -{ - "req": [1, "create_channel", { - "chain_id": 137, - "token": "0xeeee567890abcdef...", - // remove-next-line - "amount": "100000000", - // remove-next-line - "session_key": "0x1234567890abcdef..." - }, 1619123456789], - "sig": ["0x9876fedcba..."] -} -``` - -#### Manage Session Keys - -New methods for session key operations have been added. - -##### Get Session Keys - -Request: -```json -{ - "req": [1, "get_session_keys", {}, 1619123456789], - "sig": ["0x..."] -} -``` - -Response: -```json -{ - "res": [1, "get_session_keys", { - "session_keys": [{ - "id": "sk_123", - "session_key": "0x9876543210fedcba...", - "application": "My Trading App", - "allowances": [ - { "asset": "usdc", "amount": "1000.0", "used": "250.0" } - ], - "scope": "app.create", - "expires_at": 1719123456789, - "created_at": 1619123456789 - }] - }, 1619123456789], - "sig": ["0x..."] -} -``` - -##### Revoke Session Key Request - -Request: -```json -{ - "req": [1, "revoke_session_key", { - "session_key": "0x1234567890abcdef..." - }, 1619123456789], - "sig": ["0x..."] -} -``` - -Response: -```json -{ - "res": [1, "revoke_session_key", { - "session_key": "0x1234567890abcdef..." - }, 1619123456789], - "sig": ["0x..."] -} -``` - -## 0.3.x Breaking changes - -The 0.3.x release includes breaking changes to the SDK architecture, smart contract interfaces, and Clearnode API enhancements listed below. - -**Not ready to migrate?** Unfortunately, at this time Yellow Network does not provide ClearNodes running the previous version of the protocol, so you will need to migrate to the latest version to continue using the Network. - -### Nitrolite SDK - -You should definitely read this section if you are using the Nitrolite SDK. - -#### Client: Replaced `stateWalletClient` with `StateSigner` - -The `stateWalletClient` parameter of `NitroliteClient` has been replaced with a required `stateSigner` parameter that implements the `StateSigner` interface. - -When initializing the client, you should use either `WalletStateSigner` or `SessionKeyStateSigner` to handle state signing. - -```typescript -// remove-next-line -import { createNitroliteClient } from '@erc7824/nitrolite'; -// add-start -import { - createNitroliteClient, - WalletStateSigner -} from '@erc7824/nitrolite'; -// add-end - -const client = createNitroliteClient({ - publicClient, - walletClient, - // remove-next-line - stateWalletClient: sessionWalletClient, - // add-next-line - stateSigner: new WalletStateSigner(walletClient), - addresses, -}); -``` - -**For session key signing:** - -```typescript -import { SessionKeyStateSigner } from '@erc7824/nitrolite'; - -const stateSigner = new SessionKeyStateSigner('0x...' as Hex); -``` - -#### Actions: Modified `createChannel` Parameters - -The `CreateChannelParams` interface has been fully restructured for better clarity. - -You should use the new [`CreateChannel` ClearNode API endpoint](#added-create_channel-method) to get the response, that fully resembles the channel creation parameters. - -```typescript -// remove-start -const { channelId, initialState, txHash } = await client.createChannel( - tokenAddress, - { - initialAllocationAmounts: [amount1, amount2], - stateData: '0x...', - } -); -// remove-end -// add-start -const { channelId, initialState, txHash } = await client.createChannel({ - channel: { - participants: [address1, address2], - adjudicator: adjudicatorAddress, - challenge: 86400n, - nonce: 42n, - }, - unsignedInitialState: { - intent: StateIntent.Initialize, - version: 0n, - data: '0x', - allocations: [ - { destination: address1, token: tokenAddress, amount: amount1 }, - { destination: address2, token: tokenAddress, amount: amount2 }, - ], - }, - serverSignature: '0x...', -}); -// add-end -``` - -#### Actions: Structured Typed RPC Request Parameters - -RPC requests now use endpoint-specific object-based parameters instead of untyped arrays for improved type safety. - -You should update your RPC request creation code to use the new structured format and RPC types. - -```typescript -// remove-start -const request = NitroliteRPC.createRequest( - requestId, - RPCMethod.GetChannels, - [participant, status], - timestamp -); -// remove-end -// add-start -const request = NitroliteRPC.createRequest({ - method: RPCMethod.GetChannels, - params: { - participant, - status, - }, - requestId, - timestamp, -}); -// add-end -``` - -#### Actions: Standardized Channel Operations Responses - -The responses for `CloseChannel` and `ResizeChannel` methods have been aligned with newly added `CreateChannel` endpoint for consistency. - -Update your response handling code to use the new `RPCChannelOperation` type. - -```typescript -// remove-start -export interface ResizeChannelResponseParams { - channelId: Hex; - stateData: Hex; - intent: number; - version: number; - allocations: RPCAllocation[]; - stateHash: Hex; - serverSignature: ServerSignature; -} - -export interface CloseChannelResponseParams { - channelId: Hex; - intent: number; - version: number; - stateData: Hex; - allocations: RPCAllocation[]; - stateHash: Hex; - serverSignature: ServerSignature; -} -// remove-end -// add-start -export interface RPCChannelOperation { - channelId: Hex; - state: RPCChannelOperationState; - serverSignature: Hex; -} - -export interface CreateChannelResponse extends GenericRPCMessage { - method: RPCMethod.CreateChannel; - params: RPCChannelOperation & { - channel: RPCChannel; - }; -} - -export interface ResizeChannelResponse extends GenericRPCMessage { - method: RPCMethod.ResizeChannel; - params: RPCChannelOperation; -} - -export interface CloseChannelResponse extends GenericRPCMessage { - method: RPCMethod.CloseChannel; - params: RPCChannelOperation; -} -// add-end -``` - -#### Actions: Modified `Signature` Type - -The `Signature` struct has been replaced with a simple `Hex` type to support EIP-1271 and EIP-6492 signatures. - -Update your signature-handling code to use the new `Hex` type. Still, if using Nitrolite utils correctly, you will not need to change anything, as the utils will handle the conversion for you. - -```typescript -// remove-start -interface Signature { - v: number; - r: Hex; - s: Hex; -} - -const sig: Signature = { - v: 27, - r: '0x...', - s: '0x...' -}; -// remove-end -// add-start -type Signature = Hex; - -const sig: Signature = '0x...'; -// add-end -``` - -#### Added: Pagination Types and Parameters - -To support pagination in ClearNode API requests, new types and parameters have been added. - -For now, only `GetLedgerTransactions` request has been updated to include pagination. - -```typescript -export interface PaginationFilters { - /** Pagination offset. */ - offset?: number; - /** Number of transactions to return. */ - limit?: number; - /** Sort order by created_at. */ - sort?: 'asc' | 'desc'; -} -``` - -### Clearnode API - -You should read this section only if you are using the ClearNode API directly, or if you are using the Nitrolite SDK with custom ClearNode API requests. - -#### Actions: Structured Request Parameters - -ClearNode API requests have migrated from array-based parameters to structured object parameters for improved type safety and API clarity. - -Update all your ClearNode API requests to use object-based parameters instead of arrays. - -```json -{ - // remove-next-line - "req": [1, "auth_request", [{ - // add-next-line - "req": [1, "auth_request", { - "address": "0x1234567890abcdef...", - "session_key": "0x9876543210fedcba...", - "app_name": "Example App", - // remove-next-line - "allowances": [ "usdc", "100.0" ], - // add-start - "allowances": [ - { - "asset": "usdc", - "amount": "100.0" - } - ], - // add-end - "scope": "app.create", - "expire": "3600", - "application": "0xApp1234567890abcdef..." - // remove-next-line - }], 1619123456789], - // add-next-line - }, 1619123456789], - "sig": ["0x5432abcdef..."] -} -``` - -#### Added: `create_channel` Method - -A new `create_channel` method has been added to facilitate the improved single-transaction channel opening flow. - -Use this method to request channel creation parameters from the broker, then submit the returned data to the smart contract via Nitrolite SDK or directly. - -**Request:** -```json -{ - "req": [1, "create_channel", { - "chain_id": 137, - "token": "0xeeee567890abcdef...", - "amount": "100000000", - "session_key": "0x1234567890abcdef..." // Optional - }, 1619123456789], - "sig": ["0x9876fedcba..."] -} -``` - -**Response:** -```json -{ - "res": [1, "create_channel", { - "channel_id": "0x4567890123abcdef...", - "channel": { - "participants": ["0x1234567890abcdef...", "0xbbbb567890abcdef..."], - "adjudicator": "0xAdjudicatorContractAddress...", - "challenge": 3600, - "nonce": 1619123456789 - }, - "state": { - "intent": 1, - "version": 0, - "state_data": "0xc0ffee", - "allocations": [ - { - "destination": "0x1234567890abcdef...", - "token": "0xeeee567890abcdef...", - "amount": "100000000" - }, - { - "destination": "0xbbbb567890abcdef...", - "token": "0xeeee567890abcdef...", - "amount": "0" - } - ] - }, - "server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c" - }, 1619123456789], - "sig": ["0xabcd1234..."] -} -``` - -#### API: Standardized Channel Operation Responses - -The responses for `create_channel`, `close_channel`, and `resize_channel` methods have been unified for consistency. - -Update your response parsing to handle the new unified structure with `channel_id`, `state`, and `server_signature` fields. - -```json -// remove-start -{ - "res": [1, "close_channel", { - "channelId": "0x4567890123abcdef...", - "intent": 3, - "version": 123, - "stateData": "0x0000000000000000000000000000000000000000000000000000000000001ec7", - "allocations": [...], - "stateHash": "0x...", - "serverSignature": "0x..." - }, 1619123456789], - "sig": ["0xabcd1234..."] -} -// remove-end -// add-start -{ - "res": [1, "close_channel", { - "channel_id": "0x4567890123abcdef...", - "state": { - "intent": 3, - "version": 123, - "state_data": "0xc0ffee", - "allocations": [ - { - "destination": "0x1234567890abcdef...", - "token": "0xeeee567890abcdef...", - "amount": "50000" - } - ] - }, - "server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c" - }, 1619123456789], - "sig": ["0xabcd1234..."] -} -// add-end -``` - -#### Added: Pagination Metadata - -Pagination-supporting endpoints now include a `metadata` struct in their responses with pagination information. - -Update your response handling for `get_channels`, `get_app_sessions`, `get_ledger_entries`, and `get_ledger_transactions` to use the new metadata structure. - -```json -// remove-start -{ - "res": [1, "get_channels", [ - [ - { - "channel_id": "0xfedcba9876543210...", - "status": "open", - // ... channel data - } - ] - ], 1619123456789], - "sig": ["0xabcd1234..."] -} -// remove-end -// add-start -{ - "res": [1, "get_channels", { - "channels": [ - { - "channel_id": "0xfedcba9876543210...", - "status": "open", - // ... channel data - } - ], - "metadata": { - "page": 1, - "per_page": 10, - "total_count": 56, - "page_count": 6 - } - }, 1619123456789], - "sig": ["0xabcd1234..."] -} -// add-end -``` - -The metadata fields provide: -- `page`: Current page number -- `per_page`: Number of items per page -- `total_count`: Total number of items available -- `page_count`: Total number of pages - -### Contracts - -You should read this section only if you are using the Nitrolite smart contracts directly. - -#### Action: Replaced `Signature` Struct with `bytes` - -The `Signature` struct has been removed and replaced with `bytes` type to support EIP-1271, EIP-6492, and other signature formats. - -Update all contract interactions that use signatures to pass `bytes` instead of the struct. - -```solidity -// remove-start -struct Signature { - uint8 v; - bytes32 r; - bytes32 s; -} - -function join( - bytes32 channelId, - uint256 index, - Signature calldata sig -) external returns (bytes32); - -function challenge( - bytes32 channelId, - State calldata candidate, - State[] calldata proofs, - Signature calldata challengerSig -) external; -// remove-end -// add-start -// Signature struct is removed - -function join( - bytes32 channelId, - uint256 index, - bytes calldata sig -) external returns (bytes32); - -function challenge( - bytes32 channelId, - State calldata candidate, - State[] calldata proofs, - bytes calldata challengerSig -) external; -// add-end -``` - -#### Actions: Updated `State` Signature Array - -The `State` struct now uses `bytes[]` for signatures instead of `Signature[]`. - -```solidity -struct State { - uint8 intent; - uint256 version; - bytes data; - Allocation[] allocations; - // remove-next-line - Signature[] sigs; - // add-next-line - bytes[] sigs; -} -``` - -#### Added: Auto-Join Channel Creation Flow - -Channels can now become operational immediately after the `create()` call if all participant signatures are provided. - -When calling `create()` with complete signatures from all participants, the channel automatically becomes active without requiring a separate `join()` call. - -**Single signature (requires join):** -```solidity -// Create channel with only creator's signature -State memory initialState = State({ - intent: StateIntent.Fund, - version: 0, - data: "0x", - allocations: allocations, - sigs: [creatorSignature] // Only one signature -}); - -bytes32 channelId = custody.create(channel, initialState); -// Channel status: JOINING - requires server to call join() -``` - -**Complete signatures (auto-active):** -```solidity -// Create channel with all participants' signatures -State memory initialState = State({ - intent: StateIntent.Fund, - version: 0, - data: "0x", - allocations: allocations, - sigs: [creatorSignature, serverSignature] // All signatures -}); - -bytes32 channelId = custody.create(channel, initialState); -// Channel status: ACTIVE - ready for use immediately -``` - -#### Actions: Update Adjudicator Contracts for EIP-712 Support - -A new `EIP712AdjudicatorBase` base contract has been added to support EIP-712 typed structured data signatures in adjudicator implementations. - -The `EIP712AdjudicatorBase` provides: -- **Domain separator retrieval**: Gets EIP-712 domain separator from the channel implementation contract -- **ERC-5267 compliance**: Automatically handles EIP-712 domain data retrieval -- **Ownership management**: Built-in access control for updating channel implementation address -- **Graceful fallbacks**: Returns `NO_EIP712_SUPPORT` constant when EIP-712 is not available - -If you have custom adjudicator contracts, inherit from `EIP712AdjudicatorBase` to enable EIP-712 signature verification. - -```solidity -// remove-start -import {IAdjudicator} from "../interfaces/IAdjudicator.sol"; -import {Channel, State, Allocation, StateIntent} from "../interfaces/Types.sol"; - -contract MyAdjudicator is IAdjudicator { - function adjudicate( - Channel calldata chan, - State calldata candidate, - State[] calldata proofs - ) external view override returns (bool valid) { - return candidate.validateUnanimousSignatures(chan); - } -} -// remove-end -// add-start -import {IAdjudicator} from "../interfaces/IAdjudicator.sol"; -import {Channel, State, Allocation, StateIntent} from "../interfaces/Types.sol"; -import {EIP712AdjudicatorBase} from "./EIP712AdjudicatorBase.sol"; - -contract MyAdjudicator is IAdjudicator, EIP712AdjudicatorBase { - constructor(address owner, address channelImpl) - EIP712AdjudicatorBase(owner, channelImpl) {} - - function adjudicate( - Channel calldata chan, - State calldata candidate, - State[] calldata proofs - ) external override returns (bool valid) { - bytes32 domainSeparator = getChannelImplDomainSeparator(); - return candidate.validateUnanimousStateSignatures(chan, domainSeparator); - } -} -// add-end -``` - -#### Added: Enhanced Signature Support - -Smart contracts now support EIP-191, EIP-712, EIP-1271, and EIP-6492 signature formats for greater compatibility. - -The contracts automatically detect and verify the appropriate signature format: -- **Raw ECDSA**: Traditional `(r, s, v)` signatures -- **EIP-191**: Personal message signatures (`\x19Ethereum Signed Message:\n`) -- **EIP-712**: Typed structured data signatures -- **EIP-1271**: Smart contract wallet signatures -- **EIP-6492**: Signatures for undeployed contracts - -No changes are needed in your contract calls - the signature verification is handled automatically by the contract. diff --git a/erc7824-docs/docs/index.md b/erc7824-docs/docs/index.md deleted file mode 100644 index 09c3c9d3e..000000000 --- a/erc7824-docs/docs/index.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -sidebar_position: 1 -title: Introduction -description: Build scalable web3 applications with state channels using Nitrolite. -keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, javascript, typescript, sdk] ---- - -import { Card, CardGrid } from '@site/src/components/Card'; - -# Introduction - -Welcome to Nitrolite! Built on the ERC-7824 standard, Nitrolite is a powerful framework that enables developers to build high-performance decentralized applications with near-instant finality. - -The following guides will walk you through the complete lifecycle of Nitrolite applications, from client initialization to application sessions. Whether you're building payment systems, games, financial applications, or any use case requiring high-frequency transactions, Nitrolite provides the infrastructure you need. - - - - - - - - - - -## Key Features - -- **Instant Transactions**: Off-chain operations mean no waiting for block confirmations. -- **Minimal Gas Fees**: On-chain gas is primarily for channel opening and settlement. -- **High Throughput**: Capable of handling thousands of transactions per second. -- **Application Flexibility**: Ideal for games, payment systems, real-time interactions, and more. - -## Core SDK Architecture - -The Nitrolite SDK is designed with modularity and broad compatibility in mind: - -1. **NitroliteClient**: This is the main entry point for developers. It provides a high-level API to manage the lifecycle of state channels, including deposits, channel creation, application session management, and withdrawals. - -2. **Nitrolite RPC**: This component handles the secure, real-time, off-chain communication between channel participants and the broker. It's responsible for message signing, verification, and routing. diff --git a/erc7824-docs/docs/nitrolite_client/advanced/abstract-accounts.md b/erc7824-docs/docs/nitrolite_client/advanced/abstract-accounts.md deleted file mode 100644 index ce7b5e4c5..000000000 --- a/erc7824-docs/docs/nitrolite_client/advanced/abstract-accounts.md +++ /dev/null @@ -1,427 +0,0 @@ ---- -sidebar_position: 1 -title: Abstract Accounts -description: Using NitroliteClient with Account Abstraction -keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, account abstraction, ERC-4337] ---- - -import MethodDetails from '@site/src/components/MethodDetails'; -import { Card, CardGrid } from '@site/src/components/Card'; - -# Using with Abstract Accounts - -The `NitroliteClient` provides special support for [ERC-4337 Account Abstraction](https://eips.ethereum.org/EIPS/eip-4337) through the `txPreparer` object. This allows dApps using smart contract wallets to prepare transactions without executing them, enabling batching and other advanced patterns. - -## Transaction Preparer Overview - -The `txPreparer` is a property of the `NitroliteClient` that provides methods for preparing transaction data without sending it to the blockchain. Each method returns one or more [`PreparedTransaction`](../types.md#preparedtransaction) objects that can be used with Account Abstraction providers. - -```typescript -import { NitroliteClient } from '@erc7824/nitrolite'; - -const client = new NitroliteClient({/* config */}); - -// Instead of: await client.deposit(amount) -const txs = await client.txPreparer.prepareDepositTransactions(amount); -``` - -## Transaction Preparation Methods - -These methods allow you to prepare transactions for the entire channel lifecycle without executing them. - -### 1. Deposit Methods - -`} - example={`// Prepare deposit transaction(s) - may include ERC20 approval -const txs = await client.txPreparer.prepareDepositTransactions(1000000n); - -// For each prepared transaction -for (const tx of txs) { - await aaProvider.sendUserOperation({ - target: tx.to, - data: tx.data, - value: tx.value || 0n - }); -}`} -/> - -`} - example={`// Prepare approval transaction -const tx = await client.txPreparer.prepareApproveTokensTransaction(2000000n); - -// Send through your AA provider -await aaProvider.sendUserOperation({ - target: tx.to, - data: tx.data -});`} -/> - -### 2. Channel Creation Methods - -`} - example={`// Prepare channel creation transaction -const tx = await client.txPreparer.prepareCreateChannelTransaction({ - initialAllocationAmounts: [700000n, 300000n], - stateData: '0x1234' -}); - -// Send it through your Account Abstraction provider -await aaProvider.sendUserOperation({ - target: tx.to, - data: tx.data, - value: tx.value || 0n -});`} -/> - -`} - example={`// Prepare deposit + channel creation (potentially 3 txs: approve, deposit, create) -const txs = await client.txPreparer.prepareDepositAndCreateChannelTransactions( - 1000000n, - { - initialAllocationAmounts: [700000n, 300000n], - stateData: '0x1234' - } -); - -// Bundle these transactions into a single UserOperation -await aaProvider.sendUserOperation({ - userOperations: txs.map(tx => ({ - target: tx.to, - data: tx.data, - value: tx.value || 0n - })) -});`} -/> - -### 3. Channel Operation Methods - -`} - example={`// Prepare checkpoint transaction -const tx = await client.txPreparer.prepareCheckpointChannelTransaction({ - channelId: '0x...', - candidateState: state -}); - -await aaProvider.sendUserOperation({ - target: tx.to, - data: tx.data -});`} -/> - -`} - example={`// Prepare challenge transaction -const tx = await client.txPreparer.prepareChallengeChannelTransaction({ - channelId: '0x...', - candidateState: state -}); - -await aaProvider.sendUserOperation({ - target: tx.to, - data: tx.data -});`} -/> - -`} - example={`// Prepare resize transaction -const tx = await client.txPreparer.prepareResizeChannelTransaction({ - channelId: '0x...', - candidateState: state -}); - -await aaProvider.sendUserOperation({ - target: tx.to, - data: tx.data -});`} -/> - -### 4. Channel Closing Methods - -`} - example={`// Prepare close channel transaction -const tx = await client.txPreparer.prepareCloseChannelTransaction({ - finalState: { - channelId: '0x...', - stateData: '0x...', - allocations: [allocation1, allocation2], - version: 10n, - serverSignature: signature - } -}); - -await aaProvider.sendUserOperation({ - target: tx.to, - data: tx.data -});`} -/> - -### 5. Withdrawal Methods - -`} - example={`// Prepare withdrawal transaction -const tx = await client.txPreparer.prepareWithdrawalTransaction(500000n); - -await aaProvider.sendUserOperation({ - target: tx.to, - data: tx.data -});`} -/> - - -## Understanding PreparedTransaction - -The [`PreparedTransaction`](../types.md#preparedtransaction) type is the core data structure returned by all transaction preparation methods. It contains all the information needed to construct a transaction or UserOperation: - -```typescript -type PreparedTransaction = { - // Target contract address - to: Address; - - // Contract call data - data?: Hex; - - // ETH value to send (0n for token operations) - value?: bigint; -}; -``` - -Each `PreparedTransaction` represents a single contract call that can be: - -1. **Executed directly** - If you're using a standard EOA wallet -2. **Bundled into a UserOperation** - For account abstraction providers -3. **Batched with other transactions** - For advanced use cases - -## Integration Examples - -The Nitrolite transaction preparer can be integrated with any Account Abstraction provider. Here are examples with popular AA SDKs: - -### With Safe Account Abstraction SDK - -```typescript -import { NitroliteClient } from '@erc7824/nitrolite'; -import { AccountAbstraction } from '@safe-global/account-abstraction-kit-poc'; - -// Initialize clients -const client = new NitroliteClient({/* config */}); -const aaKit = new AccountAbstraction(safeProvider); - -// Prepare transaction -const tx = await client.txPreparer.prepareCreateChannelTransaction({ - initialAllocationAmounts: [700000n, 300000n], - stateData: '0x1234' -}); - -// Send through AA provider -const safeTransaction = await aaKit.createTransaction({ - transactions: [{ - to: tx.to, - data: tx.data, - value: tx.value?.toString() || '0' - }] -}); - -const txResponse = await aaKit.executeTransaction(safeTransaction); -``` - -### With Biconomy SDK - -```typescript -import { NitroliteClient } from '@erc7824/nitrolite'; -import { BiconomySmartAccountV2 } from "@biconomy/account"; - -// Initialize clients -const client = new NitroliteClient({/* config */}); -const smartAccount = await BiconomySmartAccountV2.create({/* config */}); - -// Prepare transaction -const txs = await client.txPreparer.prepareDepositAndCreateChannelTransactions( - 1000000n, - { - initialAllocationAmounts: [700000n, 300000n], - stateData: '0x1234' - } -); - -// Build user operation -const userOp = await smartAccount.buildUserOp( - txs.map(tx => ({ - to: tx.to, - data: tx.data, - value: tx.value || 0n - })) -); - -// Send user operation -const userOpResponse = await smartAccount.sendUserOp(userOp); -await userOpResponse.wait(); -``` - -## Advanced Use Cases - -The transaction preparer is especially powerful when combined with advanced Account Abstraction features. - -### Batching Multiple Operations - -One of the main advantages of Account Abstraction is the ability to batch multiple operations into a single transaction: - -```typescript -// Collect prepared transactions from different operations -const preparedTxs = []; - -// 1. Add token approval if needed -const allowance = await client.getTokenAllowance(); -if (allowance < totalNeeded) { - const approveTx = await client.txPreparer.prepareApproveTokensTransaction(totalNeeded); - preparedTxs.push(approveTx); -} - -// 2. Add deposit -const depositTx = await client.txPreparer.prepareDepositTransactions(amount); -preparedTxs.push(...depositTx); - -// 3. Add channel creation -const createChannelTx = await client.txPreparer.prepareCreateChannelTransaction(params); -preparedTxs.push(createChannelTx); - -// 4. Execute all as a batch with your AA provider -await aaProvider.sendUserOperation({ - userOperations: preparedTxs.map(tx => ({ - target: tx.to, - data: tx.data, - value: tx.value || 0n - })) -}); -``` - -### Gas Sponsoring - -Account Abstraction enables gas sponsorship, where someone else pays for the transaction gas: - -```typescript -// Prepare transaction -const tx = await client.txPreparer.prepareCreateChannelTransaction(params); - -// Use a sponsored transaction -await paymasterProvider.sponsorTransaction({ - target: tx.to, - data: tx.data, - value: tx.value || 0n, - user: userAddress -}); -``` - -### Session Keys - -Some AA wallets support session keys, which are temporary keys with limited permissions: - -```typescript -// Create a session key with permissions only for specific operations -const sessionKeyData = await aaWallet.createSessionKey({ - permissions: [ - { - target: client.addresses.custody, - // Only allow specific functions - functionSelector: [ - "0xdeposit(...)", - "0xwithdraw(...)" - ] - } - ], - expirationTime: Date.now() + 3600 * 1000 // 1 hour -}); - -// Use the session key to prepare and send transactions -const tx = await client.txPreparer.prepareDepositTransactions(amount); -await aaWallet.executeWithSessionKey(sessionKeyData, { - target: tx.to, - data: tx.data, - value: tx.value || 0n -}); -``` - -## Best Practices - -
-
-

Batch Related Operations

-

Use prepareDepositAndCreateChannelTransactions to batch deposit and channel creation into a single user operation.

-
- -
-

Handle Approvals

-

For ERC20 tokens, prepareDepositTransactions will include an approval transaction if needed. Always process all returned transactions.

-
- -
-

State Signing

-

Even when using Account Abstraction, state signatures are handled separately using the stateWalletClient (or walletClient if not specified).

-
- -
-

Error Handling

-

The preparation methods throw the same errors as their execution counterparts, so use the same error handling patterns.

-
- -
-

Check Token Allowances

-

Before preparing token operations, you can check if approval is needed:

-
-      
-const allowance = await client.getTokenAllowance();
-if (allowance < amount) {
-  // Need approval
-}
-      
-    
-
- -
-

Gas Estimation

-

When using Account Abstraction, gas estimation is typically handled by the AA provider, but you can request estimates if needed.

-
-
- -## Limitations - -:::caution Important -- The transaction preparer **doesn't handle sequencing or nonce management** - that's the responsibility of your AA provider. -- Some operations (like checkpointing) require signatures from all participants, which must be collected separately from the transaction preparation. -::: \ No newline at end of file diff --git a/erc7824-docs/docs/nitrolite_client/advanced/erc20-service.md b/erc7824-docs/docs/nitrolite_client/advanced/erc20-service.md deleted file mode 100644 index c8e3c6e23..000000000 --- a/erc7824-docs/docs/nitrolite_client/advanced/erc20-service.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -sidebar_position: 3 -title: Erc20Service -description: Documentation for the ERC20Service class -keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, advanced, erc20, tokens] ---- - -# Erc20Service - -The `Erc20Service` class provides a convenient interface for interacting with ERC20 tokens. It handles token approvals, allowance checks, and balance inquiries that are essential for deposit and withdrawal operations in the Nitrolite system. - -## Initialization - -```typescript -import { Erc20Service } from '@erc7824/nitrolite'; - -const erc20Service = new Erc20Service( - publicClient, // viem PublicClient - walletClient // viem WalletClient -); -``` - -## Core Methods - -| Method | Description | Parameters | Return Type | -|--------|-------------|------------|------------| -| `approve` | Approves a spender to use tokens. | `tokenAddress: Address, spender: Address, amount: bigint` | `Promise` | -| `getTokenAllowance` | Gets token allowance for a spender. | `tokenAddress: Address, owner: Address, spender: Address` | `Promise` | -| `getTokenBalance` | Gets token balance for an account. | `tokenAddress: Address, account: Address` | `Promise` | - -## Method Details - -### Approve Tokens - -Approves a spender (typically the Custody contract) to transfer tokens on behalf of the owner. - -```typescript -// Approve the custody contract to spend 1000 tokens -const txHash = await erc20Service.approve( - tokenAddress, // ERC20 token address - spenderAddress, // Custody contract address - 1000000000000000000n // Amount to approve (1 token with 18 decimals) -); -``` - -**Important notes:** -- For security reasons, always specify the exact amount you want to approve -- Consider using the ERC20 token decimals for the amount calculation -- The transaction will fail if the owner has insufficient balance - -### Get Token Allowance - -Retrieves the current allowance granted by an owner to a spender. - -```typescript -// Check current allowance -const allowance = await erc20Service.getTokenAllowance( - tokenAddress, // ERC20 token address - ownerAddress, // Owner's address - spenderAddress // Spender's address (custody contract) -); - -console.log(`Current allowance: ${allowance}`); - -// Check if allowance is sufficient -if (allowance < requiredAmount) { - console.log('Need to approve more tokens'); -} -``` - -### Get Token Balance - -Retrieves the token balance for a specific account. - -```typescript -// Check token balance -const balance = await erc20Service.getTokenBalance( - tokenAddress, // ERC20 token address - accountAddress // Account to check -); - -console.log(`Account balance: ${balance}`); - -// Check if balance is sufficient -if (balance < requiredAmount) { - console.log('Insufficient token balance'); -} -``` - -## Transaction Preparation - -For Account Abstraction support, `Erc20Service` provides a transaction preparation method: - -| Method | Description | Parameters | Return Type | -|--------|-------------|------------|------------| -| `prepareApprove` | Prepares an approval transaction. | `tokenAddress: Address, spender: Address, amount: bigint` | `Promise` | - -Example: -```typescript -// Prepare approval transaction -const tx = await erc20Service.prepareApprove( - tokenAddress, - spenderAddress, - amount -); - -// Use with your Account Abstraction provider -const userOp = await aaProvider.buildUserOperation({ - target: tx.to, - data: tx.data, - value: 0n // ERC20 approvals don't require ETH -}); -``` - -## Implementation Details - -The `Erc20Service` uses the standard ERC20 interface methods: - -- `approve`: Allows a spender to withdraw tokens from the owner's account, up to the specified amount -- `allowance`: Returns the remaining tokens that a spender is allowed to withdraw -- `balanceOf`: Returns the token balance of the specified account - -## Working with Token Decimals - -ERC20 tokens typically have decimal places (most commonly 18). When working with token amounts, you should account for these decimals: - -```typescript -import { parseUnits } from 'viem'; - -// For a token with 18 decimals -const tokenDecimals = 18; - -// Convert 1.5 tokens to the smallest unit -const amount = parseUnits('1.5', tokenDecimals); - -// Approve the amount -await erc20Service.approve(tokenAddress, spenderAddress, amount); -``` - -## Error Handling - -The `Erc20Service` throws specific error types: - -- `TokenError`: For token-specific errors (insufficient balance, approval failures) -- `ContractCallError`: When calls to the contract fail -- `WalletClientRequiredError`: When wallet client is needed but not provided - -Example: -```typescript -try { - await erc20Service.approve(tokenAddress, spenderAddress, amount); -} catch (error) { - if (error instanceof TokenError) { - console.error(`Token error: ${error.message}`); - console.error(`Suggestion: ${error.suggestion}`); - - // Check for specific token error conditions - if (error.details?.errorName === 'InsufficientBalance') { - console.log(`Available balance: ${error.details.available}`); - } - } -} -``` - -## Common Patterns - -### Checking and Approving Tokens - -A common pattern is to check if the current allowance is sufficient before approving more tokens: - -```typescript -// Get current allowance -const allowance = await erc20Service.getTokenAllowance( - tokenAddress, - ownerAddress, - spenderAddress -); - -// If allowance is insufficient, approve more tokens -if (allowance < requiredAmount) { - await erc20Service.approve(tokenAddress, spenderAddress, requiredAmount); -} - -// Now proceed with the operation that requires the approval -// (e.g., deposit into custody contract) -``` - -### Handling Multiple Tokens - -If your application works with multiple tokens, you can reuse the same `Erc20Service` instance: - -```typescript -// Same service instance for different tokens -const erc20Service = new Erc20Service(publicClient, walletClient); - -// Work with token A -const balanceA = await erc20Service.getTokenBalance(tokenAddressA, accountAddress); - -// Work with token B -const balanceB = await erc20Service.getTokenBalance(tokenAddressB, accountAddress); -``` - -## Integration with NitroliteClient - -When using the `NitroliteClient`, you typically don't need to interact with `Erc20Service` directly, as the client handles these operations for you: - -```typescript -// NitroliteClient handles token approvals automatically during deposit -await nitroliteClient.deposit(amount); - -// For explicit approval without deposit -await nitroliteClient.approveTokens(amount); - -// Get token balance through the client -const balance = await nitroliteClient.getTokenBalance(); -``` - -However, for advanced use cases or custom token interaction, direct access to `Erc20Service` can be useful. \ No newline at end of file diff --git a/erc7824-docs/docs/nitrolite_client/advanced/index.md b/erc7824-docs/docs/nitrolite_client/advanced/index.md deleted file mode 100644 index 01f03d640..000000000 --- a/erc7824-docs/docs/nitrolite_client/advanced/index.md +++ /dev/null @@ -1,150 +0,0 @@ ---- -sidebar_position: 3 -title: Advanced Usage -description: Advanced topics for working with the NitroliteClient -keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, advanced] ---- - -# Advanced Usage - -This section covers advanced topics for working with the `@erc7824/nitrolite` SDK. These are intended for developers who need deeper control over the state channel operations or want to integrate with specialized systems. - -## Available Topics - -### Low-level Services - -The NitroliteClient is built on top of specialized services that can be used directly for more fine-grained control: - -- [NitroliteService](./nitrolite-service.md) - Core service for interacting with the custody contract and managing channels -- [Erc20Service](./erc20-service.md) - Service for interacting with ERC20 tokens - -### Account Abstraction Integration - -- [Abstract Accounts](./abstract-accounts.md) - Using NitroliteClient with ERC-4337 Account Abstraction for smart contract wallets - -## When to Use Advanced Features - -Consider using the advanced features when: - -1. **Building custom workflows** that require more control than the high-level NitroliteClient API provides -2. **Integrating with smart contract wallets** or other account abstraction systems -3. **Implementing specialized monitoring or management systems** for state channels -4. **Developing cross-chain applications** that require custom handling of state channel operations -5. **Optimizing gas usage** through transaction batching and other techniques - -## Example: Direct Service Usage - -While using the high-level NitroliteClient is recommended for most applications, here's how you can work directly with the services: - -```typescript -import { - NitroliteService, - Erc20Service, - getChannelId, - signState -} from '@erc7824/nitrolite'; - -// Initialize services -const nitroliteService = new NitroliteService( - publicClient, - addresses, - walletClient, - account.address -); - -const erc20Service = new Erc20Service( - publicClient, - walletClient -); - -// Check token allowance -const allowance = await erc20Service.getTokenAllowance( - tokenAddress, - account.address, - addresses.custody -); - -// Approve tokens if needed -if (allowance < depositAmount) { - await erc20Service.approve(tokenAddress, addresses.custody, depositAmount); -} - -// Deposit funds -await nitroliteService.deposit(tokenAddress, depositAmount); - -// Create channel with custom parameters -const channelNonce = generateChannelNonce(account.address); -const channel = { - participants: [account.address, counterpartyAddress], - adjudicator: addresses.adjudicator, - challenge: 100n, // Challenge duration in seconds - nonce: channelNonce -}; - -// Prepare initial state -const initialState = { - intent: StateIntent.INITIALIZE, - version: 0n, - data: '0x1234', // Application-specific data - allocations: [ - { destination: account.address, token: tokenAddress, amount: 700000n }, - { destination: counterpartyAddress, token: tokenAddress, amount: 300000n } - ], - sigs: [] // Will be filled with signatures -}; - -// Sign the state -const channelId = getChannelId(channel); -const stateHash = getStateHash(channelId, initialState); -const signature = await signState(walletClient, stateHash); -initialState.sigs = [signature]; - -// Create the channel -await nitroliteService.createChannel(channel, initialState); -``` - -## Example: Complex Transaction Preparation - -For applications using Account Abstraction, you can prepare complex transaction sequences: - -```typescript -import { NitroliteClient, NitroliteTransactionPreparer } from '@erc7824/nitrolite'; - -// Initialize client -const client = new NitroliteClient({/* config */}); - -// Access the transaction preparer directly -const txPreparer = client.txPreparer; - -// Prepare a complete sequence (approve, deposit, create channel) -const allTxs = []; - -// 1. Check and prepare token approval if needed -const allowance = await client.getTokenAllowance(); -if (allowance < depositAmount) { - const approveTx = await txPreparer.prepareApproveTokensTransaction(depositAmount); - allTxs.push(approveTx); -} - -// 2. Prepare deposit -const depositTx = await txPreparer.prepareDepositTransactions(depositAmount); -allTxs.push(...depositTx); - -// 3. Prepare channel creation -const createChannelTx = await txPreparer.prepareCreateChannelTransaction({ - initialAllocationAmounts: [700000n, 300000n], - stateData: '0x1234' -}); -allTxs.push(createChannelTx); - -// 4. Use with your Account Abstraction provider -await aaProvider.sendUserOperation({ - userOperations: allTxs.map(tx => ({ - target: tx.to, - data: tx.data, - value: tx.value || 0n - })) -}); -``` - -These advanced techniques give you greater flexibility and control over the state channel operations, but they also require a deeper understanding of the underlying protocol. \ No newline at end of file diff --git a/erc7824-docs/docs/nitrolite_client/advanced/nitrolite-service.md b/erc7824-docs/docs/nitrolite_client/advanced/nitrolite-service.md deleted file mode 100644 index c4452fc41..000000000 --- a/erc7824-docs/docs/nitrolite_client/advanced/nitrolite-service.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -sidebar_position: 2 -title: NitroliteService -description: Documentation for the core NitroliteService class -keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, advanced] ---- - -# NitroliteService - -The `NitroliteService` class is the core service that directly interacts with the Nitrolite Custody smart contract. It handles channel management, deposits, withdrawals, and all other channel-specific operations following the channel lifecycle. - -## Initialization - -```typescript -import { NitroliteService } from '@erc7824/nitrolite'; - -const nitroliteService = new NitroliteService( - publicClient, // viem PublicClient - addresses, // ContractAddresses - walletClient, // viem WalletClient - account // Account address -); -``` - -## Channel Lifecycle Methods - -### 1. Deposit Operations - -| Method | Description | Parameters | Return Type | -|--------|-------------|------------|------------| -| `deposit` | Deposits tokens/ETH into the custody contract. | `tokenAddress: Address, amount: bigint` | `Promise` | -| `withdraw` | Withdraws tokens from the custody contract. | `tokenAddress: Address, amount: bigint` | `Promise` | - -Example: -```typescript -// Deposit ETH or token -const txHash = await nitroliteService.deposit(tokenAddress, amount); - -// Withdraw ETH or token -const txHash = await nitroliteService.withdraw(tokenAddress, amount); -``` - -### 2. Channel Creation - -| Method | Description | Parameters | Return Type | -|--------|-------------|------------|------------| -| `createChannel` | Creates a new channel with the given parameters. | `channel: Channel, initialState: State` | `Promise` | - -Example: -```typescript -// Create a channel -const txHash = await nitroliteService.createChannel(channel, initialState); -``` - -Where: -- `channel` defines the participants, adjudicator, challenge period, and nonce -- `initialState` contains the initial allocation of funds and state data - -### 3. Channel Operations - -| Method | Description | Parameters | Return Type | -|--------|-------------|------------|------------| -| `checkpoint` | Checkpoints a state on-chain. | `channelId: ChannelId, candidateState: State, proofStates?: State[]` | `Promise` | -| `challenge` | Challenges a channel with a candidate state. | `channelId: ChannelId, candidateState: State, proofStates?: State[]` | `Promise` | -| `resize` | Resizes a channel with a candidate state. | `channelId: ChannelId, candidateState: State, proofStates?: State[]` | `Promise` | - -Example: -```typescript -// Checkpoint a channel state -const txHash = await nitroliteService.checkpoint(channelId, candidateState); - -// Challenge a channel -const txHash = await nitroliteService.challenge(channelId, candidateState); - -// Resize a channel -const txHash = await nitroliteService.resize(channelId, candidateState); -``` - -### 4. Channel Closing - -| Method | Description | Parameters | Return Type | -|--------|-------------|------------|------------| -| `close` | Closes a channel using a final state. | `channelId: ChannelId, finalState: State` | `Promise` | - -Example: -```typescript -// Close a channel -const txHash = await nitroliteService.close(channelId, finalState); -``` - -### 5. Account Information - -| Method | Description | Parameters | Return Type | -|--------|-------------|------------|------------| -| `getAccountChannels` | Gets channel IDs for an account. | `accountAddress: Address` | `Promise` | -| `getAccountInfo` | Gets account info for a token. | `accountAddress: Address, tokenAddress: Address` | `Promise` | - -Example: -```typescript -// Get all channels for an account -const channels = await nitroliteService.getAccountChannels(accountAddress); - -// Get detailed account info -const info = await nitroliteService.getAccountInfo(accountAddress, tokenAddress); -console.log(`Available: ${info.available}, Locked: ${info.locked}`); -``` - -## Transaction Preparation Methods - -For Account Abstraction support, NitroliteService provides transaction preparation methods that return transaction data without executing it: - -| Method | Description | Parameters | Return Type | -|--------|-------------|------------|------------| -| `prepareDeposit` | Prepares deposit transaction. | `tokenAddress: Address, amount: bigint` | `Promise` | -| `prepareCreateChannel` | Prepares channel creation transaction. | `channel: Channel, initialState: State` | `Promise` | -| `prepareCheckpoint` | Prepares checkpoint transaction. | `channelId: ChannelId, candidateState: State, proofStates?: State[]` | `Promise` | -| `prepareChallenge` | Prepares challenge transaction. | `channelId: ChannelId, candidateState: State, proofStates?: State[]` | `Promise` | -| `prepareResize` | Prepares resize transaction. | `channelId: ChannelId, candidateState: State, proofStates?: State[]` | `Promise` | -| `prepareClose` | Prepares close transaction. | `channelId: ChannelId, finalState: State` | `Promise` | -| `prepareWithdraw` | Prepares withdraw transaction. | `tokenAddress: Address, amount: bigint` | `Promise` | - -Example: -```typescript -// Prepare deposit transaction -const tx = await nitroliteService.prepareDeposit(tokenAddress, amount); - -// Use with your Account Abstraction provider -const userOp = await aaProvider.buildUserOperation({ - target: tx.to, - data: tx.data, - value: tx.value || 0n -}); -``` - -## Implementation Details - -The `NitroliteService` connects to the Custody contract using: - -- A viem `PublicClient` for read operations -- A viem `WalletClient` for write operations and signing -- The contract address specified in the configuration - -The service handles: -- Contract interaction -- Parameter validation -- Error handling -- Transaction preparation - -## Error Handling - -The `NitroliteService` throws specific error types: - -- `ContractCallError`: When calls to the contract fail -- `InvalidParameterError`: When parameters are invalid -- `MissingParameterError`: When required parameters are missing -- `WalletClientRequiredError`: When wallet client is needed but not provided -- `AccountRequiredError`: When account is needed but not provided - -Example: -```typescript -try { - await nitroliteService.deposit(tokenAddress, amount); -} catch (error) { - if (error instanceof ContractCallError) { - console.error(`Contract call failed: ${error.message}`); - console.error(`Suggestion: ${error.suggestion}`); - } -} -``` - -## Advanced Usage - -### Custom Contract Interaction - -For advanced use cases, you might need to interact directly with the contract: - -```typescript -// Get the custody contract address -const custodyAddress = nitroliteService.custodyAddress; - -// Use with your own custom contract interaction -const customContract = getContract({ - address: custodyAddress, - abi: custodyAbi, - // Additional configuration... -}); -``` - -### Multiple Channel Management - -For applications managing multiple channels: - -```typescript -// Get all channels for the account -const channels = await nitroliteService.getAccountChannels(accountAddress); - -// Process each channel -for (const channelId of channels) { - // Get channel info from contract - // Process channel state -} -``` \ No newline at end of file diff --git a/erc7824-docs/docs/nitrolite_client/advanced/supported-sig-formats.md b/erc7824-docs/docs/nitrolite_client/advanced/supported-sig-formats.md deleted file mode 100644 index 9422f138e..000000000 --- a/erc7824-docs/docs/nitrolite_client/advanced/supported-sig-formats.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -sidebar_position: 4 -title: Supported Signature Formats -description: Documentation for supported signature formats in NitroliteClient -keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, advanced, signature, format, ECDSA, EIP-191, EIP-712, EIP-1271, EIP-6492] ---- - -# Supported Signature Formats - -The nitrolite smart contract supports multiple signature formats over a State to accommodate various use cases and compatibility with different wallets and applications. - -The message being signed is a channelId and State, formatted in a specific way. The most common is a `packedState`, which is calculated as follows: - -```solidity -abi.encode(channelId, state.intent, state.version, state.data, state.allocations) -``` - -## EOA signatures - -Externally Owned Accounts (EOAs) can sign messages with their private key using the ECDSA. - -Based on how the message is handled before signing, the following formats are supported: - -### Raw ECDSA Signature - -The message is a `packedState`, that is hashed with `keccak256` before signing. The signature is a 65-byte ECDSA signature. - -### EIP-191 Signature - -You can read more about EIP-191 in the [EIP-191 specification](https://eips.ethereum.org/EIPS/eip-191). - -The message is a `packedState` prefixed with `"\x19Ethereum Signed Message:\n" + len(packedState)` and hashed with `keccak256` before signing. The signature is a 65-byte ECDSA signature. - -### EIP-712 Signature - -You can read more about EIP-712 in the [EIP-712 specification](https://eips.ethereum.org/EIPS/eip-712). - -The message is an `AllowStateHash` typed data, calculated as follows: - -```solidity -abi.encode( - typeHash, - channelId, - state.intent, - state.version, - keccak256(state.data), - keccak256(abi.encode(state.allocations)) -); -``` - -Where `typeHash` is `AllowStateHash(bytes32 channelId,uint8 intent,uint256 version,bytes data,Allocation[] allocations)Allocation(address destination,address token,uint256 amount)`. - -The message is then hashed with `keccak256`, appended to `"\x19\x01" || domainSeparator` and signed. The signature is a 65-byte ECDSA signature. - -`||` is a concatenation operator, and `domainSeparator` is calculated as follows: - -```solidity -keccak256( - abi.encode( - EIP712_TYPE_HASH, - keccak256(name), - keccak256(version), - chainId, - verifyingContract - ) -); -``` - -`EIP712_TYPE_HASH` is `keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")`. - -Additionally, `name`, `version` are the name and version of the Custody contract, `chainId` is the chain ID of the network, and `verifyingContract` is the address of the contract. - -## Smart Contract Signatures - -Smart Contracts that support [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) or [EIP-6492](https://eips.ethereum.org/EIPS/eip-6492) can sign messages using their own logic. When checking such signatures, the nitrolite smart contract will pass the `keccak256` hash of the `packedState` as a message hash for verification. - -See the aforementioned EIP standards for details on how these signatures are structured and verified. If you want to add support for such signatures in your client, you probably need to look at how signature verification logic is implemented in the Smart Contract (Smart Wallet, etc) that will use them. - -## Challenge Signatures - -The aforementioned signature formats are used to sign States, however to submit a challenge, the user must provide a `challengerSignature`, which proves that the user has the right to challenge a Channel. - -Depending on a signature format, the `challengerSignature` is calculated differently from the common State signature: - -- **Raw ECDSA, EIP-191, EIP-1271 and EIP-6492**: The message (`packedState`) is suffixed with a `challenge` string (`abi.encodePacked(packedState, "challenge")`). -- **EIP-712**: The `typeHash` name is `AllowChallengeStateHash`, while type format remains the same. diff --git a/erc7824-docs/docs/nitrolite_client/index.mdx b/erc7824-docs/docs/nitrolite_client/index.mdx deleted file mode 100644 index 0ddb5914f..000000000 --- a/erc7824-docs/docs/nitrolite_client/index.mdx +++ /dev/null @@ -1,122 +0,0 @@ ---- -sidebar_position: 2 -title: NitroliteClient -description: Nitrolite client SDK documentation -keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, react tutorial] ---- - -import { Card, CardGrid } from '@site/src/components/Card'; - -# NitroliteClient - -The `NitroliteClient` class is the main entry point for interacting with Nitrolite state channels. It provides a comprehensive set of methods for managing channels, deposits, and funds. - - - - - - - - -## Quick Start - -```typescript -import { NitroliteClient } from '@erc7824/nitrolite'; - -// Initialize client -const nitroliteClient = new NitroliteClient({ - publicClient, - walletClient, - addresses: { - custody: '0x...', - adjudicator: '0x...', - guestAddress: '0x...', - tokenAddress: '0x...' - }, - chainId: 137, // Polygon mainnet - challengeDuration: 100n -}); - -// 1. Deposit funds -const depositTxHash = await nitroliteClient.deposit(1000000n); - -// 2. Create a channel -const { channelId, initialState, txHash } = await nitroliteClient.createChannel({ - initialAllocationAmounts: [700000n, 300000n], - stateData: '0x1234' -}); - -// 3. Resize the channel when needed -const resizeTxHash = await nitroliteClient.resizeChannel({ - resizeState: { - channelId, - stateData: '0x5678', - allocations: newAllocations, - version: 2n, - intent: StateIntent.RESIZE, - serverSignature: signature - }, - proofStates: [] -}); - -// 4. Close the channel -const closeTxHash = await nitroliteClient.closeChannel({ - finalState: { - channelId, - stateData: '0x5678', - allocations: newAllocations, - version: 5n, - serverSignature: signature - } -}); - -// 5. Withdraw funds -const withdrawTxHash = await nitroliteClient.withdrawal(800000n); -``` - -{/* ## Learning Path - - - - - - - - - */} \ No newline at end of file diff --git a/erc7824-docs/docs/nitrolite_client/methods.mdx b/erc7824-docs/docs/nitrolite_client/methods.mdx deleted file mode 100644 index 2cd7f3973..000000000 --- a/erc7824-docs/docs/nitrolite_client/methods.mdx +++ /dev/null @@ -1,236 +0,0 @@ ---- -sidebar_position: 1 -title: Methods -description: Complete API reference for NitroliteClient -keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, react tutorial] ---- - -import MethodDetails from '@site/src/components/MethodDetails'; -import { Card, CardGrid } from '@site/src/components/Card'; - -# Methods - -This page provides a complete reference for all methods available in the `NitroliteClient` class from the `@erc7824/nitrolite` package. - -## Channel Lifecycle Methods - -These methods are organized according to the typical lifecycle of a state channel. - -### 1. Deposit Methods - -The deposit phase includes methods for managing funds in the custody contract and handling token approvals. - - - - - - - - - -### 2. Channel Creation Methods - - - - - -### 3. Channel Operation Methods - - - - - - - -### 4. Channel Closing Methods - - - -### 5. Withdrawal Methods - - - -## Account Information Methods - -These methods provide information about your account's state: - - - - - -:::caution Advanced Usage: Transaction Preparation -For Account Abstraction support and transaction preparation methods, see the [Abstract Accounts](./advanced/abstract-accounts) page. -::: - -## Example: Complete Channel Lifecycle - -```typescript -import { NitroliteClient } from '@erc7824/nitrolite'; - -// Initialize the client -const client = new NitroliteClient({ - publicClient, - walletClient, - addresses: { custody, adjudicator, guestAddress, tokenAddress }, - chainId: 137, - challengeDuration: 100n -}); - -// 1. Deposit funds -const depositTxHash = await client.deposit(1000000n); - -// 2. Create a channel -const { channelId, initialState } = await client.createChannel({ - initialAllocationAmounts: [700000n, 300000n], - stateData: '0x1234' -}); - -// 3. Get account info to verify funds are locked -const accountInfo = await client.getAccountInfo(); -console.log(`Locked in channels: ${accountInfo.locked}`); - -// 4. Later, resize the channel -const resizeTxHash = await client.resizeChannel({ - resizeState: { - channelId, - stateData: '0x5678', - allocations: newAllocations, - version: 2n, - intent: StateIntent.RESIZE, - serverSignature: signature - }, - proofStates: [] -}); - -// 5. Close the channel -const closeTxHash = await client.closeChannel({ - finalState: { - channelId, - stateData: '0x5678', - allocations: [ - { destination: userAddress, token: tokenAddress, amount: 800000n }, - { destination: counterpartyAddress, token: tokenAddress, amount: 200000n } - ], - version: 5n, - serverSignature: signature - } -}); - -// 6. Withdraw funds -const withdrawTxHash = await client.withdrawal(800000n); -``` \ No newline at end of file diff --git a/erc7824-docs/docs/nitrolite_client/types.md b/erc7824-docs/docs/nitrolite_client/types.md deleted file mode 100644 index e82cec173..000000000 --- a/erc7824-docs/docs/nitrolite_client/types.md +++ /dev/null @@ -1,371 +0,0 @@ ---- -sidebar_position: 2 -title: Type Definitions -description: Detailed type definitions used in the NitroliteClient -keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, react tutorial] ---- - -# Type Definitions - -This page documents the core types used throughout the `@erc7824/nitrolite` SDK. Understanding these types is essential for effectively working with the `NitroliteClient`. - -## Core Types - -### ChannelId - -```typescript -type ChannelId = Hex; -``` - -A unique identifier for a state channel, represented as a hexadecimal string. - -### StateHash - -```typescript -type StateHash = Hex; -``` - -A hash of a channel state, represented as a hexadecimal string. - -### Signature - -```typescript -type Signature = Hex; -``` - -Represents a cryptographic signature used for signing state channel states as a hexadecimal string. - -### Allocation - -```typescript -interface Allocation { - destination: Address; // Where funds are sent on channel closure - token: Address; // ERC-20 token address (zero address for ETH) - amount: bigint; // Token amount allocated -} -``` - -Represents the allocation of funds to a particular destination. - -### Channel - -```typescript -interface Channel { - participants: [Address, Address]; // List of participants [Host, Guest] - adjudicator: Address; // Address of the contract that validates states - challenge: bigint; // Duration in seconds for challenge period - nonce: bigint; // Unique per channel with same participants and adjudicator -} -``` - -Represents the core configuration of a state channel. - -### StateIntent - -```typescript -enum StateIntent { - OPERATE, // Operate the state application - INITIALIZE, // Initial funding state - RESIZE, // Resize state - FINALIZE, // Final closing state -} -``` - -Indicates the intent of a state update. The intent determines how the state is processed by the channel participants and the blockchain. - -### State - -```typescript -interface State { - intent: StateIntent; // Intent of the state - version: bigint; // Version number, incremented for each update - data: Hex; // Application data encoded as hex - allocations: [Allocation, Allocation]; // Asset allocation for each participant - sigs: Signature[]; // State hash signatures -} -``` - -Represents a complete state channel state, including allocations and signatures. - -## Configuration Types - -### NitroliteClientConfig - -```typescript -interface NitroliteClientConfig { - /** The viem PublicClient for reading blockchain data. */ - publicClient: PublicClient; - - /** - * The viem WalletClient used for: - * 1. Sending on-chain transactions in direct execution methods (e.g., `client.deposit`). - * 2. Providing the 'account' context for transaction preparation (`client.txPreparer`). - * 3. Signing off-chain states *if* `stateWalletClient` is not provided. - */ - walletClient: WalletClient>; - - /** - * Optional: A separate viem WalletClient used *only* for signing off-chain state updates (`signMessage`). - * Provide this if you want to use a different key (e.g., a "hot" key from localStorage) - * for state signing than the one used for on-chain transactions. - * If omitted, `walletClient` will be used for state signing. - */ - stateWalletClient?: WalletClient>; - - /** Contract addresses required by the SDK. */ - addresses: ContractAddresses; - - /** Chain ID for the channel */ - chainId: number; - - /** Optional: Default challenge duration (in seconds) for new channels. Defaults to 0 if omitted. */ - challengeDuration?: bigint; -} -``` - -Configuration for initializing the `NitroliteClient`. - -### ContractAddresses - -```typescript -interface ContractAddresses { - custody: Address; // Custody contract address - adjudicator: Address; // Adjudicator contract address - guestAddress: Address; // Guest participant address - tokenAddress: Address; // Token address (zero address for ETH) -} -``` - -Addresses of contracts used by the Nitrolite SDK. - -## Channel Lifecycle Parameter Types - -### 1. Deposit Phase - -Deposit operations primarily use simple `bigint` parameters for amounts. - -### 2. Channel Creation - -```typescript -interface CreateChannelParams { - initialAllocationAmounts: [bigint, bigint]; // Initial allocation for [host, guest] - stateData?: Hex; // Application-specific data -} -``` - -Parameters for creating a new channel. - -### 3. Channel Operations - -```typescript -interface CheckpointChannelParams { - channelId: ChannelId; // Channel ID to checkpoint - candidateState: State; // State to checkpoint - proofStates?: State[]; // Optional proof states -} - -interface ChallengeChannelParams { - channelId: ChannelId; // Channel ID to challenge - candidateState: State; // State to submit as a challenge - proofStates?: State[]; // Optional proof states -} - -interface ResizeChannelParams { - resizeState: { - channelId: ChannelId; - stateData: Hex; - allocations: [Allocation, Allocation]; - version: bigint; - intent: StateIntent; - serverSignature: Signature; - }; - proofStates: State[]; -} -``` - -Parameters for channel operations. - -### 4. Channel Closing - -```typescript -interface CloseChannelParams { - stateData?: Hex; // Optional application data for the final state - finalState: { - channelId: ChannelId; // Channel ID to close - stateData: Hex; // Application-specific data - allocations: [Allocation, Allocation]; // Final allocations - version: bigint; // State version - serverSignature: Signature; // Server's signature on the state - }; -} -``` - -Parameters for collaboratively closing a channel. - -## Return Types - -### AccountInfo - -```typescript -interface AccountInfo { - available: bigint; // Available funds in the custody contract - channelCount: bigint; // Number of channels -} -``` - -Information about an account's funds in the custody contract. - -### PreparedTransaction - -```typescript -type PreparedTransaction = { - to: Address; // Target contract address - data?: Hex; // Contract call data - value?: bigint; // ETH value to send -}; -``` - -Represents the data needed to construct a transaction, used by the transaction preparer for Account Abstraction. - -## Type Usage By Channel Lifecycle Phase - -### 1. Deposit Phase - -```typescript -// Deposit -await client.deposit(amount: bigint): Promise - -// Check token details -const balance: bigint = await client.getTokenBalance() -const allowance: bigint = await client.getTokenAllowance() -``` - -### 2. Channel Creation Phase - -```typescript -// Create channel -const result: { - channelId: ChannelId; - initialState: State; - txHash: Hash -} = await client.createChannel({ - initialAllocationAmounts: [bigint, bigint], - stateData: Hex -}) -``` - -### 3. Channel Operation Phase - -```typescript -// Resize -await client.resizeChannel({ - resizeState: { - channelId: ChannelId, - stateData: Hex, - allocations: [Allocation, Allocation], - version: bigint, - intent: StateIntent, - serverSignature: Signature - }, - proofStates: State[] -}): Promise - -// Resize is structured as: -const resizeParams: ResizeChannelParams = { - resizeState: { - channelId, - stateData: '0x1234', - allocations: [ - { destination: addr1, token: tokenAddr, amount: 600000n }, - { destination: addr2, token: tokenAddr, amount: 400000n } - ], - version: 2n, // Incremented from previous - intent: StateIntent.RESIZE, - serverSignature: signature - }, - proofStates: [] -} -``` - -### 4. Channel Closing Phase - -```typescript -// Close -await client.closeChannel({ - finalState: { - channelId: ChannelId, - stateData: Hex, - allocations: [Allocation, Allocation], - version: bigint, - serverSignature: Signature - } -}): Promise -``` - -### 5. Withdrawal Phase - -```typescript -// Withdraw -await client.withdrawal(amount: bigint): Promise -``` - -## State Intent Lifecycle - -The `StateIntent` enum value determines how a state is interpreted: - -1. `StateIntent.INITIALIZE`: Used when creating a channel, defines the initial funding allocations -2. `StateIntent.OPERATE`: Used during normal operations, for application-specific state updates -3. `StateIntent.RESIZE`: Used when changing allocation amounts, e.g., adding funds to a channel -4. `StateIntent.FINALIZE`: Used when closing a channel, defines the final allocations - -Example of state progression: - -```typescript -// 1. Initial state (on channel creation) -const initialState = { - intent: StateIntent.INITIALIZE, - version: 0n, - data: '0x1234', - allocations: [ - { destination: userAddr, token: tokenAddr, amount: 700000n }, - { destination: guestAddr, token: tokenAddr, amount: 300000n } - ], - sigs: [userSig, guestSig] -}; - -// 2. Operation state (during application usage) -const operationalState = { - intent: StateIntent.OPERATE, - version: 1n, // Incremented - data: '0x5678', // Application data changed - allocations: [ - { destination: userAddr, token: tokenAddr, amount: 650000n }, // Balance changed - { destination: guestAddr, token: tokenAddr, amount: 350000n } // Balance changed - ], - sigs: [userSig, guestSig] -}; - -// 3. Resize state (adding funds) -const resizeState = { - intent: StateIntent.RESIZE, - version: 2n, - data: '0x5678', - allocations: [ - { destination: userAddr, token: tokenAddr, amount: 950000n }, // Added funds - { destination: guestAddr, token: tokenAddr, amount: 450000n } // Added funds - ], - sigs: [userSig, guestSig] -}; - -// 4. Final state (closing channel) -const finalState = { - intent: StateIntent.FINALIZE, - version: 3n, - data: '0x9ABC', - allocations: [ - { destination: userAddr, token: tokenAddr, amount: 930000n }, - { destination: guestAddr, token: tokenAddr, amount: 470000n } - ], - sigs: [userSig, guestSig] -}; -``` diff --git a/erc7824-docs/docs/nitrolite_rpc/index.md b/erc7824-docs/docs/nitrolite_rpc/index.md deleted file mode 100644 index 75d0d1f7d..000000000 --- a/erc7824-docs/docs/nitrolite_rpc/index.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -sidebar_position: 3 -title: NitroliteRPC -description: Overview of the NitroliteRPC, its core logic, and links to detailed API and type definitions. -keywords: [erc7824, statechannels, state channels, nitrolite, rpc, websockets, messaging, protocol] ---- - -import { Card, CardGrid } from '@site/src/components/Card'; -import MethodDetails from '@site/src/components/MethodDetails'; - -# NitroliteRPC - -The NitroliteRPC provides a secure, reliable real-time communication protocol for state channel applications. It enables off-chain message exchange, state updates, and channel management. This system is built around the `NitroliteRPC` class, which provides the foundational methods for message construction, signing, parsing, and verification. - - - - - - -## Core Logic: The `NitroliteRPC` Class - -The `NitroliteRPC` class is central to the RPC system. It offers a suite of static methods to handle the low-level details of the NitroliteRPC protocol. - -### Message Creation - - - - - - - - - -### Message Signing - -", description: "The RPC request object to sign." }, - { name: "signer", type: "MessageSigner", description: "An async function that takes a message string and returns a Promise (signature)." } - ]} - returns="Promise>" - example={`// Assuming 'request' is a NitroliteRPCRequest and 'signer' is a MessageSigner -const signedRequest = await NitroliteRPC.signRequestMessage(request, signer);`} -/> - -", description: "The RPC response object to sign." }, - { name: "signer", type: "MessageSigner", description: "An async function that takes a message string and returns a Promise (signature)." } - ]} - returns="Promise>" - example={`// Assuming 'response' is a NitroliteRPCResponse and 'signer' is a MessageSigner -const signedResponse = await NitroliteRPC.signResponseMessage(response, signer);`} -/> - -### Message Parsing & Validation - - - -These methods ensure that all communication adheres to the defined RPC structure and security requirements. - -## Generic Message Structure - -The `NitroliteRPC` class operates on messages adhering to the following general structures. For precise details on each field and for specific message types, please refer to the [RPC Type Definitions](./rpc_types). - -```typescript -// Generic Request message structure -{ - "req": [requestId, method, params, timestamp], // Core request tuple - "int"?: Intent, // Optional intent for state changes - "acc"?: AccountID, // Optional account scope (channel/app ID) - "sig": [signature] // Array of signatures -} - -// Generic Response message structure -{ - "res": [requestId, method, dataPayload, timestamp], // Core response tuple - "acc"?: AccountID, // Optional account scope - "int"?: Intent, // Optional intent - "sig"?: [signature] // Optional signatures for certain response types -} -``` - -## Next Steps - -Dive deeper into the specifics of the RPC system: - - - - - \ No newline at end of file diff --git a/erc7824-docs/docs/nitrolite_rpc/message_creation_api.md b/erc7824-docs/docs/nitrolite_rpc/message_creation_api.md deleted file mode 100644 index 39e17ed53..000000000 --- a/erc7824-docs/docs/nitrolite_rpc/message_creation_api.md +++ /dev/null @@ -1,403 +0,0 @@ ---- -sidebar_position: 3 -title: RPC Message Creation API -description: Detailed reference for functions that create specific, signed Nitrolite RPC request messages. -keywords: [erc7824, statechannels, state channels, nitrolite, rpc, api, message creation, sdk] ---- - -import MethodDetails from '@site/src/components/MethodDetails'; - -# RPC Message Creation API - -The functions detailed below are part of the `@erc7824/nitrolite` SDK and provide a convenient way to create fully formed, signed, and stringified JSON RPC request messages. These messages are ready for transmission over a WebSocket or any other transport layer communicating with a Nitrolite-compatible broker. - -Each function typically takes a `MessageSigner` (a function you provide to sign messages, usually integrating with a user's wallet) and other relevant parameters, then returns a `Promise` which resolves to the JSON string of the signed RPC message. - -## Authentication - -These functions are used for the client authentication flow with the broker. The typical sequence is: -1. Client sends an `auth_request` (using `createAuthRequestMessage`). -2. Broker responds with an `auth_challenge`. -3. Client signs the challenge and sends an `auth_verify` (using `createAuthVerifyMessageFromChallenge` or `createAuthVerifyMessage`). -4. Broker responds with `auth_success` or `auth_failure`. - -```mermaid -sequenceDiagram - participant Client - participant Broker - - Client->>Broker: auth_request (via createAuthRequestMessage) - Broker-->>Client: auth_challenge (response) - Client->>Broker: auth_verify (via createAuthVerifyMessage or createAuthVerifyMessageFromChallenge) - Broker-->>Client: auth_success / auth_failure (response) -``` - - - - - - - -## General & Keep-Alive - -Functions for general RPC interactions like keep-alive messages. - - - -## Query Operations - -Functions for retrieving information from the broker. - - - - - - - - - -## Application Session Management - -Functions for creating and closing application sessions (state channels). - - - - - -## Application-Specific Messaging - -Function for sending messages within an active application session. - - - -## Ledger Channel Management - -Functions for managing the underlying ledger channels (direct channels with the broker). - - - - - -## Advanced: Creating a Local Signer for Development - -To begin, you'll often need a fresh key pair (private key, public key, and address). The `generateKeyPair` utility can be used for this: - -```typescript -import { ethers } from "ethers"; // Make sure ethers is installed - -// Definition (as provided in your example) -interface CryptoKeypair { - privateKey: string; - publicKey: string; - address: string; -} - -export const generateKeyPair = async (): Promise => { - try { - const wallet = ethers.Wallet.createRandom(); - const privateKeyHash = ethers.utils.keccak256(wallet.privateKey); - const walletFromHashedKey = new ethers.Wallet(privateKeyHash); - - return { - privateKey: privateKeyHash, - publicKey: walletFromHashedKey.publicKey, - address: walletFromHashedKey.address, - }; - } catch (error) { - console.error('Error generating keypair, using fallback:', error); - const randomHex = ethers.utils.randomBytes(32); - const privateKey = ethers.utils.keccak256(randomHex); - const wallet = new ethers.Wallet(privateKey); - - return { - privateKey: privateKey, - publicKey: wallet.publicKey, - address: wallet.address, - }; - } -}; - -// Usage: -async function main() { - const keyPair = await generateKeyPair(); - console.log("Generated Private Key:", keyPair.privateKey); - console.log("Generated Address:", keyPair.address); - // Store keyPair.privateKey securely if you need to reuse this signer -} -``` -This function creates a new random wallet, hashes its private key for deriving a new one (a common pattern for deterministic key generation or added obfuscation, though the security implications depend on the exact use case), and returns the private key, public key, and address. - -### Creating a Signer from a Private Key - -Once you have a private key (either generated as above or from a known development account), you can create a `MessageSigner` compatible with the RPC message creation functions. The `MessageSigner` interface typically expects an asynchronous `sign` method. - -```typescript -import { ethers } from "ethers"; -import { Hex } from "viem"; // Assuming Hex type is from viem or similar - -// Definitions (as provided in your example) -type RequestData = unknown; // Placeholder for actual request data type -type ResponsePayload = unknown; // Placeholder for actual response payload type - -interface WalletSigner { - publicKey: string; - address: Hex; - sign: (payload: RequestData | ResponsePayload) => Promise; -} - -export const createEthersSigner = (privateKey: string): WalletSigner => { - try { - const wallet = new ethers.Wallet(privateKey); - - return { - publicKey: wallet.publicKey, - address: wallet.address as Hex, - sign: async (payload: RequestData | ResponsePayload): Promise => { - try { - // The NitroliteRPC.hashMessage method should ideally be used here - // to ensure the exact same hashing logic as the SDK internals. - // For demonstration, using a generic hashing approach: - const messageToSign = JSON.stringify(payload); - const messageHash = ethers.utils.id(messageToSign); // ethers.utils.id performs keccak256 - const messageBytes = ethers.utils.arrayify(messageHash); - - const flatSignature = await wallet._signingKey().signDigest(messageBytes); - const signature = ethers.utils.joinSignature(flatSignature); - return signature as Hex; - } catch (error) { - console.error('Error signing message:', error); - throw error; - } - }, - }; - } catch (error) { - console.error('Error creating ethers signer:', error); - throw error; - } -}; - -// Usage: -async function setupSigner() { - const keyPair = await generateKeyPair(); // Or use a known private key - const localSigner = createEthersSigner(keyPair.privateKey); - - // Now 'localSigner' can be passed to the RPC message creation functions: - // const authRequest = await createAuthRequestMessage(localSigner.sign, localSigner.address as Address); - // console.log("Auth Request with local signer:", authRequest); -} -``` - -**Important Considerations for `createEthersSigner`:** -* **Hashing Consistency:** The `sign` method within `createEthersSigner` must hash the payload in a way that is **identical** to how the `NitroliteRPC` class (specifically `NitroliteRPC.hashMessage`) expects messages to be hashed before signing. The example above uses `ethers.utils.id(JSON.stringify(payload))`. It's crucial to verify if the SDK's internal hashing uses a specific message prefix (e.g., EIP-191 personal_sign prefix) or a different serialization method. If the SDK does *not* use a standard EIP-191 prefix, or uses a custom one, your local signer's hashing logic must replicate this exactly for signatures to be valid. Using `NitroliteRPC.hashMessage(payload)` directly (if `payload` matches the `NitroliteRPCMessage` structure) is the safest way to ensure consistency. -* **Type Compatibility:** Ensure the `Address` type expected by functions like `createAuthRequestMessage` is compatible with `localSigner.address`. The example uses `localSigner.address as Address` assuming `Address` is `0x${string}`. -* **Error Handling:** The provided examples include basic error logging. Robust applications should implement more sophisticated error handling. diff --git a/erc7824-docs/docs/nitrolite_rpc/rpc_types.md b/erc7824-docs/docs/nitrolite_rpc/rpc_types.md deleted file mode 100644 index 2848c3156..000000000 --- a/erc7824-docs/docs/nitrolite_rpc/rpc_types.md +++ /dev/null @@ -1,296 +0,0 @@ ---- -sidebar_position: 4 # Adjusted sidebar_position if message_creation_api is 3 -title: RPC Type Definitions -description: Comprehensive type definitions for the Nitrolite RPC protocol. -keywords: [erc7824, statechannels, state channels, nitrolite, rpc, types, typescript, protocol, api, definitions] ---- - -# Nitrolite RPC Type Definitions - -This page provides a comprehensive reference for all TypeScript types, interfaces, and enums used by the Nitrolite RPC system, as defined in the `@erc7824/nitrolite` SDK. These definitions are crucial for understanding the structure of messages exchanged with the Nitrolite broker. - -## Core Types - -These are fundamental types used throughout the RPC system. - -### `RequestID` -A unique identifier for an RPC request. Typically a number. -```typescript -export type RequestID = number; -``` - -### `Timestamp` -Represents a Unix timestamp in milliseconds. Used for message ordering and security. -```typescript -export type Timestamp = number; -``` - -### `AccountID` -A unique identifier for a channel or application session, represented as a hexadecimal string. -```typescript -export type AccountID = Hex; // from 'viem' -``` - -### `Intent` -Represents the allocation intent change as an array of big integers. This is used to specify how funds should be re-distributed in a state update. -```typescript -export type Intent = bigint[]; -``` - -## Message Payloads - -These types define the core data arrays within RPC messages. - -### `RequestData` -The structured data payload within a request message. -```typescript -export type RequestData = [RequestID, string, any[], Timestamp?]; -``` -- `RequestID`: The unique ID of this request. -- `string`: The name of the RPC method being called. -- `any[]`: An array of parameters for the method. -- `Timestamp?`: An optional timestamp for when the request was created. - -### `ResponseData` -The structured data payload within a successful response message. -```typescript -export type ResponseData = [RequestID, string, any[], Timestamp?]; -``` -- `RequestID`: The ID of the original request this response is for. -- `string`: The name of the original RPC method. -- `any[]`: An array containing the result(s) of the method execution. -- `Timestamp?`: An optional timestamp for when the response was created. - -### `NitroliteRPCErrorDetail` -Defines the structure of the error object within an error response. -```typescript -export interface NitroliteRPCErrorDetail { - error: string; -} -``` -- `error`: A string describing the error that occurred. - -### `ErrorResponseData` -The structured data payload for an error response message. -```typescript -export type ErrorResponseData = [RequestID, "error", [NitroliteRPCErrorDetail], Timestamp?]; -``` -- `RequestID`: The ID of the original request this error is for. -- `"error"`: A literal string indicating this is an error response. -- `[NitroliteRPCErrorDetail]`: An array containing a single `NitroliteRPCErrorDetail` object. -- `Timestamp?`: An optional timestamp for when the error response was created. - -### `ResponsePayload` -A union type representing the payload of a response, which can be either a success (`ResponseData`) or an error (`ErrorResponseData`). -```typescript -export type ResponsePayload = ResponseData | ErrorResponseData; -``` - -## Message Envelopes - -These interfaces define the overall structure of messages sent over the wire. - -### `NitroliteRPCMessage` -The base wire format for Nitrolite RPC messages. -```typescript -export interface NitroliteRPCMessage { - req?: RequestData; - res?: ResponsePayload; - int?: Intent; - sig?: Hex[]; -} -``` -- `req?`: The request payload, if this is a request message. -- `res?`: The response payload, if this is a response message. -- `int?`: Optional allocation intent change. -- `sig?`: Optional array of cryptographic signatures (hex strings). - -## Parsing Results - -### `ParsedResponse` -Represents the result of parsing an incoming Nitrolite RPC response message. -```typescript -export interface ParsedResponse { - isValid: boolean; - error?: string; - isError?: boolean; - requestId?: RequestID; - method?: string; - data?: any[] | NitroliteRPCErrorDetail; - acc?: AccountID; - int?: Intent; - timestamp?: Timestamp; -} -``` -- `isValid`: `true` if the message was successfully parsed and passed basic structural validation. -- `error?`: If `isValid` is `false`, contains a description of the parsing or validation error. -- `isError?`: `true` if the parsed response represents an error (i.e., `method === "error"`). Undefined if `isValid` is `false`. -- `requestId?`: The `RequestID` from the response payload. Undefined if the structure is invalid. -- `method?`: The method name from the response payload. Undefined if the structure is invalid. -- `data?`: The extracted data payload (result array for success, `NitroliteRPCErrorDetail` object for error). Undefined if the structure is invalid or the error payload is malformed. -- `acc?`: The `AccountID` from the message envelope, if present. -- `int?`: The `Intent` from the message envelope, if present. -- `timestamp?`: The `Timestamp` from the response payload. Undefined if the structure is invalid. - -## Request Parameter Structures - -These interfaces define the expected parameters for specific RPC methods. - -### `AppDefinition` -Defines the structure of an application's configuration. -```typescript -export interface AppDefinition { - protocol: string; - participants: Hex[]; - weights: number[]; - quorum: number; - challenge: number; - nonce?: number; -} -``` -- `protocol`: The protocol identifier or name for the application logic (e.g., `"NitroRPC/0.2"`). -- `participants`: An array of participant addresses (Ethereum addresses as `Hex`) involved in the application. -- `weights`: An array representing the relative weights or stakes of participants. Order corresponds to the `participants` array. -- `quorum`: The number/percentage of participants (based on weights) required to reach consensus. -- `challenge`: A parameter related to the challenge period or mechanism (e.g., duration in seconds). -- `nonce?`: An optional unique number (nonce) used to ensure the uniqueness of the application instance and prevent replay attacks. - -### `CreateAppSessionRequest` -Parameters for the `create_app_session` RPC method. -```typescript -export interface CreateAppSessionRequest { - definition: AppDefinition; - token: Hex; - allocations: bigint[]; -} -``` -- `definition`: The `AppDefinition` object detailing the application being created. -- `token`: The `Hex` address of the ERC20 token contract used for allocations within this application session. -- `allocations`: An array of `bigint` representing the initial allocation distribution among participants. The order corresponds to the `participants` array in the `definition`. - -Example: -```json -{ - "definition": { - "protocol": "NitroRPC/0.2", - "participants": [ - "0xAaBbCcDdEeFf0011223344556677889900aAbBcC", - "0x00112233445566778899AaBbCcDdEeFf00112233" - ], - "weights": [100, 0], // Example: Participant 1 has 100% weight - "quorum": 100, // Example: 100% quorum needed - "challenge": 86400, // Example: 1 day challenge period - "nonce": 12345 - }, - "token": "0xTokenContractAddress00000000000000000000", - "allocations": ["1000000000000000000", "0"] // 1 Token for P1, 0 for P2 (as strings for bigint) -} -``` - -### `CloseAppSessionRequest` -Parameters for the `close_app_session` RPC method. -```typescript -export interface CloseAppSessionRequest { - app_id: Hex; - allocations: bigint[]; -} -``` -- `app_id`: The unique `AccountID` (as `Hex`) of the application session to be closed. -- `allocations`: An array of `bigint` representing the final allocation distribution among participants upon closing. Order corresponds to the `participants` array in the application's definition. - -### `ResizeChannel` -Parameters for the `resize_channel` RPC method. -```typescript -export interface ResizeChannel { - channel_id: Hex; - participant_change: bigint; - funds_destination: Hex; -} -``` -- `channel_id`: The unique `AccountID` (as `Hex`) of the direct ledger channel to be resized. -- `participant_change`: The `bigint` amount by which the participant's allocation in the channel should change (positive to add funds, negative to remove). -- `funds_destination`: The `Hex` address where funds will be sent if `participant_change` is negative (withdrawal), or the source of funds if positive (though typically handled by prior on-chain deposit). - -## Function Types (Signers & Verifiers) - -These types define the signatures for functions used in cryptographic operations. - -### `MessageSigner` -A function that signs a message payload. -```typescript -export type MessageSigner = (payload: RequestData | ResponsePayload) => Promise; -``` -- Takes a `RequestData` or `ResponsePayload` object (the array part of the message). -- Returns a `Promise` that resolves to the cryptographic signature as a `Hex` string. - -### `SingleMessageVerifier` -A function that verifies a single message signature. -```typescript -export type SingleMessageVerifier = ( - payload: RequestData | ResponsePayload, - signature: Hex, - address: Address // from 'viem' -) => Promise; -``` -- Takes the `RequestData` or `ResponsePayload` object, the `Hex` signature, and the expected signer's `Address`. -- Returns a `Promise` that resolves to `true` if the signature is valid for the given payload and address, `false` otherwise. - -## Usage Examples - -### Creating Message Payloads and Envelopes -```typescript -// Example Request Payload (for a 'ping' method) -const pingRequestData: RequestData = [1, "ping", []]; // Assuming timestamp is added by sender utility - -// Example Request Envelope -const pingRequestMessage: NitroliteRPCMessage = { - req: pingRequestData, - // sig: ["0xSignatureIfPreSigned..."] // Signature added by signing utility -}; - -// Example Application-Specific Request -const appActionData: RequestData = [2, "message", [{ move: "rock" }], Date.now()]; -const appActionMessage: ApplicationRPCMessage = { - sid: "0xAppSessionId...", - req: appActionData, - // sig: ["0xSignature..."] -}; - -// Example Successful Response Payload -const pongResponseData: ResponseData = [1, "ping", ["pong"], Date.now()]; - -// Example Error Detail -const errorDetail: NitroliteRPCErrorDetail = { error: "Method parameters are invalid." }; - -// Example Error Response Payload -const errorResponseData: ErrorResponseData = [2, "error", [errorDetail], Date.now()]; - -// Example Response Envelope (Success) -const successResponseEnvelope: NitroliteRPCMessage = { - res: pongResponseData, -}; -``` - -### Working with Signers (Conceptual) -```typescript -// Conceptual: How a MessageSigner might be used -async function signAndSend(payload: RequestData, signer: MessageSigner, sendMessageToServer: (msg: string) => void) { - const signature = await signer(payload); - const message: NitroliteRPCMessage = { - req: payload, - sig: [signature] - }; - sendMessageToServer(JSON.stringify(message)); -} -``` - -## Implementation Considerations - -When working with these types: - -1. **Serialization**: Messages are typically serialized to JSON strings for transmission (e.g., over WebSockets). -2. **Signing**: Payloads (`req` or `res` arrays) are what get signed, not the entire envelope. The resulting signature is then added to the `sig` field of the `NitroliteRPCMessage` envelope. -3. **Validation**: Always validate the structure and types of incoming messages against these definitions, preferably using utilities provided by the SDK. -4. **Error Handling**: Properly check for `isError` in `ParsedResponse` and use `NitroliteErrorCode` to understand the nature of failures. -5. **BigInts**: Note the use of `bigint` for `Intent` and allocation amounts. Ensure your environment and serialization/deserialization logic handle `bigint` correctly (e.g., converting to/from strings for JSON). -6. **Hex Strings**: Types like `AccountID`, `Hex` (for signatures, token addresses) imply hexadecimal string format (e.g., `"0x..."`). diff --git a/erc7824-docs/docs/quick_start/application_session.md b/erc7824-docs/docs/quick_start/application_session.md deleted file mode 100644 index 08f1e1e0c..000000000 --- a/erc7824-docs/docs/quick_start/application_session.md +++ /dev/null @@ -1,1071 +0,0 @@ ---- -sidebar_position: 7 -title: Create Application Sessions -description: Create and manage off-chain application sessions to interact with ClearNodes. -keywords: [erc7824, nitrolite, application session, state channels, app session] ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Create Application Sessions - -After connecting to a ClearNode and checking your channel balances, you can create application sessions to interact with specific applications on the state channel network. Application sessions allow you to perform off-chain transactions and define custom behavior for your interactions. - -## Understanding Application Sessions - -```mermaid -graph TD - A[Create Application Session] --> B[Session Active] - B --> C[Off-Chain Transactions] - C --> D[Close Session] -``` - -Application sessions in Nitrolite allow you to: - -- Create isolated environments for specific interactions -- Define rules for off-chain transactions -- Specify how funds are allocated between participants -- Implement custom application logic and state management - -An application session serves as a mechanism to track and manage interactions between participants, with the ClearNode acting as a facilitator. - -## Creating an Application Session - -To create an application session, you'll use the `createAppSessionMessage` helper from NitroliteRPC. Here's how to do it: - - - - -```javascript -import { createAppSessionMessage, parseRPCResponse, MessageSigner, CreateAppSessionRPCParams } from '@erc7824/nitrolite'; -import { useCallback } from 'react'; -import { Address } from 'viem'; - -function useCreateApplicationSession() { - const createApplicationSession = useCallback( - async ( - signer: MessageSigner, - sendRequest: (message: string) => Promise, - participantA: Address, - participantB: Address, - amount: string, - ) => { - try { - // Define the application parameters - const appDefinition = { - protocol: 'nitroliterpc', - participants: [participantA, participantB], - weights: [100, 0], // Weight distribution for consensus - quorum: 100, // Required consensus percentage - challenge: 0, // Challenge period - nonce: Date.now(), // Unique identifier - }; - - // Define allocations with asset type instead of token address - const allocations = [ - { - participant: participantA, - asset: 'usdc', - amount: amount, - }, - { - participant: participantB, - asset: 'usdc', - amount: '0', - }, - ]; - - // Create a signed message using the createAppSessionMessage helper - const signedMessage = await createAppSessionMessage( - signer, - [ - { - definition: appDefinition, - allocations: allocations, - }, - ] - ); - - // Send the signed message to the ClearNode - const response = await sendRequest(signedMessage); - - // Handle the response - if (response.app_session_id) { - // Store the app session ID for future reference - localStorage.setItem('app_session_id', response.app_session_id); - return { success: true, app_session_id: response.app_session_id, response }; - } else { - return { success: true, response }; - } - } catch (error) { - console.error('Error creating application session:', error); - return { - success: false, - error: error instanceof Error - ? error.message - : 'Unknown error during session creation', - }; - } - }, - [] - ); - - return { createApplicationSession }; -} - -// Usage example -function MyComponent() { - const { createApplicationSession } = useCreateApplicationSession(); - - const handleCreateSession = async () => { - // Define your WebSocket send wrapper - const sendRequest = async (payload: string) => { - return new Promise((resolve, reject) => { - // Assuming ws is your WebSocket connection - const handleMessage = (event) => { - try { - const message = parseRPCResponse(event.data); - if (message.method === RPCMethod.CreateAppSession) { - ws.removeEventListener('message', handleMessage); - resolve(message.params); - } - } catch (error) { - console.error('Error parsing message:', error); - } - }; - - ws.addEventListener('message', handleMessage); - ws.send(payload); - - // Set timeout to prevent hanging - setTimeout(() => { - ws.removeEventListener('message', handleMessage); - reject(new Error('App session creation timeout')); - }, 10000); - }); - }; - - const result = await createApplicationSession( - walletSigner, // Your signer object - sendRequest, // Function to send the request - '0xYourAddress', // Your address - '0xOtherAddress', // Other participant's address - '100', // Amount - ); - - if (result.success) { - console.log(`Application session created with ID: ${result.app_session_id}`); - } else { - console.error(`Failed to create application session: ${result.error}`); - } - }; - - return ( - - ); -} -``` - - - - -```typescript -// app-session.service.ts -import { Injectable } from '@angular/core'; -import { createAppSessionMessage } from '@erc7824/nitrolite'; -import { ethers } from 'ethers'; -import { BehaviorSubject, Observable, from } from 'rxjs'; -import { tap, catchError } from 'rxjs/operators'; - -@Injectable({ - providedIn: 'root' -}) -export class AppSessionService { - private webSocket: WebSocket | null = null; - private appIdSubject = new BehaviorSubject(null); - - public appId$ = this.appIdSubject.asObservable(); - - constructor() { - // Retrieve app ID from storage if available - const storedAppId = localStorage.getItem('app_session_id'); - if (storedAppId) { - this.appIdSubject.next(storedAppId); - } - } - - public setWebSocket(ws: WebSocket): void { - this.webSocket = ws; - } - - public createApplicationSession( - signer: any, - participantA: string, - participantB: string, - amount: string, - ): Observable { - if (!this.webSocket) { - throw new Error('WebSocket connection is not established'); - } - - return from(this.createAppSessionAsync( - signer, - participantA, - participantB, - amount, - )).pipe( - tap(result => { - if (result.success && result.app_session_id) { - localStorage.setItem('app_session_id', result.app_session_id); - this.appIdSubject.next(result.app_session_id); - } - }), - catchError(error => { - console.error('Error creating application session:', error); - throw error; - }) - ); - } - - private async createAppSessionAsync( - signer: any, - participantA: string, - participantB: string, - amount: string, - ): Promise { - try { - - // Define the application parameters - const appDefinition = { - protocol: 'nitroliterpc', - participants: [participantA, participantB], - weights: [100, 0], - quorum: 100, - challenge: 0, - nonce: Date.now(), - }; - - // Define the allocations with asset type - const allocations = [ - { - participant: participantA, - asset: 'usdc', - amount: amount, - }, - { - participant: participantB, - asset: 'usdc', - amount: '0', - }, - ]; - - // Create message signer function - const messageSigner = async (payload: any) => { - const message = JSON.stringify(payload); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const signature = await signer.signMessage(messageBytes); - return signature; - }; - - // Create the signed message - const signedMessage = await createAppSessionMessage( - messageSigner, - [ - { - definition: appDefinition, - allocations: allocations, - }, - ] - ); - - // Send the message and wait for response - return await this.sendRequest(signedMessage); - } catch (error) { - console.error('Error in createAppSessionAsync:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - private sendRequest(payload: string): Promise { - return new Promise((resolve, reject) => { - if (!this.webSocket) { - reject(new Error('WebSocket not connected')); - return; - } - - const handleMessage = (event: MessageEvent) => { - try { - const message = JSON.parse(event.data); - if (message.res && message.res[1] === 'create_app_session') { - this.webSocket?.removeEventListener('message', handleMessage); - resolve({ - success: true, - app_session_id: message.res[2]?.[0]?.app_session_id || null, - status: message.res[2]?.[0]?.status || "open", - response: message.res[2] - }); - } - - if (message.err) { - this.webSocket?.removeEventListener('message', handleMessage); - reject(new Error(`Error: ${message.err[1]} - ${message.err[2]}`)); - } - } catch (error) { - console.error('Error parsing message:', error); - } - }; - - this.webSocket.addEventListener('message', handleMessage); - this.webSocket.send(payload); - - // Set timeout to prevent hanging - setTimeout(() => { - this.webSocket?.removeEventListener('message', handleMessage); - reject(new Error('App session creation timeout')); - }, 10000); - }); - } -} - -// app-session.component.ts -import { Component, OnInit } from '@angular/core'; -import { AppSessionService } from './app-session.service'; - -@Component({ - selector: 'app-session-creator', - template: ` -
-

Create Application Session

-
- Current Application Session ID: {{ appId }} -
- -
- {{ error }} -
-
- `, - styles: [` - .app-session-container { - margin: 20px; - padding: 15px; - border: 1px solid #eee; - border-radius: 5px; - } - .error-message { - color: red; - margin-top: 10px; - } - `] -}) -export class AppSessionComponent implements OnInit { - appId: string | null = null; - isCreating = false; - error: string | null = null; - - constructor(private appSessionService: AppSessionService) {} - - ngOnInit(): void { - // Subscribe to app ID changes - this.appSessionService.appId$.subscribe(id => { - this.appId = id; - }); - - // Initialize WebSocket (implementation would depend on your setup) - const ws = new WebSocket('wss://your-clearnode-endpoint'); - ws.onopen = () => { - this.appSessionService.setWebSocket(ws); - }; - } - - createAppSession(): void { - this.isCreating = true; - this.error = null; - - // Sample values - in a real app you'd get these from input fields or a service - const participantA = '0xYourAddress'; - const participantB = '0xOtherAddress'; - const amount = '1000000'; // 1 USDC with 6 decimals - - // Assuming you have access to a signer (e.g., from MetaMask) - const signer = window.ethereum && new ethers.providers.Web3Provider(window.ethereum).getSigner(); - - if (!signer) { - this.error = 'No wallet connected'; - this.isCreating = false; - return; - } - - this.appSessionService.createApplicationSession( - signer, - participantA, - participantB, - amount - ).subscribe({ - next: (result) => { - console.log('App session created:', result); - this.isCreating = false; - }, - error: (err) => { - this.error = `Failed to create application session: ${err.message}`; - this.isCreating = false; - } - }); - } -} -``` - -
- - -```javascript - - - - - - -``` - - - - -```javascript -import { createAppSessionMessage } from '@erc7824/nitrolite'; -import WebSocket from 'ws'; -import { ethers } from 'ethers'; - -/** - * Create an app session - * @param {string} participantA - First participant's address - * @param {string} participantB - Second participant's address - * @param {WebSocket} ws - WebSocket connection to the ClearNode - * @param {object} wallet - Ethers wallet for signing - * @returns {Promise} The app session ID - */ -async function createAppSession(participantA, participantB, ws, wallet) { - try { - console.log(`Creating app session between ${participantA} and ${participantB}`); - - // Message signer function - const messageSigner = async (payload) => { - try { - const message = JSON.stringify(payload); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = wallet.signingKey.sign(messageBytes); - return signature; - } catch (error) { - console.error("Error signing message:", error); - throw error; - } - }; - - // Create app definition - const appDefinition = { - protocol: "nitroliterpc", - participants: [participantA, participantB], - weights: [100, 0], - quorum: 100, - challenge: 0, - nonce: Date.now(), - }; - - // Define the allocations with asset type (e.g., 1 USDC with 6 decimals) - const amount = '1000000'; - - // Define allocations - const allocations = [ - { - participant: participantA, - asset: 'usdc', - amount: amount, - }, - { - participant: participantB, - asset: 'usdc', - amount: '0', - }, - ]; - - // Create the signed message - const signedMessage = await createAppSessionMessage( - messageSigner, - [ - { - definition: appDefinition, - allocations: allocations, - }, - ] - ); - - // Send the message and wait for response - return new Promise((resolve, reject) => { - // Create a one-time message handler for the app session response - const handleAppSessionResponse = (data) => { - try { - const rawData = typeof data === 'string' ? data : data.toString(); - const message = JSON.parse(rawData); - - console.log('Received app session creation response:', message); - - // Check if this is an app session response - if (message.res && - (message.res[1] === 'create_app_session' || - message.res[1] === 'app_session_created')) { - // Remove the listener once we get the response - ws.removeListener('message', handleAppSessionResponse); - - // Extract app session ID from response - const appSessionId = message.res[2]?.[0]?.app_session_id; - if (!appSessionId) { - reject(new Error('Failed to get app session ID from response')); - return; - } - - resolve(appSessionId); - } - - // Check for error responses - if (message.err) { - ws.removeListener('message', handleAppSessionResponse); - reject(new Error(`Error ${message.err[1]}: ${message.err[2]}`)); - } - } catch (error) { - console.error('Error handling app session response:', error); - } - }; - - // Add the message handler - ws.on('message', handleAppSessionResponse); - - // Set timeout to prevent hanging - setTimeout(() => { - ws.removeListener('message', handleAppSessionResponse); - reject(new Error('App session creation timeout')); - }, 10000); - - // Send the signed message - ws.send(signedMessage); - }); - } catch (error) { - console.error('Error creating app session:', error); - throw error; - } -} - -// Usage example -const participantA = '0x1234...'; // Your address -const participantB = '0x5678...'; // Other participant's address - -// Assuming you have a WebSocket connection and wallet initialized -createAppSession(participantA, participantB, ws, wallet) - .then(appSessionId => { - console.log(`Application session created with ID: ${appSessionId}`); - // Store the app session ID for future reference - }) - .catch(error => { - console.error('Failed to create application session:', error); - }); -``` - - - - -```javascript -/** - * Nitrolite app sessions manager for game rooms - */ -import { createAppSessionMessage } from '@erc7824/nitrolite'; -import { ethers } from 'ethers'; - -// Map to store app sessions by room ID -const roomAppSessions = new Map(); - -/** - * Create an app session for a game room - * @param {string} roomId - Room ID - * @param {string} participantA - First player's address - * @param {string} participantB - Second player's address - * @param {WebSocket} ws - WebSocket connection to the ClearNode - * @param {object} wallet - Ethers wallet for signing - * @returns {Promise} The app session ID - */ -export async function createAppSession(roomId, participantA, participantB, ws, wallet) { - try { - console.log(`Creating app session for room ${roomId}`); - - // Get the server's address - const serverAddress = wallet.address; - - // Create app definition with server as a participant - // In this setup, the server acts as a referee/facilitator - const appDefinition = { - protocol: "app_nitrolite_v0", - participants: [participantA, participantB, serverAddress], - weights: [0, 0, 100], // Server has full control - quorum: 100, - challenge: 0, - nonce: Date.now(), - }; - - // Define allocations with asset type - const allocations = [ - { - participant: participantA, - asset: 'usdc', - amount: '0', - }, - { - participant: participantB, - asset: 'usdc', - amount: '0', - }, - { - participant: serverAddress, - asset: 'usdc', - amount: '0', - }, - ]; - - // Message signer function - const messageSigner = async (payload) => { - try { - const message = JSON.stringify(payload); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = wallet.signingKey.sign(messageBytes); - return signature; - } catch (error) { - console.error("Error signing message:", error); - throw error; - } - }; - - // Create the signed message - const signedMessage = await createAppSessionMessage( - messageSigner, - [ - { - definition: appDefinition, - allocations: allocations, - }, - ] - ); - - // Send the message and wait for response - return new Promise((resolve, reject) => { - // Create a one-time message handler for the app session response - const handleAppSessionResponse = (data) => { - try { - const rawData = typeof data === 'string' ? data : data.toString(); - const message = JSON.parse(rawData); - - console.log('Received app session creation response:', message); - - // Check if this is an app session response - if (message.res && - (message.res[1] === 'create_app_session' || - message.res[1] === 'app_session_created')) { - // Remove the listener once we get the response - ws.removeListener('message', handleAppSessionResponse); - - // Extract app session ID from response - const appSessionId = message.res[2]?.[0]?.app_session_id; - if (!appSessionId) { - reject(new Error('Failed to get app session ID from response')); - return; - } - - // Get status from response - const status = message.res[2]?.[0]?.status || "open"; - - // Store the app session for this room - roomAppSessions.set(roomId, { - appSessionId, - status, - participantA, - participantB, - serverAddress, - createdAt: Date.now() - }); - - resolve(appSessionId); - } - - // Check for error responses - if (message.err) { - ws.removeListener('message', handleAppSessionResponse); - reject(new Error(`Error ${message.err[1]}: ${message.err[2]}`)); - } - } catch (error) { - console.error('Error handling app session response:', error); - } - }; - - // Add the message handler - ws.on('message', handleAppSessionResponse); - - // Set timeout to prevent hanging - setTimeout(() => { - ws.removeListener('message', handleAppSessionResponse); - reject(new Error('App session creation timeout')); - }, 10000); - - // Send the signed message - ws.send(signedMessage); - }); - } catch (error) { - console.error(`Error creating app session for room ${roomId}:`, error); - throw error; - } -} - -/** - * Get the app session for a room - * @param {string} roomId - Room ID - * @returns {Object|null} The app session or null if not found - */ -export function getAppSession(roomId) { - return roomAppSessions.get(roomId) || null; -} - -/** - * Check if a room has an app session - * @param {string} roomId - Room ID - * @returns {boolean} Whether the room has an app session - */ -export function hasAppSession(roomId) { - return roomAppSessions.has(roomId); -} -``` - - -
- -## Key Components of an Application Session - -When creating an application session, you need to define several key components: - -| Component | Description | Example | -|-----------|-------------|---------| -| **Protocol** | Identifier for the application protocol | `"nitroliterpc"` | -| **Participants** | Array of participant addresses | `[userAddress, counterpartyAddress]` | -| **Weights** | Weight distribution for consensus | `[100, 0]` for user-controlled, `[50, 50]` for equal | -| **Quorum** | Required percentage for consensus | Usually `100` for full consensus | -| **Challenge** | Time period for disputing state | `0` for no challenge period | -| **Nonce** | Unique identifier | Typically `Date.now()` | -| **Allocations** | Array of allocation objects with: | `[{ participant: "0xAddress", asset: "usdc", amount: "100" }]` | -| | - participant: Address of the participant | | -| | - asset: Asset identifier (e.g., "usdc", "eth") | | -| | - amount: String amount with precision | | - -### Response Components - -When a ClearNode responds to your application session creation request, it provides: - -| Component | Description | Example | -|-----------|-------------|---------| -| **app_session_id** | Unique identifier for the application session | `"0x0ac588b2924edbbbe34bb4c51d089771bd7bd7018136c8c4317624112a8c9f79"` | -| **status** | Current state of the application session | `"open"` | - -## Understanding the Response - -When you create an application session, the ClearNode responds with information about the created session: - -```javascript -// Example response -{ - "res": [ - 2, // Request ID - "create_app_session", // Method name - [ - { - "app_session_id": "0x0ac588b2924edbbbe34bb4c51d089771bd7bd7018136c8c4317624112a8c9f79", // Session ID - "status": "open" - } - ], - 1631234567890 // Timestamp - ], - "sig": ["0xSignature"] -} -``` - -The most important part of the response is the `app_session_id`, which you'll need for all future interactions with this application session. - -## Application Session Use Cases - -Application sessions can be used for various scenarios: - -1. **Peer-to-peer payments**: Direct token transfers between users -2. **Gaming**: Turn-based games with state transitions -3. **Content access**: Pay-per-use access to digital content -4. **Service payments**: Metered payment for API or service usage -5. **Multi-party applications**: Applications involving more than two participants - -## Best Practices - -When working with application sessions, follow these best practices: - -1. **Store the app_session_id securely**: You'll need it for all session-related operations -2. **Verify session creation**: Check for successful creation before proceeding -3. **Handle timeouts**: Implement proper timeout handling for session creation -4. **Clean up listeners**: Always remove message event listeners to prevent memory leaks -5. **Handle errors gracefully**: Provide clear error messages to users - -## Next Steps - -After creating an application session, you can: - -1. Use the session for application-specific transactions -2. [Check your channel balances](balances) to monitor funds -3. [Close the application session](close_session) when you're done - -For advanced use cases, see our detailed documentation on application workflows. \ No newline at end of file diff --git a/erc7824-docs/docs/quick_start/balances.md b/erc7824-docs/docs/quick_start/balances.md deleted file mode 100644 index 5245797c9..000000000 --- a/erc7824-docs/docs/quick_start/balances.md +++ /dev/null @@ -1,396 +0,0 @@ ---- -sidebar_position: 6 -title: Channel Asset Management -description: Monitor off-chain balances in your active state channels using NitroliteRPC. -keywords: [erc7824, nitrolite, balances, off-chain, ledger balances, clearnode] ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Channel Asset Management - -After connecting to a ClearNode, you'll need to monitor the off-chain balances in your state channels. This guide explains how to retrieve and work with off-chain balance information using the NitroliteRPC protocol. - -## Understanding Off-Chain Balances - -Off-chain balances in Nitrolite represent: - -- Your current funds in the state channel -- Balances that update in real-time as transactions occur -- The source of truth for application operations -- Assets that are backed by on-chain deposits - -## Checking Off-Chain Balances - -To monitor your channel funds, you need to retrieve the current off-chain balances from the ClearNode. - -## Understanding the Ledger Balances Request - -The `get_ledger_balances` request is used to retrieve the current off-chain balances for a specific participant from the ClearNode: - -- **Request params**: `[{ participant: "0xAddress" }]` where `0xAddress` is the participant's address -- **Response**: Array containing the balances for different assets held by the participant - -The response contains a list of assets and their amounts for the specified participant. The balances are represented as strings with decimal precision, making it easier to display them directly without additional conversion. - -```javascript -// Example response format for get_ledger_balances -{ - "res": [1, "get_ledger_balances", [[ // The nested array contains balance data - { - "asset": "usdc", // Asset identifier - "amount": "100.0" // Amount as a string with decimal precision - }, - { - "asset": "eth", - "amount": "0.5" - } - ]], 1619123456789], // Timestamp - "sig": ["0xabcd1234..."] -} -``` - -To retrieve these balances, use the `get_ledger_balances` request with the ClearNode: - - - - -```javascript -import { createGetLedgerBalancesMessage, parseRPCMessage, RPCMethod } from '@erc7824/nitrolite'; -import { ethers } from 'ethers'; - -// Your message signer function (same as in auth flow) -const messageSigner = async (payload) => { - const message = JSON.stringify(payload); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = stateWallet.signingKey.sign(messageBytes); - return signature; -}; - -// Function to get ledger balances -async function getLedgerBalances(ws, participant) { - return new Promise((resolve, reject) => { - // Create a unique handler for this specific request - const handleMessage = (event) => { - const message = parseRPCMessage(event.data); - - // Check if this is a response to our get_ledger_balances request - if (message.method === RPCMethod.GetLedgerBalances) { - // Remove the message handler to avoid memory leaks - ws.removeEventListener('message', handleMessage); - - // Resolve with the balances data - resolve(message.params); - } - }; - - // Add the message handler - ws.addEventListener('message', handleMessage); - - // Create and send the ledger balances request - createGetLedgerBalancesMessage(messageSigner, participant) - .then(message => { - ws.send(message); - }) - .catch(error => { - // Remove the message handler on error - ws.removeEventListener('message', handleMessage); - reject(error); - }); - - // Set a timeout to prevent hanging indefinitely - setTimeout(() => { - ws.removeEventListener('message', handleMessage); - reject(new Error('Timeout waiting for ledger balances')); - }, 10000); // 10 second timeout - }); -} - -// Usage example -const participantAddress = '0x1234567890abcdef1234567890abcdef12345678'; - -try { - const balances = await getLedgerBalances(ws, participantAddress); - - console.log('Channel ledger balances:', balances); - // Example output: - // [ - // [ - // { "asset": "usdc", "amount": "100.0" }, - // { "asset": "eth", "amount": "0.5" } - // ] - // ] - - // Process your balances - if (balances.length > 0) { - // Display each asset balance - balances.forEach(balance => { - console.log(`${balance.asset.toUpperCase()} balance: ${balance.amount}`); - }); - - // Example: find a specific asset - const usdcBalance = balances.find(b => b.asset.toLowerCase() === 'usdc'); - if (usdcBalance) { - console.log(`USDC balance: ${usdcBalance.amount}`); - } - } else { - console.log('No balance data available'); - } -} catch (error) { - console.error('Failed to get ledger balances:', error); -} -``` - - - - -```javascript -import { ethers } from 'ethers'; -import { generateRequestId, getCurrentTimestamp, generateRequestId, parseRPCMessage, RPCMethod } from '@erc7824/nitrolite'; - -// Function to create a signed ledger balances request -async function createLedgerBalancesRequest(signer, participant) { - const requestId = generateRequestId(); - const method = RPCMethod.GetLedgerBalances; // Use the RPC method enum for clarity - const params = [{ participant }]; // Note: updated parameter name to 'participant' - const timestamp = getCurrentTimestamp(); - - // Create the request structure - const requestData = [requestId, method, params, timestamp]; - const request = { req: requestData }; - - // Sign the request - const message = JSON.stringify(request); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = signer.wallet.signingKey.sign(messageBytes); - - // Add signature to the request - request.sig = [signature]; - - return { stringified: JSON.stringify(request), requestId }; -} - -// Function to get ledger balances -async function getLedgerBalances(ws, participant, signer) { - return new Promise((resolve, reject) => { - createLedgerBalancesRequest(signer, participant) - .then(({ stringified, requestId }) => { - // Set up message handler - const handleMessage = (event) => { - try { - const message = parseRPCMessage(event.data); - - // Check if this is our response - if (message.requestId === requestId && - message.method === RPCMethod.GetLedgerBalances) { - - // Remove the listener - ws.removeEventListener('message', handleMessage); - - // Resolve with the balances data - resolve(message.params); - } - } catch (error) { - console.error('Error parsing message:', error); - } - }; - - // Add message listener - ws.addEventListener('message', handleMessage); - - // Send the request - ws.send(stringified); - - // Set timeout - setTimeout(() => { - ws.removeEventListener('message', handleMessage); - reject(new Error('Timeout waiting for ledger balances')); - }, 10000); - }) - .catch(reject); - }); -} - -// Usage example -const participantAddress = '0x1234567890abcdef1234567890abcdef12345678'; - -try { - const balances = await getLedgerBalances(ws, participantAddress, stateWallet); - - console.log('Channel ledger balances:', balances); - // Process and display balances - // ... - -} catch (error) { - console.error('Failed to get ledger balances:', error); -} -``` - - - - -## Checking Balances for a Participant - -To retrieve off-chain balances for a participant, use the `createGetLedgerBalancesMessage` helper function: - -```javascript -import { createGetLedgerBalancesMessage, parseRPCResponse, RPCMethod } from '@erc7824/nitrolite'; -import { ethers } from 'ethers'; - -// Function to get ledger balances for a participant -async function getLedgerBalances(ws, participant, messageSigner) { - return new Promise((resolve, reject) => { - // Message handler for the response - const handleMessage = (event) => { - try { - const message = parseRPCResponse(event.data); - - // Check if this is a response to our get_ledger_balances request - if (message.method === RPCMethod.GetLedgerBalances) { - // Clean up by removing the event listener - ws.removeEventListener('message', handleMessage); - - // Resolve with the balance data - resolve(message.params); - } - } catch (error) { - console.error('Error parsing message:', error); - } - }; - - // Set up timeout to avoid hanging indefinitely - const timeoutId = setTimeout(() => { - ws.removeEventListener('message', handleMessage); - reject(new Error('Timeout waiting for ledger balances')); - }, 10000); // 10 second timeout - - // Add the message handler - ws.addEventListener('message', handleMessage); - - // Create and send the balance request - createGetLedgerBalancesMessage(messageSigner, participant) - .then(message => { - ws.send(message); - }) - .catch(error => { - clearTimeout(timeoutId); - ws.removeEventListener('message', handleMessage); - reject(error); - }); - }); -} - -// Example usage -const participantAddress = '0x1234567890abcdef1234567890abcdef12345678'; - -getLedgerBalances(ws, participantAddress, messageSigner) - .then(balances => { - console.log('Channel balances:', balances); - - // Process and display your balances - if (balances.length > 0) { - console.log('My balances:'); - balances.forEach(balance => { - console.log(`- ${balance.asset.toUpperCase()}: ${balance.amount}`); - }); - } else { - console.log('No balance data available'); - } - }) - .catch(error => { - console.error('Failed to get ledger balances:', error); - }); -``` - -## Processing Balance Data - -When you receive balance data from the ClearNode, it's helpful to format it for better readability: - -```javascript -// Simple function to format your balance data for display -function formatMyBalances(balances) { - // Return formatted balance information - return balances.map(balance => ({ - asset: balance.asset.toUpperCase(), - amount: balance.amount, - // You can add additional formatting here if needed - displayAmount: `${balance.amount} ${balance.asset.toUpperCase()}` - })); -} - -// Example usage -const myFormattedBalances = formatMyBalances(balancesFromClearNode); - -if (myFormattedBalances && myFormattedBalances.length > 0) { - console.log('My balances:'); - myFormattedBalances.forEach(balance => { - console.log(`- ${balance.displayAmount}`); - }); -} else { - console.log('No balance data available'); -} -``` - -## Best Practices for Balance Checking - -When working with off-chain balances, follow these best practices: - -### Regular Balance Polling - -Set up a regular interval to check balances, especially in active applications: - -```javascript -// Simple balance monitoring function -function startBalanceMonitoring(ws, participantAddress, messageSigner, intervalMs = 30000) { - // Check immediately on start - getLedgerBalances(ws, participantAddress, messageSigner) - .then(displayBalances) - .catch(err => console.error('Initial balance check failed:', err)); - - // Set up interval for regular checks - const intervalId = setInterval(() => { - getLedgerBalances(ws, participantAddress, messageSigner) - .then(displayBalances) - .catch(err => console.error('Balance check failed:', err)); - }, intervalMs); // Check every 30 seconds by default - - // Return function to stop monitoring - return () => clearInterval(intervalId); -} - -// Simple display function -function displayBalances(balances) { - console.log(`Balance update at ${new Date().toLocaleTimeString()}:`); - - // Format and display your balances - if (balances.length > 0) { - console.log('My balances:'); - balances.forEach(balance => { - console.log(`- ${balance.asset.toUpperCase()}: ${balance.amount}`); - }); - } else { - console.log('No balance data available'); - } -} -``` - -## Common Errors and Troubleshooting - -When retrieving off-chain balances, you might encounter these common issues: - -| Error Type | Description | Solution | -|------------|-------------|----------| -| **Authentication errors** | WebSocket connection loses authentication | Re-authenticate before requesting balances again | -| **Channel not found** | The channel ID is invalid or the channel has been closed | Verify the channel ID and check if the channel is still active | -| **Connection issues** | WebSocket disconnects during a balance request | Implement reconnection logic with exponential backoff | -| **Timeout** | The ClearNode does not respond in a timely manner | Set appropriate timeouts and implement retry logic | - -## Next Steps - -Now that you understand how to monitor off-chain balances in your channels, you can: - -1. [Create an application session](application_session) to start transacting off-chain -2. Learn about [channel closing](close_session) when you're done with the channel \ No newline at end of file diff --git a/erc7824-docs/docs/quick_start/close_session.md b/erc7824-docs/docs/quick_start/close_session.md deleted file mode 100644 index d933fb152..000000000 --- a/erc7824-docs/docs/quick_start/close_session.md +++ /dev/null @@ -1,1197 +0,0 @@ ---- -sidebar_position: 8 -title: Close Application Session -description: Properly close application sessions and finalize fund allocations using NitroliteRPC. -keywords: [erc7824, nitrolite, close session, finalize, state channels] ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Close Application Session - -Once you've completed your interactions with an application session, it's important to properly close it. This finalizes the fund allocations and ensures that all participants agree on the final state. - -## Why Properly Closing Sessions Matters - -Closing an application session correctly is important because it: - -- Finalizes fund allocations between participants -- Updates on-chain balances (if applicable) -- Frees up resources on the ClearNode -- Ensures the proper completion of all operations -- Prevents potential disputes - -```mermaid -graph TD - A[Active Session] --> B[Define Final Allocations] - B --> C[Sign Close Message] - C --> D[Send to ClearNode] - D --> E[Session Closed] -``` - -## Closing an Application Session - -To close an application session, you'll use the `createCloseAppSessionMessage` helper from NitroliteRPC. Here's how to do it: - - - - -```javascript -import { useCallback } from 'react'; -import { createCloseAppSessionMessage, parseRPCResponse, MessageSigner, CloseAppSessionRPCParams } from '@erc7824/nitrolite'; - -/** - * Hook for closing an application session - */ -function useCloseApplicationSession() { - const closeApplicationSession = useCallback( - async ( - signer: MessageSigner, - sendRequest: (message: string) => Promise, - appId: string, - participantA: Address, - participantB: Address, - amount: string - ) => { - try { - if (!appId) { - throw new Error('Application ID is required to close the session.'); - } - - // Create allocations with asset type - const allocations = [ - { - participant: participantA, - asset: 'usdc', - amount: '0', - }, - { - participant: participantB, - asset: 'usdc', - amount: amount, - }, - ]; - - // Create the close request - const closeRequest = { - app_session_id: appId, - allocations: allocations, - }; - - // Create the signed message - const signedMessage = await createCloseAppSessionMessage( - signer, - [closeRequest] - ); - - // Send the request and wait for response - const response = await sendRequest(signedMessage); - - // Check for success - if (response.app_session_id) { - // Clean up local storage - localStorage.removeItem('app_session_id'); - return { - success: true, - app_id: response.app_session_id, - status: response.status || "closed", - response - }; - } else { - return { success: true, response }; - } - } catch (error) { - console.error('Error closing application session:', error); - return { - success: false, - error: error instanceof Error - ? error.message - : 'Unknown error during close session', - }; - } - }, - [] - ); - - return { closeApplicationSession }; -} - -// Usage example -function MyComponent() { - const { closeApplicationSession } = useCloseApplicationSession(); - - const handleCloseSession = async () => { - // Define your WebSocket send wrapper - const sendRequest = async (payload) => { - return new Promise((resolve, reject) => { - // Assuming ws is your WebSocket connection - const handleMessage = (event) => { - try { - const message = parseRPCResponse(event.data); - if (message.method === RPCMethod.CloseAppSession) { - ws.removeEventListener('message', handleMessage); - resolve(message.params); - } - } catch (error) { - console.error('Error parsing message:', error); - } - }; - - ws.addEventListener('message', handleMessage); - ws.send(payload); - - // Set timeout to prevent hanging - setTimeout(() => { - ws.removeEventListener('message', handleMessage); - reject(new Error('Close app session timeout')); - }, 10000); - }); - }; - - // Get the app session ID from where it was stored - const appId = localStorage.getItem('app_session_id'); - - // Define participant addresses - const participantA = '0xYourAddress'; - const participantB = '0xOtherAddress'; - - // Define amount - const amount = '1000000'; // Amount to allocate to participantB - - const result = await closeApplicationSession( - walletSigner, // Your signer object - sendRequest, // Function to send the request - appId, // Application session ID - participantA, // First participant address - participantB, // Second participant address - amount // Amount to allocate to participant B - ); - - if (result.success) { - console.log('Application session closed successfully'); - } else { - console.error(`Failed to close application session: ${result.error}`); - } - }; - - return ( - - ); -} -``` - - - - -```typescript -// app-session.service.ts -import { Injectable } from '@angular/core'; -import { createCloseAppSessionMessage } from '@erc7824/nitrolite'; -import { ethers } from 'ethers'; -import { BehaviorSubject, Observable, from } from 'rxjs'; -import { tap, catchError } from 'rxjs/operators'; - -@Injectable({ - providedIn: 'root' -}) -export class AppSessionService { - private webSocket: WebSocket | null = null; - private appIdSubject = new BehaviorSubject(null); - - public appId$ = this.appIdSubject.asObservable(); - - constructor() { - // Retrieve app ID from storage if available - const storedAppId = localStorage.getItem('app_session_id'); - if (storedAppId) { - this.appIdSubject.next(storedAppId); - } - } - - public setWebSocket(ws: WebSocket): void { - this.webSocket = ws; - } - - // This method closes an application session - public closeApplicationSession( - signer: any, - appId: string, - finalAllocations: number[] - ): Observable { - if (!this.webSocket) { - throw new Error('WebSocket connection is not established'); - } - - if (!appId) { - throw new Error('Application ID is required'); - } - - return from(this.closeAppSessionAsync( - signer, - appId, - participantA, - participantB, - amount - )).pipe( - tap(result => { - if (result.success) { - // Remove app ID from storage - localStorage.removeItem('app_session_id'); - this.appIdSubject.next(null); - } - }), - catchError(error => { - console.error('Error closing application session:', error); - throw error; - }) - ); - } - - private async closeAppSessionAsync( - signer: any, - appId: string, - participantA: string, - participantB: string, - amount: string - ): Promise { - try { - // Create allocations with asset type - const allocations = [ - { - participant: participantA, - asset: 'usdc', - amount: '0', - }, - { - participant: participantB, - asset: 'usdc', - amount: amount, - }, - ]; - - // Create close request - const closeRequest = { - app_session_id: appId, - allocations: allocations, - }; - - - // Create message signer function - const messageSigner = async (payload: any) => { - const message = JSON.stringify(payload); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const signature = await signer.signMessage(messageBytes); - return signature; - }; - - // Create the signed message - const signedMessage = await createCloseAppSessionMessage( - messageSigner, - [closeRequest] - ); - - // Send the message and wait for response - return await this.sendRequest(signedMessage); - } catch (error) { - console.error('Error in closeAppSessionAsync:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } - } - - private sendRequest(payload: string): Promise { - return new Promise((resolve, reject) => { - if (!this.webSocket) { - reject(new Error('WebSocket not connected')); - return; - } - - const handleMessage = (event: MessageEvent) => { - try { - const message = JSON.parse(event.data); - if (message.res && message.res[1] === 'close_app_session') { - this.webSocket?.removeEventListener('message', handleMessage); - resolve({ - success: true, - app_id: message.res[2]?.[0]?.app_session_id || null, - status: message.res[2]?.[0]?.status || "closed", - response: message.res[2] - }); - } - - if (message.err) { - this.webSocket?.removeEventListener('message', handleMessage); - reject(new Error(`Error: ${message.err[1]} - ${message.err[2]}`)); - } - } catch (error) { - console.error('Error parsing message:', error); - } - }; - - this.webSocket.addEventListener('message', handleMessage); - this.webSocket.send(payload); - - // Set timeout to prevent hanging - setTimeout(() => { - this.webSocket?.removeEventListener('message', handleMessage); - reject(new Error('Close session timeout')); - }, 10000); - }); - } -} - -// app-session-close.component.ts -import { Component, OnInit } from '@angular/core'; -import { AppSessionService } from './app-session.service'; - -@Component({ - selector: 'app-session-closer', - template: ` -
-

Close Application Session

-
- Current Application Session ID: {{ appId }} - -
-
- - -
-
- - -
-
- - -
- -
- No active application session found. -
- -
- {{ error }} -
- -
- Application session closed successfully! -
-
- `, - styles: [` - .app-session-container { - margin: 20px; - padding: 15px; - border: 1px solid #eee; - border-radius: 5px; - } - .allocation-controls { - margin: 15px 0; - } - .allocation-item { - margin-bottom: 10px; - } - .error-message { - color: red; - margin-top: 10px; - } - .success-message { - color: green; - margin-top: 10px; - } - .info-message { - color: blue; - margin-top: 10px; - } - `] -}) -export class AppSessionCloseComponent implements OnInit { - appId: string | null = null; - isClosing = false; - error: string | null = null; - success = false; - - // Default allocations (e.g., 800000 / 200000 split for 1 USDC) - allocation1 = 800000; - allocation2 = 200000; - - constructor(private appSessionService: AppSessionService) {} - - ngOnInit(): void { - // Subscribe to app ID changes - this.appSessionService.appId$.subscribe(id => { - this.appId = id; - this.success = false; - }); - } - - closeAppSession(): void { - if (!this.appId) { - this.error = 'No active application session'; - return; - } - - this.isClosing = true; - this.error = null; - this.success = false; - - // Define participants - const participantA = '0xYourAddress'; - const participantB = '0xOtherAddress'; - - // Create allocations with asset type - const amount = this.allocation2.toString(); - - // Assuming you have access to a signer (e.g., from MetaMask) - const signer = window.ethereum && new ethers.providers.Web3Provider(window.ethereum).getSigner(); - - if (!signer) { - this.error = 'No wallet connected'; - this.isClosing = false; - return; - } - - this.appSessionService.closeApplicationSession( - signer, - this.appId, - participantA, - participantB, - amount - ).subscribe({ - next: (result) => { - console.log('App session closed:', result); - this.success = true; - this.isClosing = false; - }, - error: (err) => { - this.error = `Failed to close application session: ${err.message}`; - this.isClosing = false; - } - }); - } -} -``` - -
- - -```javascript - - - - - - -``` - - - - -```javascript -import { createCloseAppSessionMessage } from '@erc7824/nitrolite'; -import { ethers } from 'ethers'; - -/** - * Close an app session - * @param {string} appId - The app session ID to close - * @param {string} participantA - First participant's address - * @param {string} participantB - Second participant's address - * @param {string} amount - Amount to allocate to participant B - * @param {WebSocket} ws - WebSocket connection to the ClearNode - * @param {object} wallet - Ethers wallet for signing - * @returns {Promise} Success status - */ -async function closeAppSession(appId, participantA, participantB, amount, ws, wallet) { - try { - console.log(`Closing app session ${appId} with amount ${amount}`); - - if (!appId) { - throw new Error('App session ID is required to close the session'); - } - - // Message signer function - const messageSigner = async (payload) => { - try { - const message = JSON.stringify(payload); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = wallet.signingKey.sign(messageBytes); - return signature; - } catch (error) { - console.error("Error signing message:", error); - throw error; - } - }; - - // Create allocations with asset type - const allocations = [ - { - participant: participantA, - asset: 'usdc', - amount: '0', - }, - { - participant: participantB, - asset: 'usdc', - amount: amount, - }, - ]; - - // Create the close request - const closeRequest = { - app_session_id: appId, - allocations: allocations, - }; - - // Create the signed message - const signedMessage = await createCloseAppSessionMessage( - messageSigner, - [closeRequest] - ); - - // Send the message and wait for response - return new Promise((resolve, reject) => { - // Create a one-time message handler for the close session response - const handleCloseSessionResponse = (data) => { - try { - const rawData = typeof data === 'string' ? data : data.toString(); - const message = JSON.parse(rawData); - - console.log('Received close session response:', message); - - // Check if this is a close session response - if (message.res && - (message.res[1] === 'close_app_session' || - message.res[1] === 'app_session_closed')) { - // Remove the listener once we get the response - ws.removeListener('message', handleCloseSessionResponse); - - // Extract app ID and status from response - const appId = message.res[2]?.[0]?.app_id; - const status = message.res[2]?.[0]?.status || "closed"; - - resolve({ - success: true, - app_session_id: appId, - status: status - }); - } - - // Check for error responses - if (message.err) { - ws.removeListener('message', handleCloseSessionResponse); - reject(new Error(`Error ${message.err[1]}: ${message.err[2]}`)); - } - } catch (error) { - console.error('Error handling close session response:', error); - } - }; - - // Add the message handler - ws.on('message', handleCloseSessionResponse); - - // Set timeout to prevent hanging - setTimeout(() => { - ws.removeListener('message', handleCloseSessionResponse); - reject(new Error('Close session timeout')); - }, 10000); - - // Send the signed message - ws.send(signedMessage); - }); - } catch (error) { - console.error(`Error closing app session ${appId}:`, error); - throw error; - } -} - -// Usage example -const appId = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; -const participantA = '0x1234...'; // Your address -const participantB = '0x5678...'; // Other participant's address -const amount = '200000'; // 0.2 USDC to participant B - -// Assuming you have a WebSocket connection and wallet initialized -closeAppSession(appId, participantA, participantB, amount, ws, wallet) - .then(result => { - if (result.success) { - console.log(`Application session ${result.app_id} closed successfully with status: ${result.status}`); - } - }) - .catch(error => { - console.error('Failed to close application session:', error); - }); -``` - - - - -```javascript -/** - * Close an app session for a game room - * @param {string} roomId - Room ID - * @param {Array} allocations - Final allocations [player1, player2, server] - * @param {WebSocket} ws - WebSocket connection to the ClearNode - * @param {object} wallet - Ethers wallet for signing - * @returns {Promise} Success status - */ -export async function closeAppSession(roomId, participantA, participantB, participantServer, amount, ws, wallet) { - try { - // Get the app session for this room - const appSession = roomAppSessions.get(roomId); - if (!appSession) { - console.warn(`No app session found for room ${roomId}`); - return false; - } - - // Make sure appSessionId exists - const appSessionId = appSession.appSessionId; - if (!appSessionId) { - console.error(`No appSessionId found in app session for room ${roomId}`); - return false; - } - - console.log(`Closing app session ${appSessionId} for room ${roomId}`); - - // Message signer function - const messageSigner = async (payload) => { - try { - const message = JSON.stringify(payload); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = wallet.signingKey.sign(messageBytes); - return signature; - } catch (error) { - console.error("Error signing message:", error); - throw error; - } - }; - - // Create allocations with asset type - const allocations = [ - { - participant: participantA, - asset: 'usdc', - amount: '0', - }, - { - participant: participantB, - asset: 'usdc', - amount: amount, - }, - { - participant: participantServer, - asset: 'usdc', - amount: '0', - }, - ]; - - // Create close request - const closeRequest = { - app_session_id: appSessionId, - allocations: allocations, - }; - - // Create the signed message - const signedMessage = await createCloseAppSessionMessage( - messageSigner, - [closeRequest] - ); - - // Send the message and wait for response - return new Promise((resolve, reject) => { - // Create a one-time message handler for the close session response - const handleCloseSessionResponse = (data) => { - try { - const rawData = typeof data === 'string' ? data : data.toString(); - const message = JSON.parse(rawData); - - console.log('Received close session response:', message); - - // Check if this is a close session response - if (message.res && - (message.res[1] === 'close_app_session' || - message.res[1] === 'app_session_closed')) { - // Remove the listener once we get the response - ws.removeListener('message', handleCloseSessionResponse); - - // Extract app ID and status from response - const appId = message.res[2]?.[0]?.app_id; - const status = message.res[2]?.[0]?.status || "closed"; - - // Remove the app session - roomAppSessions.delete(roomId); - - resolve({ - success: true, - app_session_id: appId, - status: status, - roomId: roomId - }); - } - - // Check for error responses - if (message.err) { - ws.removeListener('message', handleCloseSessionResponse); - reject(new Error(`Error ${message.err[1]}: ${message.err[2]}`)); - } - } catch (error) { - console.error('Error handling close session response:', error); - } - }; - - // Add the message handler - ws.on('message', handleCloseSessionResponse); - - // Set timeout to prevent hanging - setTimeout(() => { - ws.removeListener('message', handleCloseSessionResponse); - reject(new Error('Close session timeout')); - }, 10000); - - // Send the signed message - ws.send(signedMessage); - }); - } catch (error) { - console.error(`Error closing app session for room ${roomId}:`, error); - return false; - } -} -``` - - -
- -## Close Session Flow - -The following sequence diagram shows the interaction between your application and the ClearNode when closing a session: - -```mermaid -sequenceDiagram - participant App as Your Application - participant CN as ClearNode - - App->>CN: Send close_app_session request - CN->>App: Return success response - App->>App: Clean up local storage -``` - -## Understanding Final Allocations - -When closing an application session, you must specify the final allocations of funds between participants: - -- **Final Allocations**: An array of allocation objects for each participant -- **Participant**: The address of the participant receiving funds -- **Asset**: The asset type (e.g., "usdc", "eth") -- **Amount**: The amount as a string (e.g., "1000000" or "0.5") - -Examples: - -```javascript -// Initial allocations when creating the session: -// [ -// { participant: participantA, asset: "usdc", amount: "1000000" }, -// { participant: participantB, asset: "usdc", amount: "0" } -// ] - -// Possible final allocations when closing: -const allocations = [ - { participant: participantA, asset: "usdc", amount: "1000000" }, - { participant: participantB, asset: "usdc", amount: "0" } -]; // No change - all funds to participant A - -// OR -const allocations = [ - { participant: participantA, asset: "usdc", amount: "0" }, - { participant: participantB, asset: "usdc", amount: "1000000" } -]; // All funds to participant B - -// OR -const allocations = [ - { participant: participantA, asset: "usdc", amount: "700000" }, - { participant: participantB, asset: "usdc", amount: "300000" } -]; // Split 70/30 -``` - -## Understanding the Close Session Response - -When you close an application session, the ClearNode responds with information about the closed session: - -```javascript -// Example response -{ - "res": [ - 3, // Request ID - "close_app_session", // Method name - [ - { - "app_session_id": "0x0ac588b2924edbbbe34bb4c51d089771bd7bd7018136c8c4317624112a8c9f79", // Session ID - "status": "closed" - } - ], - 1631234567890 // Timestamp - ], - "sig": ["0xSignature"] -} -``` - -## Response Components - -When closing an application session, the ClearNode responds with: - -| Component | Description | Example | -|-----------|-------------|---------| -| **app_session_id** | Identifier of the application session | `"0x0ac588b2924edbbbe34bb4c51d089771bd7bd7018136c8c4317624112a8c9f79"` | -| **status** | Final state of the application session | `"closed"` | - -## Common Scenarios for Closing Sessions - -Here are some common scenarios where you would close an application session: - -| Scenario | Description | Typical Allocation Pattern | -|----------|-------------|---------------------------| -| **Payment Completed** | User completes payment for a service | All funds to the service provider | -| **Game Completed** | End of a game with a winner | Winner gets the pot | -| **Partial Service** | Service partially delivered | Split based on completion percentage | -| **Cancellation** | Service cancelled before completion | Funds returned to original funder | -| **Dispute Resolution** | Resolution after a dispute | Split according to dispute resolution | - - -## Best Practices - -When closing application sessions, follow these best practices: - -1. **Verify all transactions are complete** before closing -2. **Include appropriate timeouts** to prevent hanging operations -3. **Clean up event listeners** to prevent memory leaks -4. **Implement retry logic** for failure recovery -5. **Store transaction records** for audit purposes -6. **Validate final allocations** match expected state - -## Common Errors and Troubleshooting - -When closing application sessions, you might encounter these issues: - -| Error Type | Description | Solution | -|------------|-------------|----------| -| **Authentication errors** | WebSocket connection loses authentication | Re-authenticate before closing the session | -| **Session not found** | The app_session_id is invalid or the session was already closed | Verify the app_session_id is correct and the session is still active | -| **Allocation mismatch** | The total in final allocations doesn't match initial funding | Ensure allocations sum to the correct total amount | -| **Connection issues** | WebSocket disconnects during a close request | Implement reconnection logic with exponential backoff | -| **Timeout** | The ClearNode does not respond in a timely manner | Set appropriate timeouts and implement retry logic | - -## Next Steps - -After closing an application session, you can: - -1. [Check your channel balances](balances) to verify the finalized allocations -2. [Create a new application session](application_session) if you need to continue with different parameters - -For more advanced scenarios, see our detailed documentation on state channel lifecycle management. \ No newline at end of file diff --git a/erc7824-docs/docs/quick_start/connect_to_the_clearnode.md b/erc7824-docs/docs/quick_start/connect_to_the_clearnode.md deleted file mode 100644 index dbe8a787e..000000000 --- a/erc7824-docs/docs/quick_start/connect_to_the_clearnode.md +++ /dev/null @@ -1,1791 +0,0 @@ ---- -sidebar_position: 5 -title: Connect to the ClearNode -description: Establish connection with ClearNode for reliable off-chain transaction processing and verification. -keywords: [erc7824, nitrolite, clearnode, off-chain, validation, messaging, nitroliterpc, websocket] ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Connect to the ClearNode - -A ClearNode is a specialized service that facilitates off-chain communication, message relay, and state validation in the Nitrolite ecosystem. This guide explains how to establish and manage connections to a ClearNode using the NitroliteRPC protocol. - -## What is a ClearNode? - -A **[ClearNode](https://github.com/erc7824/clearnode)** is an implementation of a message broker for the Clearnet protocol. It serves as a critical infrastructure component in the Nitrolite ecosystem, providing several important functions in the state channel network: - -- **Multi-Chain Support**: Connect to multiple EVM blockchains (Polygon, Celo, Base) -- **Off-Chain Payments**: Efficient payment channels for high-throughput transactions -- **Virtual Applications**: Create multi-participant applications -- **Quorum-Based Signatures**: Support for multi-signature schemes with weight-based quorums - -## Understanding NitroliteRPC - -NitroliteRPC is a utility in our SDK that standardizes message creation for communication with ClearNodes. It's not a full protocol implementation but rather a set of helper functions that ensure your application constructs properly formatted messages for ClearNode interaction. - -Key functions of NitroliteRPC include: - -- **Message Construction**: Creates properly formatted request messages -- **Signature Management**: Handles the cryptographic signing of messages -- **Standard Format Enforcement**: Ensures all messages follow the required format for ClearNode compatibility -- **Authentication Flow Helpers**: Simplifies the authentication process - -Under the hood, NitroliteRPC provides functions that generate message objects with the correct structure, timestamps, and signature formatting so you don't have to build these messages manually when communicating with ClearNodes. - -## Connecting to a ClearNode - -After initializing your client and creating a channel, you need to establish a WebSocket connection to a ClearNode. It's important to understand that the Nitrolite SDK doesn't provide its own transport layer - you'll need to implement the WebSocket connection yourself using your preferred library. - - - - -```javascript -// Import your preferred WebSocket library -import WebSocket from 'ws'; // Node.js -// or use the browser's built-in WebSocket - -// Create a WebSocket connection to the ClearNode -const ws = new WebSocket('wss://clearnode.example.com'); - -// Set up basic event handlers -ws.onopen = () => { - console.log('WebSocket connection established'); - // Connection is open, can now proceed with authentication -}; - -ws.onmessage = (event) => { - const message = JSON.parse(event.data); - console.log('Received message:', message); - // Process incoming messages -}; - -ws.onerror = (error) => { - console.error('WebSocket error:', error); -}; - -ws.onclose = (event) => { - console.log(`WebSocket closed: ${event.code} ${event.reason}`); -}; -``` - - - - -```javascript -class ClearNodeConnection { - constructor(url) { - this.url = url; - this.ws = null; - this.isConnected = false; - this.reconnectAttempts = 0; - this.maxReconnectAttempts = 5; - this.reconnectInterval = 3000; // ms - this.messageHandlers = new Map(); - } - - // Register message handlers - onMessage(type, handler) { - this.messageHandlers.set(type, handler); - } - - connect() { - this.ws = new WebSocket(this.url); - - this.ws.onopen = this.handleOpen.bind(this); - this.ws.onmessage = this.handleMessage.bind(this); - this.ws.onerror = this.handleError.bind(this); - this.ws.onclose = this.handleClose.bind(this); - } - - handleOpen() { - console.log(`Connected to ClearNode at ${this.url}`); - this.isConnected = true; - this.reconnectAttempts = 0; - - // Emit connected event - this.emit('connected'); - } - - handleMessage(event) { - try { - const message = JSON.parse(event.data); - - // Determine message type (auth_challenge, auth_success, etc.) - const messageType = message.res ? message.res[1] : 'unknown'; - - // Emit specific message event - this.emit('message', message); - - // Call specific handler if registered - if (this.messageHandlers.has(messageType)) { - this.messageHandlers.get(messageType)(message); - } - } catch (error) { - console.error('Error handling message:', error); - } - } - - handleError(error) { - console.error('WebSocket error:', error); - this.emit('error', error); - } - - handleClose(event) { - this.isConnected = false; - console.log(`WebSocket closed: ${event.code} ${event.reason}`); - - // Emit disconnected event - this.emit('disconnected', event); - - // Attempt to reconnect - this.attemptReconnect(); - } - - attemptReconnect() { - if (this.reconnectAttempts >= this.maxReconnectAttempts) { - console.error('Maximum reconnection attempts reached'); - return; - } - - this.reconnectAttempts++; - const delay = this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1); - - console.log(`Attempting to reconnect in ${delay}ms (${this.reconnectAttempts}/${this.maxReconnectAttempts})`); - - setTimeout(() => { - console.log('Reconnecting...'); - this.connect(); - }, delay); - } - - send(message) { - if (!this.isConnected) { - console.error('Cannot send message: not connected'); - return false; - } - - try { - this.ws.send(typeof message === 'string' ? message : JSON.stringify(message)); - return true; - } catch (error) { - console.error('Error sending message:', error); - return false; - } - } - - disconnect() { - if (this.ws) { - this.ws.close(1000, 'User initiated disconnect'); - } - } - - // Simple event system - #events = {}; - - on(event, callback) { - if (!this.#events[event]) this.#events[event] = []; - this.#events[event].push(callback); - return this; - } - - off(event, callback) { - if (!this.#events[event]) return this; - if (!callback) { - delete this.#events[event]; - return this; - } - this.#events[event] = this.#events[event].filter(cb => cb !== callback); - return this; - } - - emit(event, ...args) { - if (!this.#events[event]) return false; - this.#events[event].forEach(callback => callback(...args)); - return true; - } -} - -// Usage -const clearNodeConnection = new ClearNodeConnection('wss://clearnode.example.com'); -clearNodeConnection.connect(); - -// Register event handlers -clearNodeConnection.on('connected', () => { - console.log('Connection established, ready to authenticate'); -}); - -// Later, when you're done -clearNodeConnection.disconnect(); -``` - - - - -## Authentication Flow - -When connecting to a ClearNode, you need to follow a specific authentication flow using the NitroliteRPC utility to create properly formatted and signed messages: - -1. **Initial Connection**: The client establishes a WebSocket connection to the ClearNode's URL -2. **Auth Request**: On the first connection client sends an `auth_request` message with its identity information -3. **Challenge**: The ClearNode responds with an `auth_challenge` containing a random nonce -4. **Signature Verification**: The client signs the challenge along with session key and allowances using EIP712 signature and sends an `auth_verify` message -5. **Auth Result**: The ClearNode verifies the signature and responds with `auth_success` or `auth_failure` -6. **Reconnection**: On success ClearNode will return the JWT Token, which can be used for subsequent reconnections without needing to re-authenticate. - -This flow ensures that only authorized participants with valid signing keys can connect to the ClearNode and participate in channel operations. - -```mermaid -sequenceDiagram - participant Client as Your Application - participant CN as ClearNode - - Client->>CN: WebSocket Connection Request - CN->>Client: Connection Established - - Client->>Client: Create auth_request - Client->>CN: Send auth_request - - CN->>CN: Generate random challenge nonce - CN->>Client: Send auth_challenge with nonce - - Client->>Client: Sign challenge using EIP712 - Client->>CN: Send auth_verify with signature - - CN->>CN: Verify signature against address - - alt Authentication Success - CN->>Client: Send auth_success - Note over Client,CN: Client is now authenticated - Client->>CN: Can now send channel operations - else Authentication Failure - CN->>Client: Send auth_failure - Note over Client,CN: Connection remains but unauthorized - end -``` - - - - -```javascript -import { - createAuthRequestMessage, - createAuthVerifyMessage, - createEIP712AuthMessageSigner, - parseRPCResponse, - RPCMethod, -} from '@erc7824/nitrolite'; -import { ethers } from 'ethers'; - -// Create and send auth_request -const authRequestMsg = await createAuthRequestMessage({ - address: '0xYourWalletAddress', - session_key: '0xYourSignerAddress', - application: 'YourAppDomain', - expires_at: (Math.floor(Date.now() / 1000) + 3600).toString(), // 1 hour expiration (as string) - scope: 'console', - allowances: [], -}); - -// After WebSocket connection is established -ws.onopen = async () => { - console.log('WebSocket connection established'); - - ws.send(authRequestMsg); -}; - -// Handle incoming messages -ws.onmessage = async (event) => { - try { - const message = parseRPCResponse(event.data); - - // Handle auth_challenge response - switch (message.method) { - case RPCMethod.AuthChallenge: - console.log('Received auth challenge'); - - // Create EIP-712 message signer function - const eip712MessageSigner = createEIP712AuthMessageSigner( - walletClient, // Your wallet client instance - { - // EIP-712 message structure, data should match auth_request - scope: authRequestMsg.scope, - application: authRequestMsg.application, - participant: authRequestMsg.participant, - expires_at: authRequestMsg.expires_at, - allowances: authRequestMsg.allowances, - }, - { - // Domain for EIP-712 signing - name: 'Your Domain', - }, - ) - - // Create and send auth_verify with signed challenge - const authVerifyMsg = await createAuthVerifyMessage( - eip712MessageSigner, // Our custom eip712 signer function - message, - ); - - ws.send(authVerifyMsg); - break; - // Handle auth_success or auth_failure - case RPCMethod.AuthVerify: - if (!message.params.success) { - console.log('Authentication failed'); - return; - } - console.log('Authentication successful'); - // Now you can start using the channel - - window.localStorage.setItem('clearnode_jwt', message.params.jwtToken); // Store JWT token for future use - break; - case RPCMethod.Error: { - console.error('Authentication failed:', message.params.error); - } - } - } catch (error) { - console.error('Error handling message:', error); - } -}; -``` - - - - -```javascript -import { - createAuthRequestMessage, - createAuthVerifyMessageFromChallenge, - createGetLedgerBalancesMessage, - createGetConfigMessage, - createEIP712AuthMessageSigner, - parseRPCResponse, - RPCMethod, -} from '@erc7824/nitrolite'; -import { ethers } from 'ethers'; - -// After connection is established, send auth request -ws.onopen = async () => { - const authRequestMsg = await createAuthRequestMessage({ - address: '0xYourWalletAddress', - session_key: '0xYourSignerAddress', - application: 'YourAppDomain', - expires_at: (Math.floor(Date.now() / 1000) + 3600).toString(), // 1 hour expiration (as string) - scope: 'console', - allowances: [], - }); - ws.send(authRequestMsg); -}; - -// If you want to manually extract and handle the challenge -ws.onmessage = async (event) => { - try { - const message = parseRPCResponse(event.data); - - if (message.method === RPCMethod.AuthChallenge) { - // Extract the challenge manually from the response - if ( - message.params.challengeMessage - ) { - const challenge = message.params.challengeMessage; - - // Create EIP-712 message signer function - const eip712MessageSigner = createEIP712AuthMessageSigner( - walletClient, // Your wallet client instance - { - // EIP-712 message structure, data should match auth_request - scope: authRequestMsg.scope, - application: authRequestMsg.application, - participant: authRequestMsg.participant, - expires_at: authRequestMsg.expires_at, - allowances: authRequestMsg.allowances, - }, - { - // Domain for EIP-712 signing - name: 'Your Domain', - }, - ) - - // Create auth_verify with the explicitly provided challenge - const authVerifyMsg = await createAuthVerifyMessageFromChallenge( - eip712MessageSigner, - challenge - ); - - ws.send(authVerifyMsg); - } else { - console.error('Malformed challenge response'); - } - } - } catch (error) { - console.error('Error handling message:', error); - } -}; -``` - - - - -```javascript -import { createAuthVerifyMessageWithJWT, parseRPCResponse, RPCMethod } from '@erc7824/nitrolite'; -import { ethers } from 'ethers'; - -// After WebSocket connection is established -ws.onopen = async () => { - console.log('WebSocket connection established'); - - // Create and send auth_verify with JWT for reconnection - // Get the stored JWT token - const jwtToken = window.localStorage.getItem('clearnode_jwt'); - - const authRequestMsg = await createAuthVerifyMessageWithJWT( - jwtToken, // JWT token for reconnection - ); - - ws.send(authRequestMsg); -}; - -// Handle incoming messages -ws.onmessage = async (event) => { - try { - const message = parseRPCResponse(event.data); - - // Handle auth_success or auth_failure - switch (message.method) { - case RPCMethod.AuthVerify: - if (message.params.success) { - console.log('Authentication successful'); - // Now you can start using the channel - } - break; - case RPCMethod.Error: - console.error('Authentication failed:', message.params.error); - break; - } - } catch (error) { - console.error('Error handling message:', error); - } -}; -``` - - - - -### EIP-712 Signature - -In the authentication process, the client must sign messages using EIP-712 structured data signatures. This ensures that the messages are tamper-proof and verifiable by the ClearNode. - -The format of the EIP-712 message is as follows: - -```javascript -{ - "types": { - "EIP712Domain": [ - { "name": "name", "type": "string" } - ], - "Policy": [ - { "name": "challenge", "type": "string" }, - { "name": "scope", "type": "string" }, - { "name": "wallet", "type": "address" }, - { "name": "session_key", "type": "address" }, - { "name": "expires_at", "type": "uint64" }, - { "name": "allowances", "type": "Allowance[]" } - ], - "Allowance": [ - { "name": "asset", "type": "string" }, - { "name": "amount", "type": "string" } - ] - }, - // Domain and primary type - domain: { - name: 'Your App Domain' - }, - primaryType: 'Policy', - message: { - challenge: 'RandomChallengeString', - scope: 'console', - wallet: '0xYourWalletAddress', - session_key: '0xYourSignerAddress', - expires_at: 1762417301, - allowances: [] - } -} -``` - -### Message Signer - -In methods that require signing messages, that are not part of the authentication flow, you should use a custom message signer function `MessageSigner`. This function takes the payload and returns a signed message that can be sent to the ClearNode using ECDSA signature. - -There are also, several things to consider: this method SHOULD sign plain JSON payloads and NOT [ERC-191](https://eips.ethereum.org/EIPS/eip-191) data, because it allows signatures to be compatible with non-EVM chains. Since most of the libraries, like `ethers` or `viem`, use EIP-191 by default, you will need to overwrite the default behavior to sign plain JSON payloads. -The other thing to consider is that providing an EOA private key directly in the code is not recommended for production applications. Instead, we are recommending to generate session keys -- temporary keys that are used for signing messages during the session. This way, you can avoid exposing your main wallet's private key and reduce the risk of compromising your funds. - -The simplest implementation of a message signer function looks like this: - -> **Warning** -> For this example use `ethers` library version `5.7.2`. The `ethers` library version `6.x` has breaking changes that are not allowed in this example. - -```javascript -import { MessageSigner, RequestData, ResponsePayload } from '@erc7824/nitrolite'; -import { ethers } from 'ethers'; -import { Hex } from 'viem'; - -const messageSigner = async (payload: RequestData | ResponsePayload): Promise => { - try { - const wallet = new ethers.Wallet('0xYourPrivateKey'); - - const messageBytes = ethers.utils.arrayify(ethers.utils.id(JSON.stringify(payload))); - - const flatSignature = await wallet._signingKey().signDigest(messageBytes); - - const signature = ethers.utils.joinSignature(flatSignature); - - return signature as Hex; - } catch (error) { - console.error('Error signing message:', error); - throw error; - } -} -``` - -## Getting Channel Information - -After authenticating with a ClearNode, you can request information about your channels. This is useful to verify your connection is working correctly and to retrieve channel data. - -```javascript -import { createGetChannelsMessage, parseRPCResponse, RPCMethod } from '@erc7824/nitrolite'; - -// Example of using the function after authentication is complete -ws.addEventListener('message', async (event) => { - const message = parseRPCResponse(event.data); - - // Check if this is a successful authentication message - if (message.method === RPCMethod.AuthVerify && message.params.success) { - console.log('Successfully authenticated, requesting channel information...'); - - // Request channel information using the built-in helper function - const getChannelsMsg = await createGetChannelsMessage( - messageSigner, // Provide message signer function from previous example - client.stateWalletSigner.getAddress() - ); - - ws.send(getChannelsMsg); - } - - // Handle get_channels response - if (message.method === RPCMethod.GetChannels) { - console.log('Received channels information:'); - const channelsList = message.params; - - if (channelsList && channelsList.length > 0) { - channelsList.forEach((channel, index) => { - console.log(`Channel ${index + 1}:`); - console.log(`- Channel ID: ${channel.channel_id}`); - console.log(`- Status: ${channel.status}`); - console.log(`- Participant: ${channel.participant}`); - console.log(`- Token: ${channel.token}`); - console.log(`- Amount: ${channel.amount}`); - console.log(`- Chain ID: ${channel.chain_id}`); - console.log(`- Adjudicator: ${channel.adjudicator}`); - console.log(`- Challenge: ${channel.challenge}`); - console.log(`- Nonce: ${channel.nonce}`); - console.log(`- Version: ${channel.version}`); - console.log(`- Created: ${channel.created_at}`); - console.log(`- Updated: ${channel.updated_at}`); - }); - } else { - console.log('No active channels found'); - } - } -}); -``` - -### Response Format - -The response to a `get_channels` request includes detailed information about each channel: - -```javascript -{ - "res": [1, "get_channels", [[ // Notice the nested array structure - { - "channel_id": "0xfedcba9876543210...", - "participant": "0x1234567890abcdef...", - "status": "open", // Can be "open", "closed", "settling", etc. - "token": "0xeeee567890abcdef...", // ERC20 token address - "amount": "100000", // Current channel balance - "chain_id": 137, // Chain ID (e.g., 137 for Polygon) - "adjudicator": "0xAdjudicatorContractAddress...", // Contract address - "challenge": 86400, // Challenge period in seconds - "nonce": 1, - "version": 2, - "created_at": "2023-05-01T12:00:00Z", - "updated_at": "2023-05-01T12:30:00Z" - } - ]], 1619123456789], - "sig": ["0xabcd1234..."] -} -``` - -## Framework-Specific Integration - -Here are examples of integrating ClearNode WebSocket connections with various frameworks. Since the Nitrolite SDK doesn't provide its own transport layer, these examples show how to implement WebSocket connections and the NitroliteRPC message format in different frameworks. - - - - -```javascript -import { useState, useEffect, useCallback } from 'react'; -import { ethers } from 'ethers'; -import { - createAuthRequestMessage, - createAuthVerifyMessage, - createGetChannelsMessage, - createGetLedgerBalancesMessage, - createGetConfigMessage, - generateRequestId, - getCurrentTimestamp -} from '@erc7824/nitrolite'; - -// Custom hook for ClearNode connection -function useClearNodeConnection(clearNodeUrl, stateWallet) { - const [ws, setWs] = useState(null); - const [connectionStatus, setConnectionStatus] = useState('disconnected'); - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [error, setError] = useState(null); - - // Message signer function - const messageSigner = useCallback(async (payload) => { - if (!stateWallet) throw new Error('State wallet not available'); - - try { - const message = JSON.stringify(payload); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = stateWallet.signingKey.sign(messageBytes); - return signature; - } catch (error) { - console.error("Error signing message:", error); - throw error; - } - }, [stateWallet]); - - // Create a signed request - const createSignedRequest = useCallback(async (method, params = []) => { - if (!stateWallet) throw new Error('State wallet not available'); - - const requestId = generateRequestId(); - const timestamp = getCurrentTimestamp(); - const requestData = [requestId, method, params, timestamp]; - const request = { req: requestData }; - - // Sign the request - const message = JSON.stringify(request); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = stateWallet.signingKey.sign(messageBytes); - request.sig = [signature]; - - return JSON.stringify(request); - }, [stateWallet]); - - // Send a message to the ClearNode - const sendMessage = useCallback((message) => { - if (!ws || ws.readyState !== WebSocket.OPEN) { - setError('WebSocket not connected'); - return false; - } - - try { - ws.send(typeof message === 'string' ? message : JSON.stringify(message)); - return true; - } catch (error) { - setError(`Error sending message: ${error.message}`); - return false; - } - }, [ws]); - - // Connect to the ClearNode - const connect = useCallback(() => { - if (ws) { - ws.close(); - } - - setConnectionStatus('connecting'); - setError(null); - - const newWs = new WebSocket(clearNodeUrl); - - newWs.onopen = async () => { - setConnectionStatus('connected'); - - // Start authentication process - try { - const authRequest = await createAuthRequestMessage( - messageSigner, - stateWallet.address - ); - newWs.send(authRequest); - } catch (err) { - setError(`Authentication request failed: ${err.message}`); - } - }; - - newWs.onmessage = async (event) => { - try { - const message = JSON.parse(event.data); - - // Handle authentication flow - if (message.res && message.res[1] === 'auth_challenge') { - try { - const authVerify = await createAuthVerifyMessage( - messageSigner, - message, - stateWallet.address - ); - newWs.send(authVerify); - } catch (err) { - setError(`Authentication verification failed: ${err.message}`); - } - } else if (message.res && message.res[1] === 'auth_success') { - setIsAuthenticated(true); - } else if (message.res && message.res[1] === 'auth_failure') { - setIsAuthenticated(false); - setError(`Authentication failed: ${message.res[2]}`); - } - - // Additional message handling can be added here - } catch (err) { - console.error('Error handling message:', err); - } - }; - - newWs.onerror = (error) => { - setError(`WebSocket error: ${error.message}`); - setConnectionStatus('error'); - }; - - newWs.onclose = () => { - setConnectionStatus('disconnected'); - setIsAuthenticated(false); - }; - - setWs(newWs); - }, [clearNodeUrl, messageSigner, stateWallet]); - - // Disconnect from the ClearNode - const disconnect = useCallback(() => { - if (ws) { - ws.close(); - setWs(null); - } - }, [ws]); - - // Connect when the component mounts - useEffect(() => { - if (clearNodeUrl && stateWallet) { - connect(); - } - - // Clean up on unmount - return () => { - if (ws) { - ws.close(); - } - }; - }, [clearNodeUrl, stateWallet, connect]); - - // Create helper methods for common operations - const getChannels = useCallback(async () => { - // Using the built-in helper function from NitroliteRPC - const message = await createGetChannelsMessage( - messageSigner, - stateWallet.address - ); - return sendMessage(message); - }, [messageSigner, sendMessage, stateWallet]); - - const getLedgerBalances = useCallback(async (channelId) => { - // Using the built-in helper function from NitroliteRPC - const message = await createGetLedgerBalancesMessage( - messageSigner, - channelId - ); - return sendMessage(message); - }, [messageSigner, sendMessage]); - - const getConfig = useCallback(async () => { - // Using the built-in helper function from NitroliteRPC - const message = await createGetConfigMessage( - messageSigner, - stateWallet.address - ); - return sendMessage(message); - }, [messageSigner, sendMessage, stateWallet]); - - return { - connectionStatus, - isAuthenticated, - error, - ws, - connect, - disconnect, - sendMessage, - getChannels, - getLedgerBalances, - getConfig, - createSignedRequest - }; -} - -// Example usage in a component -function ClearNodeComponent() { - const stateWallet = /* your state wallet initialization */; - const { - connectionStatus, - isAuthenticated, - error, - getChannels - } = useClearNodeConnection('wss://clearnode.example.com', stateWallet); - - return ( -
-

Status: {connectionStatus}

-

Authenticated: {isAuthenticated ? 'Yes' : 'No'}

- {error &&

Error: {error}

} - - -
- ); -} -``` - -
- - -```typescript -// In an Angular service (clearnode.service.ts) -import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { ethers } from 'ethers'; -import { - createAuthRequestMessage, - createAuthVerifyMessage, - createGetChannelsMessage, - createGetLedgerBalancesMessage, - createGetConfigMessage, - generateRequestId, - getCurrentTimestamp -} from '@erc7824/nitrolite'; - -@Injectable({ - providedIn: 'root' -}) -export class ClearNodeService { - private ws: WebSocket | null = null; - private connectionStatusSource = new BehaviorSubject('disconnected'); - private isAuthenticatedSource = new BehaviorSubject(false); - private errorSource = new BehaviorSubject(null); - private messageSubject = new BehaviorSubject(null); - - // Public observables - connectionStatus$ = this.connectionStatusSource.asObservable(); - isAuthenticated$ = this.isAuthenticatedSource.asObservable(); - error$ = this.errorSource.asObservable(); - message$ = this.messageSubject.asObservable(); - - // Custom message signer - private async messageSigner(payload: any, stateWallet: any): Promise { - try { - const message = JSON.stringify(payload); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = stateWallet.signingKey.sign(messageBytes); - return signature; - } catch (error) { - console.error("Error signing message:", error); - throw error; - } - } - - // Create a signed request - async createSignedRequest(method: string, params: any[], stateWallet: any): Promise { - const requestId = generateRequestId(); - const timestamp = getCurrentTimestamp(); - const requestData = [requestId, method, params, timestamp]; - const request: any = { req: requestData }; - - // Sign the request - const message = JSON.stringify(request); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = stateWallet.signingKey.sign(messageBytes); - request.sig = [signature]; - - return JSON.stringify(request); - } - - // Connect to the ClearNode - connect(url: string, stateWallet: any): void { - if (this.ws) { - this.ws.close(); - } - - this.connectionStatusSource.next('connecting'); - this.errorSource.next(null); - - this.ws = new WebSocket(url); - - this.ws.onopen = async () => { - this.connectionStatusSource.next('connected'); - - // Start authentication process - try { - const authRequest = await createAuthRequestMessage( - (payload) => this.messageSigner(payload, stateWallet), - stateWallet.address - ); - this.ws!.send(authRequest); - } catch (err: any) { - this.errorSource.next(`Authentication request failed: ${err.message}`); - } - }; - - this.ws.onmessage = async (event) => { - try { - const message = JSON.parse(event.data); - this.messageSubject.next(message); - - // Handle authentication flow - if (message.res && message.res[1] === 'auth_challenge') { - try { - const authVerify = await createAuthVerifyMessage( - (payload) => this.messageSigner(payload, stateWallet), - message, - stateWallet.address - ); - this.ws!.send(authVerify); - } catch (err: any) { - this.errorSource.next(`Authentication verification failed: ${err.message}`); - } - } else if (message.res && message.res[1] === 'auth_success') { - this.isAuthenticatedSource.next(true); - } else if (message.res && message.res[1] === 'auth_failure') { - this.isAuthenticatedSource.next(false); - this.errorSource.next(`Authentication failed: ${message.res[2]}`); - } - } catch (err: any) { - console.error('Error handling message:', err); - } - }; - - this.ws.onerror = (error) => { - this.errorSource.next(`WebSocket error`); - this.connectionStatusSource.next('error'); - }; - - this.ws.onclose = () => { - this.connectionStatusSource.next('disconnected'); - this.isAuthenticatedSource.next(false); - }; - } - - // Disconnect from the ClearNode - disconnect(): void { - if (this.ws) { - this.ws.close(); - this.ws = null; - } - } - - // Send a message to the ClearNode - sendMessage(message: string | object): boolean { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - this.errorSource.next('WebSocket not connected'); - return false; - } - - try { - this.ws.send(typeof message === 'string' ? message : JSON.stringify(message)); - return true; - } catch (error: any) { - this.errorSource.next(`Error sending message: ${error.message}`); - return false; - } - } - - // Helper methods for common operations - async getChannels(stateWallet: any): Promise { - // Using the built-in helper function from NitroliteRPC - const message = await createGetChannelsMessage( - (payload) => this.messageSigner(payload, stateWallet), - stateWallet.address - ); - return this.sendMessage(message); - } - - async getLedgerBalances(channelId: string, stateWallet: any): Promise { - // Using the built-in helper function from NitroliteRPC - const message = await createGetLedgerBalancesMessage( - (payload) => this.messageSigner(payload, stateWallet), - channelId - ); - return this.sendMessage(message); - } - - async getConfig(stateWallet: any): Promise { - // Using the built-in helper function from NitroliteRPC - const message = await createGetConfigMessage( - (payload) => this.messageSigner(payload, stateWallet), - stateWallet.address - ); - return this.sendMessage(message); - } -} - -// Usage in a component -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { ClearNodeService } from './clearnode.service'; -import { Subscription } from 'rxjs'; - -@Component({ - selector: 'app-clearnode', - template: ` -
-

Status: {{ connectionStatus }}

-

Authenticated: {{ isAuthenticated ? 'Yes' : 'No' }}

-

Error: {{ error }}

- - -
- ` -}) -export class ClearNodeComponent implements OnInit, OnDestroy { - connectionStatus = 'disconnected'; - isAuthenticated = false; - error: string | null = null; - - private subscriptions: Subscription[] = []; - private stateWallet: any; // Initialize your state wallet - - constructor(private clearNodeService: ClearNodeService) {} - - ngOnInit() { - this.subscriptions.push( - this.clearNodeService.connectionStatus$.subscribe( - status => this.connectionStatus = status - ), - this.clearNodeService.isAuthenticated$.subscribe( - auth => this.isAuthenticated = auth - ), - this.clearNodeService.error$.subscribe( - err => this.error = err - ), - this.clearNodeService.message$.subscribe( - message => { - if (message) { - console.log('Received message:', message); - // Handle specific message types here - } - } - ) - ); - - // Connect to ClearNode - this.clearNodeService.connect('wss://clearnode.example.com', this.stateWallet); - } - - ngOnDestroy() { - this.clearNodeService.disconnect(); - this.subscriptions.forEach(sub => sub.unsubscribe()); - } - - getChannels() { - this.clearNodeService.getChannels(this.stateWallet); - } -} -``` - -
- - -```javascript -// In a Vue component or Composable -import { ref, onMounted, onUnmounted } from 'vue'; -import { ethers } from 'ethers'; -import { - createAuthRequestMessage, - createAuthVerifyMessage, - createGetChannelsMessage, - createGetLedgerBalancesMessage, - createGetConfigMessage, - generateRequestId, - getCurrentTimestamp -} from '@erc7824/nitrolite'; - -// ClearNode connection composable -export function useClearNodeConnection(clearNodeUrl, stateWallet) { - const ws = ref(null); - const connectionStatus = ref('disconnected'); - const isAuthenticated = ref(false); - const error = ref(null); - const messages = ref([]); - - // Message signer function - const messageSigner = async (payload) => { - try { - const message = JSON.stringify(payload); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = stateWallet.signingKey.sign(messageBytes); - return signature; - } catch (error) { - console.error("Error signing message:", error); - throw error; - } - }; - - // Create a signed request - const createSignedRequest = async (method, params = []) => { - const requestId = generateRequestId(); - const timestamp = getCurrentTimestamp(); - const requestData = [requestId, method, params, timestamp]; - const request = { req: requestData }; - - // Sign the request - const message = JSON.stringify(request); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = stateWallet.signingKey.sign(messageBytes); - request.sig = [signature]; - - return JSON.stringify(request); - }; - - // Send a message to the ClearNode - const sendMessage = (message) => { - if (!ws.value || ws.value.readyState !== WebSocket.OPEN) { - error.value = 'WebSocket not connected'; - return false; - } - - try { - ws.value.send(typeof message === 'string' ? message : JSON.stringify(message)); - return true; - } catch (err) { - error.value = `Error sending message: ${err.message}`; - return false; - } - }; - - // Connect to the ClearNode - const connect = () => { - if (ws.value) { - ws.value.close(); - } - - connectionStatus.value = 'connecting'; - error.value = null; - - const newWs = new WebSocket(clearNodeUrl); - - newWs.onopen = async () => { - connectionStatus.value = 'connected'; - - // Start authentication process - try { - const authRequest = await createAuthRequestMessage( - messageSigner, - stateWallet.address - ); - newWs.send(authRequest); - } catch (err) { - error.value = `Authentication request failed: ${err.message}`; - } - }; - - newWs.onmessage = async (event) => { - try { - const message = JSON.parse(event.data); - messages.value.push(message); - - // Handle authentication flow - if (message.res && message.res[1] === 'auth_challenge') { - try { - const authVerify = await createAuthVerifyMessage( - messageSigner, - message, - stateWallet.address - ); - newWs.send(authVerify); - } catch (err) { - error.value = `Authentication verification failed: ${err.message}`; - } - } else if (message.res && message.res[1] === 'auth_success') { - isAuthenticated.value = true; - } else if (message.res && message.res[1] === 'auth_failure') { - isAuthenticated.value = false; - error.value = `Authentication failed: ${message.res[2]}`; - } - } catch (err) { - console.error('Error handling message:', err); - } - }; - - newWs.onerror = (err) => { - error.value = 'WebSocket error'; - connectionStatus.value = 'error'; - }; - - newWs.onclose = () => { - connectionStatus.value = 'disconnected'; - isAuthenticated.value = false; - }; - - ws.value = newWs; - }; - - // Disconnect from the ClearNode - const disconnect = () => { - if (ws.value) { - ws.value.close(); - ws.value = null; - } - }; - - // Helper methods for common operations - const getChannels = async () => { - // Using the built-in helper function from NitroliteRPC - const message = await createGetChannelsMessage( - messageSigner, - stateWallet.address - ); - return sendMessage(message); - }; - - const getLedgerBalances = async (channelId) => { - // Using the built-in helper function from NitroliteRPC - const message = await createGetLedgerBalancesMessage( - messageSigner, - channelId - ); - return sendMessage(message); - }; - - const getConfig = async () => { - // Using the built-in helper function from NitroliteRPC - const message = await createGetConfigMessage( - messageSigner, - stateWallet.address - ); - return sendMessage(message); - }; - - // Connect and disconnect with the component lifecycle - onMounted(() => { - if (clearNodeUrl && stateWallet) { - connect(); - } - }); - - onUnmounted(() => { - disconnect(); - }); - - return { - connectionStatus, - isAuthenticated, - error, - messages, - connect, - disconnect, - sendMessage, - getChannels, - getLedgerBalances, - getConfig, - createSignedRequest - }; -} - -// Example usage in a Vue component -export default { - setup() { - // Initialize your state wallet - const stateWallet = {}; - - const { - connectionStatus, - isAuthenticated, - error, - messages, - getChannels, - getLedgerBalances, - getConfig - } = useClearNodeConnection('wss://clearnode.example.com', stateWallet); - - return { - connectionStatus, - isAuthenticated, - error, - messages, - getChannels, - getLedgerBalances, - getConfig - }; - } -}; -``` - - - - -```javascript -const WebSocket = require('ws'); -const { ethers } = require('ethers'); -const { - createAuthRequestMessage, - createAuthVerifyMessage, - createGetLedgerBalancesMessage, - createGetConfigMessage, - generateRequestId, - getCurrentTimestamp -} = require('@erc7824/nitrolite'); -const EventEmitter = require('events'); - -class ClearNodeConnection extends EventEmitter { - constructor(url, stateWallet) { - super(); - this.url = url; - this.stateWallet = stateWallet; - this.ws = null; - this.isConnected = false; - this.isAuthenticated = false; - this.reconnectAttempts = 0; - this.maxReconnectAttempts = 5; - this.reconnectInterval = 3000; // ms - this.requestMap = new Map(); // Track pending requests - } - - // Message signer function - async messageSigner(payload) { - try { - const message = JSON.stringify(payload); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = this.stateWallet.signingKey.sign(messageBytes); - return signature; - } catch (error) { - console.error("Error signing message:", error); - throw error; - } - } - - // Create a signed request - async createSignedRequest(method, params = [], requestId = generateRequestId()) { - const timestamp = getCurrentTimestamp(); - const requestData = [requestId, method, params, timestamp]; - const request = { req: requestData }; - - // Sign the request - const message = JSON.stringify(request); - const digestHex = ethers.id(message); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = this.stateWallet.signingKey.sign(messageBytes); - request.sig = [signature]; - - return { request, requestId }; - } - - // Connect to the ClearNode - async connect() { - return new Promise((resolve, reject) => { - if (this.ws) { - this.ws.close(); - } - - this.emit('connecting'); - - this.ws = new WebSocket(this.url); - - // Set connection timeout - const connectionTimeout = setTimeout(() => { - if (!this.isConnected) { - this.ws.close(); - reject(new Error('Connection timeout')); - } - }, 10000); - - this.ws.on('open', async () => { - clearTimeout(connectionTimeout); - this.isConnected = true; - this.reconnectAttempts = 0; - this.emit('connected'); - - // Start authentication - try { - const authRequest = await createAuthRequestMessage( - (payload) => this.messageSigner(payload), - this.stateWallet.address - ); - - this.ws.send(authRequest); - // Do not resolve here, wait for auth_success - } catch (error) { - this.emit('error', `Authentication request failed: ${error.message}`); - reject(error); - } - }); - - this.ws.on('message', async (data) => { - try { - const message = JSON.parse(data); - this.emit('message', message); - - // Handle authentication flow - if (message.res && message.res[1] === 'auth_challenge') { - try { - const authVerify = await createAuthVerifyMessage( - (payload) => this.messageSigner(payload), - message, - this.stateWallet.address - ); - - this.ws.send(authVerify); - } catch (error) { - this.emit('error', `Authentication verification failed: ${error.message}`); - reject(error); - } - } else if (message.res && message.res[1] === 'auth_success') { - this.isAuthenticated = true; - this.emit('authenticated'); - resolve(); // Authentication successful - } else if (message.res && message.res[1] === 'auth_failure') { - this.isAuthenticated = false; - const error = new Error(`Authentication failed: ${message.res[2]}`); - this.emit('error', error.message); - reject(error); - } - - // Handle other response types - if (message.res && message.res[0]) { - const requestId = message.res[0]; - const handler = this.requestMap.get(requestId); - if (handler) { - handler.resolve(message); - this.requestMap.delete(requestId); - } - } - } catch (error) { - console.error('Error handling message:', error); - } - }); - - this.ws.on('error', (error) => { - clearTimeout(connectionTimeout); - this.emit('error', `WebSocket error: ${error.message}`); - reject(error); - }); - - this.ws.on('close', (code, reason) => { - clearTimeout(connectionTimeout); - this.isConnected = false; - this.isAuthenticated = false; - this.emit('disconnected', { code, reason: reason.toString() }); - - // Attempt to reconnect - this.attemptReconnect(); - }); - }); - } - - // Send a request and wait for the response - async sendRequest(method, params = []) { - if (!this.isConnected || !this.isAuthenticated) { - throw new Error('Not connected or authenticated'); - } - - const { request, requestId } = await this.createSignedRequest(method, params); - - return new Promise((resolve, reject) => { - // Set up response handler - const timeout = setTimeout(() => { - this.requestMap.delete(requestId); - reject(new Error(`Request timeout for ${method}`)); - }, 30000); - - this.requestMap.set(requestId, { - resolve: (response) => { - clearTimeout(timeout); - resolve(response); - }, - reject, - timeout - }); - - // Send the request - try { - this.ws.send(JSON.stringify(request)); - } catch (error) { - clearTimeout(timeout); - this.requestMap.delete(requestId); - reject(error); - } - }); - } - - // Helper methods for common operations - async getChannels() { - // Using the built-in helper function from NitroliteRPC - const message = await createGetChannelsMessage( - (payload) => this.messageSigner(payload), - this.stateWallet.address - ); - - return new Promise((resolve, reject) => { - try { - const parsed = JSON.parse(message); - const requestId = parsed.req[0]; - - const timeout = setTimeout(() => { - this.requestMap.delete(requestId); - reject(new Error('Request timeout for getChannels')); - }, 30000); - - this.requestMap.set(requestId, { - resolve: (response) => { - clearTimeout(timeout); - resolve(response); - }, - reject, - timeout - }); - - this.ws.send(message); - } catch (error) { - reject(error); - } - }); - } - - async getLedgerBalances(channelId) { - // Using the built-in helper function from NitroliteRPC - const message = await createGetLedgerBalancesMessage( - (payload) => this.messageSigner(payload), - channelId - ); - - return new Promise((resolve, reject) => { - try { - const parsed = JSON.parse(message); - const requestId = parsed.req[0]; - - const timeout = setTimeout(() => { - this.requestMap.delete(requestId); - reject(new Error('Request timeout for getLedgerBalances')); - }, 30000); - - this.requestMap.set(requestId, { - resolve: (response) => { - clearTimeout(timeout); - resolve(response); - }, - reject, - timeout - }); - - this.ws.send(message); - } catch (error) { - reject(error); - } - }); - } - - async getConfig() { - // Using the built-in helper function from NitroliteRPC - const message = await createGetConfigMessage( - (payload) => this.messageSigner(payload), - this.stateWallet.address - ); - - return new Promise((resolve, reject) => { - try { - const parsed = JSON.parse(message); - const requestId = parsed.req[0]; - - const timeout = setTimeout(() => { - this.requestMap.delete(requestId); - reject(new Error('Request timeout for getConfig')); - }, 30000); - - this.requestMap.set(requestId, { - resolve: (response) => { - clearTimeout(timeout); - resolve(response); - }, - reject, - timeout - }); - - this.ws.send(message); - } catch (error) { - reject(error); - } - }); - } - - // Attempt to reconnect with exponential backoff - attemptReconnect() { - if (this.reconnectAttempts >= this.maxReconnectAttempts) { - this.emit('error', 'Maximum reconnection attempts reached'); - return; - } - - this.reconnectAttempts++; - const delay = this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1); - - this.emit('reconnecting', { attempt: this.reconnectAttempts, delay }); - - setTimeout(() => { - this.connect().catch(error => { - console.error(`Reconnection attempt ${this.reconnectAttempts} failed:`, error); - }); - }, delay); - } - - // Disconnect from the ClearNode - disconnect() { - if (this.ws) { - // Clear all pending requests - for (const [requestId, handler] of this.requestMap.entries()) { - clearTimeout(handler.timeout); - handler.reject(new Error('Connection closed')); - this.requestMap.delete(requestId); - } - - this.ws.close(1000, 'User initiated disconnect'); - this.ws = null; - } - } -} - -// Example usage -async function main() { - // Initialize your state wallet (this is just a placeholder) - const privateKey = '0x1234...'; // Your private key - const stateWallet = new ethers.Wallet(privateKey); - - // Create a ClearNode connection - const clearNode = new ClearNodeConnection('wss://clearnode.example.com', stateWallet); - - // Set up event handlers - clearNode.on('connecting', () => { - console.log('Connecting to ClearNode...'); - }); - - clearNode.on('connected', () => { - console.log('Connected to ClearNode'); - }); - - clearNode.on('authenticated', () => { - console.log('Authenticated with ClearNode'); - }); - - clearNode.on('disconnected', ({ code, reason }) => { - console.log(`Disconnected from ClearNode: ${code} ${reason}`); - }); - - clearNode.on('error', (error) => { - console.error(`ClearNode error: ${error}`); - }); - - clearNode.on('reconnecting', ({ attempt, delay }) => { - console.log(`Reconnecting (${attempt}/${clearNode.maxReconnectAttempts}) in ${delay}ms...`); - }); - - try { - // Connect and authenticate - await clearNode.connect(); - console.log('Successfully connected and authenticated'); - - // Get channels - const channels = await clearNode.getChannels(); - console.log('Channels:', channels.res[2][0]); - - // Process the channels - const channelList = channels.res[2][0]; - if (channelList && channelList.length > 0) { - for (const channel of channelList) { - console.log(`Channel ID: ${channel.channel_id}`); - console.log(`Status: ${channel.status}`); - console.log(`Token: ${channel.token}`); - - // Get ledger balances for the channel - if (channel.status === 'open') { - const balances = await clearNode.getLedgerBalances(channel.channel_id); - console.log(`Balances:`, balances.res[2]); - } - } - } else { - console.log('No channels found'); - } - } catch (error) { - console.error('Error:', error); - } finally { - // Disconnect when done - clearNode.disconnect(); - } -} - -// Handle process termination -process.on('SIGINT', () => { - console.log('Shutting down...'); - // Clean up resources here - process.exit(0); -}); - -// Run the example -main().catch(console.error); -``` - - -
- - -## Security Considerations - -When working with ClearNodes and state channels, keep these security best practices in mind: - -1. **Secure State Wallet Storage**: Properly encrypt and secure the private key for your state wallet -2. **Verify Message Signatures**: Always verify that received messages have valid signatures from expected sources -3. **Monitor Connection Status**: Implement monitoring to detect unexpected disconnections or authentication failures -4. **Implement Timeout Handling**: Add timeouts for operations to prevent hanging on unresponsive connections -5. **Validate Channel States**: Verify that channel states are valid before processing or saving them -6. **Use Secure WebSocket Connections**: Always use `wss://` (WebSocket Secure) for ClearNode connections, never `ws://` -7. **Implement Rate Limiting**: Add protection against excessive message sending to prevent abuse - -## Troubleshooting Common Issues - -| Issue | Possible Causes | Solution | -| ------------------------- | ----------------------------------------- | ---------------------------------------------------------------------- | -| Connection timeout | Network latency, ClearNode unavailable | Implement retry logic with exponential backoff | -| Authentication failure | Invalid state wallet, incorrect signing | Verify your state wallet is properly initialized and signing correctly | -| Frequent disconnections | Unstable network, server-side issues | Monitor connection events and implement automatic reconnection | -| Message delivery failures | Connection issues, invalid message format | Add message queuing and confirmation mechanism | -| Invalid signature errors | EIP-191 prefix issues | Ensure you're signing raw message bytes without the EIP-191 prefix | - -## Next Steps - -After successfully connecting to a ClearNode, you can: - -1. [View and manage channel assets](balances) -2. [Create an application session](application_session) diff --git a/erc7824-docs/docs/quick_start/index.mdx b/erc7824-docs/docs/quick_start/index.mdx deleted file mode 100644 index bed5781ac..000000000 --- a/erc7824-docs/docs/quick_start/index.mdx +++ /dev/null @@ -1,86 +0,0 @@ ---- -sidebar_position: 1 -title: Quick Start -description: Learn how to use Nitrolite state channels from start to finish. -keywords: [erc7824, nitrolite, state channels, quick start, tutorial] ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Quick Start - -**[Nitrolite](https://www.npmjs.com/package/@erc7824/nitrolite)** is our official SDK for creating high-performance decentralized applications. It provides a comprehensive set of functions and types to establish WebSocket connections with ClearNode and manage application sessions. - -## Prerequisites - -Before you begin working with Nitrolite, ensure that you have: - -- **Node.js**: Version 16 or later -- **Package Manager**: npm, yarn, or pnpm -- **Development Environment**: - - For frontend: React, Vue, or similar framework - - For backend: Node.js environment -- **Channel Setup**: Create a channel from your account at [apps.yellow.com](https://apps.yellow.com) - -## Installation - -You can install Nitrolite using your preferred package manager: - - - - - ```bash - npm install @erc7824/nitrolite - ``` - - - - - ```bash - yarn add @erc7824/nitrolite - ``` - - - - - ```bash - pnpm add @erc7824/nitrolite - ``` - - - - - -## ClearNode WebSocket - -**ClearNode WebSocket URL**: `wss://clearnet.yellow.com/ws` - - -## Build with AI -We have generated a [llms-full.txt](https://erc7824.org/llms-full.txt) file that converts all our documentation into a single markdown document following the https://llmstxt.org/ standard. - - - -## Complete Workflow - -```mermaid -graph TD - A[Initialize Channel] --> B[Connect to ClearNode] - B --> C[Create Application Session] - C --> D[Perform Operations] - D --> E[Close Session] -``` - -## Next steps - -Building applications with Nitrolite involves these key steps: - -1. **[Channel Creation](initializing_channel)**: Create a channel from your account at apps.yellow.com -2. **[ClearNode Connection](connect_to_the_clearnode)**: Establish WebSocket connection for off-chain messaging -3. **[Application Sessions](application_session)**: Create sessions to run specific applications -4. **[Session Closure](close_session)**: Properly close application sessions when finished - -We recommend working through these guides in sequence to understand the complete application workflow. Each guide builds on concepts from previous sections. - -Start with the [Channel Creation](initializing_channel) guide to begin your journey with Nitrolite applications. diff --git a/erc7824-docs/docs/quick_start/initializing_channel.md b/erc7824-docs/docs/quick_start/initializing_channel.md deleted file mode 100644 index 973622f95..000000000 --- a/erc7824-docs/docs/quick_start/initializing_channel.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -sidebar_position: 2 -title: Create a Channel -description: Create and configure a channel from your account at apps.yellow.com -keywords: [channel creation, apps.yellow.com, setup, configuration] ---- - -# Create a Channel - -Before you can start building with Nitrolite, you need to create a channel from your account. This is done through [apps.yellow.com](https://apps.yellow.com), similar to how you would set up an app in other development platforms. - -## Getting Started - -To get started, visit [apps.yellow.com](https://apps.yellow.com) and create an account. You can use the platform to manage your apps, configurations, channels, and more. - - -## Creating Your First Channel - -1. **Sign up or log in** to [apps.yellow.com](https://apps.yellow.com) -2. **Navigate to your channels** where you can manage them -3. **Create a new channel** by clicking the appropriate button - -That's it! After creating your channel, you can start building your app with your business logic. You don't need to worry about channel credentials - the ClearNode will handle channel identification automatically when you connect. - -## Channel Configuration - -When creating a channel, you'll be able to configure: - -- **Channel name and description** for easy identification -- **Application settings** specific to your use case -- **Access permissions** and participant management -- **Integration settings** for your development environment - -## Next Steps - -Once your channel is created at [apps.yellow.com](https://apps.yellow.com), you're ready to: - -1. **[Start building your App](connect_to_the_clearnode)** - Connect to ClearNode and begin development -2. **[Create application sessions](application_session)** to implement your business logic diff --git a/erc7824-docs/docusaurus.config.js b/erc7824-docs/docusaurus.config.js deleted file mode 100644 index 470a3bbd6..000000000 --- a/erc7824-docs/docusaurus.config.js +++ /dev/null @@ -1,276 +0,0 @@ -// @ts-check -// `@type` JSDoc annotations allow editor autocompletion and type checking -// (when paired with `@ts-check`). -// There are various equivalent ways to declare your Docusaurus config. -// See: https://docusaurus.io/docs/api/docusaurus-config - -import { themes as prismThemes } from "prism-react-renderer"; -import fs from "node:fs"; -import path from "node:path"; - -// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) - -/** @type {import('@docusaurus/types').Config} */ -const config = { - title: "ERC7824 State channels framework", - tagline: "Statechannels norm and framework using nitro protocol", - favicon: "img/favicon.ico", - - // Set the production url of your site here - url: "https://erc7824.org", - // Set the // pathname under which your site is served - // For GitHub pages deployment, it is often '//' - baseUrl: "/", - - // GitHub pages deployment config. - // If you aren't using GitHub pages, you don't need these. - organizationName: "erc7824", // Usually your GitHub org/user name. - projectName: "nitro", // Usually your repo name. - - onBrokenLinks: "throw", - onBrokenMarkdownLinks: "warn", - - // Even if you don't use internationalization, you can use this field to set - // useful metadata like html lang. For example, if your site is Chinese, you - // may want to replace "en" with "zh-Hans". - i18n: { - defaultLocale: "en", - locales: ["en"], - }, - plugins: [ - async function pluginLlmsTxt(context) { - return { - name: "llms-txt-plugin", - loadContent: async () => { - const { siteDir } = context; - const contentDir = path.join(siteDir, "docs"); - const allMdx = []; - - // recursive function to get all mdx files - const getMdxFiles = async (dir) => { - const entries = await fs.promises.readdir(dir, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - await getMdxFiles(fullPath); - } else if ((entry.name.endsWith(".mdx") || entry.name.endsWith(".md")) && fullPath.includes("quick_start")) { - const content = await fs.promises.readFile(fullPath, "utf8"); - allMdx.push({ path: fullPath, content }); - } - } - }; - - // Define the correct order of quick start guides - const quickStartOrder = [ - "initializing_client", - "deposit_and_create_channel", - "connect_to_the_clearnode", - "balances", - "application_session", - "close_session", - "resize_channel", - "close_channel", - "withdrawal", - "index", // Keep index page last - ]; - - // Sort function to order the quick start guides - const sortQuickStartFiles = (files) => { - return files.sort((a, b) => { - // Extract the base filename without extension - const getBaseName = (path) => { - const filename = path.split("/").pop(); - return filename.split(".")[0]; - }; - - const aName = getBaseName(a.path); - const bName = getBaseName(b.path); - - // Get the index in the quickStartOrder array - const aIndex = quickStartOrder.indexOf(aName); - const bIndex = quickStartOrder.indexOf(bName); - - // If both exist in the order array, sort by that order - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - // If only one exists, prioritize it - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - // If neither exists in the order array, maintain alphabetical order - return aName.localeCompare(bName); - }); - }; - - await getMdxFiles(contentDir); - - // Sort the files based on our ordering - const sortedMdx = sortQuickStartFiles(allMdx); - - // Extract just the content in the correct order - const orderedContent = sortedMdx.map((item) => item.content); - - return { allMdx: orderedContent }; - }, - postBuild: async ({ content, routes, outDir }) => { - const { allMdx } = content; - - // Write concatenated MDX content - const concatenatedPath = path.join(outDir, "llms-full.txt"); - await fs.promises.writeFile(concatenatedPath, allMdx.join("\n\n---\n\n")); - - // we need to dig down several layers: - // find PluginRouteConfig marked by plugin.name === "docusaurus-plugin-content-docs" - const docsPluginRouteConfig = routes.filter((route) => route.plugin.name === "docusaurus-plugin-content-docs")[0]; - - // docsPluginRouteConfig has a routes property has a record with the path "/" that contains all docs routes. - const allDocsRouteConfig = docsPluginRouteConfig.routes?.filter((route) => route.path === "/")[0]; - - // A little type checking first - if (!allDocsRouteConfig?.props?.version) { - return; - } - - // this route config has a `props` property that contains the current documentation. - const currentVersionDocsRoutes = allDocsRouteConfig.props.version.docs; - - // for every single docs route we now parse a path (which is the key) and a title - const docsRecords = Object.entries(currentVersionDocsRoutes).map(([path, record]) => { - return `- [${record.title}](${path}): ${record.description}`; - }); - - // Build up llms.txt file - const llmsTxt = `# ${context.siteConfig.title}\n\n## Docs\n\n${docsRecords.join("\n")}`; - - // Write llms.txt file - const llmsTxtPath = path.join(outDir, "llms.txt"); - try { - fs.writeFileSync(llmsTxtPath, llmsTxt); - } catch (err) { - throw err; - } - }, - }; - }, - ], - - // mermaid support - markdown: { - mermaid: true, - }, - themes: ["@docusaurus/theme-mermaid"], - - presets: [ - [ - "classic", - /** @type {import('@docusaurus/preset-classic').Options} */ - ({ - docs: { - routeBasePath: "/", - sidebarPath: "./sidebars.js", - breadcrumbs: false, - }, - blog: false, - theme: { - customCss: "./src/css/custom.css", - }, - }), - ], - ], - - themeConfig: - /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ - ({ - // Replace with your project's social card - image: "img/erc7824_social_card.png", - navbar: { - logo: { - alt: "ERC-7824", - src: "img/logo.svg", - srcDark: "img/logo_dark.svg", - }, - items: [ - { - type: "search", - position: "right", - }, - { - href: "https://github.com/erc7824", - label: "GitHub", - position: "right", - }, - // { - // type: 'html', - // position: 'right', - // value: 'Learn', - // }, - ], - }, - footer: { - style: "dark", - links: [ - { - title: "Docs", - items: [ - { label: "Introduction", to: "/" }, - - ], - }, - { - title: "Community", - items: [ - { - label: "X", - href: "https://x.com/Yellow", - }, - { - label: "Telegram", - href: "https://t.me/yellow_org", - }, - { - label: "Discord", - href: "https://discord.gg/yellownetwork", - }, - ], - }, - { - title: "More", - items: [ - { - label: "GitHub", - href: "https://github.com/erc7824/nitro", - }, - ], - }, - ], - copyright: `Copyright © ${new Date().getFullYear()}, Layer 3 Foundation. ERC-7824 is under MIT License`, - }, - prism: { - theme: prismThemes.github, - darkTheme: prismThemes.dracula, - additionalLanguages: ["solidity"], - magicComments: [ - { - className: 'git-diff-remove', - line: 'remove-next-line', - block: { start: 'remove-start', end: 'remove-end' }, - }, - { - className: 'git-diff-add', - line: 'add-next-line', - block: { start: 'add-start', end: 'add-end' }, - }, - { - className: 'theme-code-block-highlighted-line', - line: 'highlight-next-line', - block: { start: 'highlight-start', end: 'highlight-end' }, - }, - ], - }, - }), -}; - -export default config; diff --git a/erc7824-docs/firebase.json b/erc7824-docs/firebase.json deleted file mode 100644 index f47555498..000000000 --- a/erc7824-docs/firebase.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "hosting": { - "public": "build", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ] - } -} diff --git a/erc7824-docs/package-lock.json b/erc7824-docs/package-lock.json deleted file mode 100644 index b21b4a3b9..000000000 --- a/erc7824-docs/package-lock.json +++ /dev/null @@ -1,19685 +0,0 @@ -{ - "name": "website", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "website", - "version": "0.0.0", - "dependencies": { - "@docusaurus/core": "^3.9.2", - "@docusaurus/preset-classic": "^3.9.2", - "@docusaurus/theme-mermaid": "^3.9.2", - "@mdx-js/react": "^3.1.1", - "clsx": "^2.0.0", - "prism-react-renderer": "^2.3.0", - "react": "^19.2.3", - "react-dom": "^19.2.3" - }, - "devDependencies": { - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/types": "3.7.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@ai-sdk/gateway": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.9.tgz", - "integrity": "sha512-E6x4h5CPPPJ0za1r5HsLtHbeI+Tp3H+YFtcH8G3dSSPFE6w+PZINzB4NxLZmg1QqSeA5HTP3ZEzzsohp0o2GEw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.17", - "@vercel/oidc": "3.0.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/provider": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", - "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", - "license": "Apache-2.0", - "dependencies": { - "json-schema": "^0.4.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@ai-sdk/provider-utils": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz", - "integrity": "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/react": { - "version": "2.0.93", - "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.93.tgz", - "integrity": "sha512-2TzhpQr10HuWxpqyHpSAUMRUqD1G2O73J2sAaJChomVDbjr7BwpM0mdR3aRamCXNtuLiJmTFQhbNzw8fXMBdYw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider-utils": "3.0.17", - "ai": "5.0.93", - "swr": "^2.2.5", - "throttleit": "2.1.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "zod": "^3.25.76 || ^4.1.8" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, - "node_modules/@ai-sdk/react/node_modules/ai": { - "version": "5.0.93", - "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.93.tgz", - "integrity": "sha512-9eGcu+1PJgPg4pRNV4L7tLjRR3wdJC9CXQoNMvtqvYNOLZHFCzjHtVIOr2SIkoJJeu2+sOy3hyiSuTmy2MA40g==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/gateway": "2.0.9", - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.17", - "@opentelemetry/api": "1.9.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@algolia/abtesting": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.10.0.tgz", - "integrity": "sha512-mQT3jwuTgX8QMoqbIR7mPlWkqQqBPQaPabQzm37xg2txMlaMogK/4hCiiESGdg39MlHZOVHeV+0VJuE7f5UK8A==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.44.0", - "@algolia/requester-browser-xhr": "5.44.0", - "@algolia/requester-fetch": "5.44.0", - "@algolia/requester-node-http": "5.44.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/autocomplete-core": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.2.tgz", - "integrity": "sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw==", - "license": "MIT", - "dependencies": { - "@algolia/autocomplete-plugin-algolia-insights": "1.19.2", - "@algolia/autocomplete-shared": "1.19.2" - } - }, - "node_modules/@algolia/autocomplete-plugin-algolia-insights": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.2.tgz", - "integrity": "sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg==", - "license": "MIT", - "dependencies": { - "@algolia/autocomplete-shared": "1.19.2" - }, - "peerDependencies": { - "search-insights": ">= 1 < 3" - } - }, - "node_modules/@algolia/autocomplete-shared": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz", - "integrity": "sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w==", - "license": "MIT", - "peerDependencies": { - "@algolia/client-search": ">= 4.9.1 < 6", - "algoliasearch": ">= 4.9.1 < 6" - } - }, - "node_modules/@algolia/client-abtesting": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.44.0.tgz", - "integrity": "sha512-KY5CcrWhRTUo/lV7KcyjrZkPOOF9bjgWpMj9z98VA+sXzVpZtkuskBLCKsWYFp2sbwchZFTd3wJM48H0IGgF7g==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.44.0", - "@algolia/requester-browser-xhr": "5.44.0", - "@algolia/requester-fetch": "5.44.0", - "@algolia/requester-node-http": "5.44.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-analytics": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.44.0.tgz", - "integrity": "sha512-LKOCE8S4ewI9bN3ot9RZoYASPi8b78E918/DVPW3HHjCMUe6i+NjbNG6KotU4RpP6AhRWZjjswbOkWelUO+OoA==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.44.0", - "@algolia/requester-browser-xhr": "5.44.0", - "@algolia/requester-fetch": "5.44.0", - "@algolia/requester-node-http": "5.44.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-common": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.44.0.tgz", - "integrity": "sha512-1yyJm4OYC2cztbS28XYVWwLXdwpLsMG4LoZLOltVglQ2+hc/i9q9fUDZyjRa2Bqt4DmkIfezagfMrokhyH4uxQ==", - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-insights": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.44.0.tgz", - "integrity": "sha512-wVQWK6jYYsbEOjIMI+e5voLGPUIbXrvDj392IckXaCPvQ6vCMTXakQqOYCd+znQdL76S+3wHDo77HZWiAYKrtA==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.44.0", - "@algolia/requester-browser-xhr": "5.44.0", - "@algolia/requester-fetch": "5.44.0", - "@algolia/requester-node-http": "5.44.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-personalization": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.44.0.tgz", - "integrity": "sha512-lkgRjOjOkqmIkebHjHpU9rLJcJNUDMm+eVSW/KJQYLjGqykEZxal+nYJJTBbLceEU2roByP/+27ZmgIwCdf0iA==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.44.0", - "@algolia/requester-browser-xhr": "5.44.0", - "@algolia/requester-fetch": "5.44.0", - "@algolia/requester-node-http": "5.44.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-query-suggestions": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.44.0.tgz", - "integrity": "sha512-sYfhgwKu6NDVmZHL1WEKVLsOx/jUXCY4BHKLUOcYa8k4COCs6USGgz6IjFkUf+niwq8NCECMmTC4o/fVQOalsA==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.44.0", - "@algolia/requester-browser-xhr": "5.44.0", - "@algolia/requester-fetch": "5.44.0", - "@algolia/requester-node-http": "5.44.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-search": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.44.0.tgz", - "integrity": "sha512-/FRKUM1G4xn3vV8+9xH1WJ9XknU8rkBGlefruq9jDhYUAvYozKimhrmC2pRqw/RyHhPivmgZCRuC8jHP8piz4Q==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.44.0", - "@algolia/requester-browser-xhr": "5.44.0", - "@algolia/requester-fetch": "5.44.0", - "@algolia/requester-node-http": "5.44.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/events": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz", - "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==", - "license": "MIT" - }, - "node_modules/@algolia/ingestion": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.44.0.tgz", - "integrity": "sha512-5+S5ynwMmpTpCLXGjTDpeIa81J+R4BLH0lAojOhmeGSeGEHQTqacl/4sbPyDTcidvnWhaqtyf8m42ue6lvISAw==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.44.0", - "@algolia/requester-browser-xhr": "5.44.0", - "@algolia/requester-fetch": "5.44.0", - "@algolia/requester-node-http": "5.44.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/monitoring": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.44.0.tgz", - "integrity": "sha512-xhaTN8pXJjR6zkrecg4Cc9YZaQK2LKm2R+LkbAq+AYGBCWJxtSGlNwftozZzkUyq4AXWoyoc0x2SyBtq5LRtqQ==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.44.0", - "@algolia/requester-browser-xhr": "5.44.0", - "@algolia/requester-fetch": "5.44.0", - "@algolia/requester-node-http": "5.44.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/recommend": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.44.0.tgz", - "integrity": "sha512-GNcite/uOIS7wgRU1MT7SdNIupGSW+vbK9igIzMePvD2Dl8dy0O3urKPKIbTuZQqiVH1Cb84y5cgLvwNrdCj/Q==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.44.0", - "@algolia/requester-browser-xhr": "5.44.0", - "@algolia/requester-fetch": "5.44.0", - "@algolia/requester-node-http": "5.44.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/requester-browser-xhr": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.44.0.tgz", - "integrity": "sha512-YZHBk72Cd7pcuNHzbhNzF/FbbYszlc7JhZlDyQAchnX5S7tcemSS96F39Sy8t4O4WQLpFvUf1MTNedlitWdOsQ==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.44.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/requester-fetch": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.44.0.tgz", - "integrity": "sha512-B9WHl+wQ7uf46t9cq+vVM/ypVbOeuldVDq9OtKsX2ApL2g/htx6ImB9ugDOOJmB5+fE31/XPTuCcYz/j03+idA==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.44.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/requester-node-http": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.44.0.tgz", - "integrity": "sha512-MULm0qeAIk4cdzZ/ehJnl1o7uB5NMokg83/3MKhPq0Pk7+I0uELGNbzIfAkvkKKEYcHALemKdArtySF9eKzh/A==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.44.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@antfu/install-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", - "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", - "license": "MIT", - "dependencies": { - "package-manager-detector": "^1.3.0", - "tinyexec": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@antfu/utils": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-9.2.1.tgz", - "integrity": "sha512-TMilPqXyii1AsiEii6l6ubRzbo76p6oshUSYPaKsmXDavyMLqjzVDkcp3pHp5ELMUNJHATcEOGxKTTsX9yYhGg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", - "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.5", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", - "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "regexpu-core": "^6.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", - "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "debug": "^4.4.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.22.10" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", - "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", - "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", - "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", - "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", - "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", - "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", - "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", - "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", - "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", - "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/template": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", - "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", - "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", - "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", - "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", - "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", - "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", - "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", - "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", - "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", - "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", - "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", - "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", - "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-constant-elements": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", - "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", - "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", - "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", - "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", - "license": "MIT", - "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", - "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", - "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", - "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz", - "integrity": "sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", - "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz", - "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", - "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", - "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz", - "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.27.1", - "@babel/plugin-syntax-import-attributes": "^7.27.1", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.0", - "@babel/plugin-transform-async-to-generator": "^7.27.1", - "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.5", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.4", - "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-dotall-regex": "^7.27.1", - "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.28.5", - "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/plugin-transform-for-of": "^7.27.1", - "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.27.1", - "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", - "@babel/plugin-transform-member-expression-literals": "^7.27.1", - "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.28.5", - "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", - "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.4", - "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.28.5", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.27.1", - "@babel/plugin-transform-private-property-in-object": "^7.27.1", - "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.4", - "@babel/plugin-transform-regexp-modifiers": "^7.27.1", - "@babel/plugin-transform-reserved-words": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.27.1", - "@babel/plugin-transform-sticky-regex": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-typeof-symbol": "^7.27.1", - "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.27.1", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "core-js-compat": "^3.43.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/preset-react": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", - "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-transform-react-display-name": "^7.28.0", - "@babel/plugin-transform-react-jsx": "^7.27.1", - "@babel/plugin-transform-react-jsx-development": "^7.27.1", - "@babel/plugin-transform-react-pure-annotations": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", - "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", - "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==", - "license": "MIT", - "dependencies": { - "core-js-pure": "^3.43.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@braintree/sanitize-url": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", - "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", - "license": "MIT" - }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.1.tgz", - "integrity": "sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/gast": "11.1.1", - "@chevrotain/types": "11.1.1", - "lodash-es": "4.17.23" - } - }, - "node_modules/@chevrotain/gast": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.1.tgz", - "integrity": "sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/types": "11.1.1", - "lodash-es": "4.17.23" - } - }, - "node_modules/@chevrotain/regexp-to-ast": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.1.tgz", - "integrity": "sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/types": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.1.tgz", - "integrity": "sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/utils": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.1.tgz", - "integrity": "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ==", - "license": "Apache-2.0" - }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@csstools/cascade-layer-name-parser": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz", - "integrity": "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/media-query-list-parser": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", - "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/postcss-alpha-function": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-alpha-function/-/postcss-alpha-function-1.0.1.tgz", - "integrity": "sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-cascade-layers": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz", - "integrity": "sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/selector-specificity": "^5.0.0", - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-cascade-layers/node_modules/@csstools/selector-specificity": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", - "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^7.0.0" - } - }, - "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@csstools/postcss-color-function": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.12.tgz", - "integrity": "sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-color-function-display-p3-linear": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function-display-p3-linear/-/postcss-color-function-display-p3-linear-1.0.1.tgz", - "integrity": "sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-color-mix-function": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.12.tgz", - "integrity": "sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-color-mix-variadic-function-arguments": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.2.tgz", - "integrity": "sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-content-alt-text": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.8.tgz", - "integrity": "sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-contrast-color-function": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@csstools/postcss-contrast-color-function/-/postcss-contrast-color-function-2.0.12.tgz", - "integrity": "sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-exponential-functions": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz", - "integrity": "sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-font-format-keywords": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz", - "integrity": "sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-gamut-mapping": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.11.tgz", - "integrity": "sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-gradients-interpolation-method": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.12.tgz", - "integrity": "sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-hwb-function": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.12.tgz", - "integrity": "sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-ic-unit": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.4.tgz", - "integrity": "sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-initial": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz", - "integrity": "sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz", - "integrity": "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/selector-specificity": "^5.0.0", - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", - "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^7.0.0" - } - }, - "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@csstools/postcss-light-dark-function": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.11.tgz", - "integrity": "sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-logical-float-and-clear": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz", - "integrity": "sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-logical-overflow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz", - "integrity": "sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-logical-overscroll-behavior": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz", - "integrity": "sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-logical-resize": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz", - "integrity": "sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-logical-viewport-units": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz", - "integrity": "sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-media-minmax": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz", - "integrity": "sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/media-query-list-parser": "^4.0.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz", - "integrity": "sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/media-query-list-parser": "^4.0.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-nested-calc": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz", - "integrity": "sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-normalize-display-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz", - "integrity": "sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-oklab-function": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.12.tgz", - "integrity": "sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.2.1.tgz", - "integrity": "sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-random-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz", - "integrity": "sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-relative-color-syntax": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.12.tgz", - "integrity": "sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-scope-pseudo-class": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz", - "integrity": "sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@csstools/postcss-sign-functions": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz", - "integrity": "sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz", - "integrity": "sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-text-decoration-shorthand": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.3.tgz", - "integrity": "sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-trigonometric-functions": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz", - "integrity": "sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-unset-value": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz", - "integrity": "sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/utilities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-2.0.0.tgz", - "integrity": "sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docsearch/core": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@docsearch/core/-/core-4.3.1.tgz", - "integrity": "sha512-ktVbkePE+2h9RwqCUMbWXOoebFyDOxHqImAqfs+lC8yOU+XwEW4jgvHGJK079deTeHtdhUNj0PXHSnhJINvHzQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": ">= 16.8.0 < 20.0.0", - "react": ">= 16.8.0 < 20.0.0", - "react-dom": ">= 16.8.0 < 20.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/@docsearch/css": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.3.2.tgz", - "integrity": "sha512-K3Yhay9MgkBjJJ0WEL5MxnACModX9xuNt3UlQQkDEDZJZ0+aeWKtOkxHNndMRkMBnHdYvQjxkm6mdlneOtU1IQ==", - "license": "MIT" - }, - "node_modules/@docsearch/react": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.3.2.tgz", - "integrity": "sha512-74SFD6WluwvgsOPqifYOviEEVwDxslxfhakTlra+JviaNcs7KK/rjsPj89kVEoQc9FUxRkAofaJnHIR7pb4TSQ==", - "license": "MIT", - "dependencies": { - "@ai-sdk/react": "^2.0.30", - "@algolia/autocomplete-core": "1.19.2", - "@docsearch/core": "4.3.1", - "@docsearch/css": "4.3.2", - "ai": "^5.0.30", - "algoliasearch": "^5.28.0", - "marked": "^16.3.0", - "zod": "^4.1.8" - }, - "peerDependencies": { - "@types/react": ">= 16.8.0 < 20.0.0", - "react": ">= 16.8.0 < 20.0.0", - "react-dom": ">= 16.8.0 < 20.0.0", - "search-insights": ">= 1 < 3" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "search-insights": { - "optional": true - } - } - }, - "node_modules/@docusaurus/babel": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.9.2.tgz", - "integrity": "sha512-GEANdi/SgER+L7Japs25YiGil/AUDnFFHaCGPBbundxoWtCkA2lmy7/tFmgED4y1htAy6Oi4wkJEQdGssnw9MA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.25.9", - "@babel/preset-env": "^7.25.9", - "@babel/preset-react": "^7.25.9", - "@babel/preset-typescript": "^7.25.9", - "@babel/runtime": "^7.25.9", - "@babel/runtime-corejs3": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@docusaurus/logger": "3.9.2", - "@docusaurus/utils": "3.9.2", - "babel-plugin-dynamic-import-node": "^2.3.3", - "fs-extra": "^11.1.1", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=20.0" - } - }, - "node_modules/@docusaurus/bundler": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.9.2.tgz", - "integrity": "sha512-ZOVi6GYgTcsZcUzjblpzk3wH1Fya2VNpd5jtHoCCFcJlMQ1EYXZetfAnRHLcyiFeBABaI1ltTYbOBtH/gahGVA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.9", - "@docusaurus/babel": "3.9.2", - "@docusaurus/cssnano-preset": "3.9.2", - "@docusaurus/logger": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "babel-loader": "^9.2.1", - "clean-css": "^5.3.3", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.11.0", - "css-minimizer-webpack-plugin": "^5.0.1", - "cssnano": "^6.1.2", - "file-loader": "^6.2.0", - "html-minifier-terser": "^7.2.0", - "mini-css-extract-plugin": "^2.9.2", - "null-loader": "^4.0.1", - "postcss": "^8.5.4", - "postcss-loader": "^7.3.4", - "postcss-preset-env": "^10.2.1", - "terser-webpack-plugin": "^5.3.9", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "webpack": "^5.95.0", - "webpackbar": "^6.0.1" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "@docusaurus/faster": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/faster": { - "optional": true - } - } - }, - "node_modules/@docusaurus/bundler/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/bundler/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/core": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.9.2.tgz", - "integrity": "sha512-HbjwKeC+pHUFBfLMNzuSjqFE/58+rLVKmOU3lxQrpsxLBOGosYco/Q0GduBb0/jEMRiyEqjNT/01rRdOMWq5pw==", - "license": "MIT", - "dependencies": { - "@docusaurus/babel": "3.9.2", - "@docusaurus/bundler": "3.9.2", - "@docusaurus/logger": "3.9.2", - "@docusaurus/mdx-loader": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-common": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", - "boxen": "^6.2.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "cli-table3": "^0.6.3", - "combine-promises": "^1.1.0", - "commander": "^5.1.0", - "core-js": "^3.31.1", - "detect-port": "^1.5.1", - "escape-html": "^1.0.3", - "eta": "^2.2.0", - "eval": "^0.1.8", - "execa": "5.1.1", - "fs-extra": "^11.1.1", - "html-tags": "^3.3.1", - "html-webpack-plugin": "^5.6.0", - "leven": "^3.1.0", - "lodash": "^4.17.21", - "open": "^8.4.0", - "p-map": "^4.0.0", - "prompts": "^2.4.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", - "react-router": "^5.3.4", - "react-router-config": "^5.1.1", - "react-router-dom": "^5.3.4", - "semver": "^7.5.4", - "serve-handler": "^6.1.6", - "tinypool": "^1.0.2", - "tslib": "^2.6.0", - "update-notifier": "^6.0.2", - "webpack": "^5.95.0", - "webpack-bundle-analyzer": "^4.10.2", - "webpack-dev-server": "^5.2.2", - "webpack-merge": "^6.0.1" - }, - "bin": { - "docusaurus": "bin/docusaurus.mjs" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "@mdx-js/react": "^3.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/cssnano-preset": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.9.2.tgz", - "integrity": "sha512-8gBKup94aGttRduABsj7bpPFTX7kbwu+xh3K9NMCF5K4bWBqTFYW+REKHF6iBVDHRJ4grZdIPbvkiHd/XNKRMQ==", - "license": "MIT", - "dependencies": { - "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.5.4", - "postcss-sort-media-queries": "^5.2.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=20.0" - } - }, - "node_modules/@docusaurus/logger": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.9.2.tgz", - "integrity": "sha512-/SVCc57ByARzGSU60c50rMyQlBuMIJCjcsJlkphxY6B0GV4UH3tcA1994N8fFfbJ9kX3jIBe/xg3XP5qBtGDbA==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=20.0" - } - }, - "node_modules/@docusaurus/mdx-loader": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.9.2.tgz", - "integrity": "sha512-wiYoGwF9gdd6rev62xDU8AAM8JuLI/hlwOtCzMmYcspEkzecKrP8J8X+KpYnTlACBUUtXNJpSoCwFWJhLRevzQ==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", - "@mdx-js/mdx": "^3.0.0", - "@slorber/remark-comment": "^1.0.0", - "escape-html": "^1.0.3", - "estree-util-value-to-estree": "^3.0.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "image-size": "^2.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "rehype-raw": "^7.0.0", - "remark-directive": "^3.0.0", - "remark-emoji": "^4.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "stringify-object": "^3.3.0", - "tslib": "^2.6.0", - "unified": "^11.0.3", - "unist-util-visit": "^5.0.0", - "url-loader": "^4.1.1", - "vfile": "^6.0.1", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/module-type-aliases": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.7.0.tgz", - "integrity": "sha512-g7WdPqDNaqA60CmBrr0cORTrsOit77hbsTj7xE2l71YhBn79sxdm7WMK7wfhcaafkbpIh7jv5ef5TOpf1Xv9Lg==", - "dev": true, - "dependencies": { - "@docusaurus/types": "3.7.0", - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router-config": "*", - "@types/react-router-dom": "*", - "react-helmet-async": "npm:@slorber/react-helmet-async@*", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/@docusaurus/plugin-content-blog": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.9.2.tgz", - "integrity": "sha512-3I2HXy3L1QcjLJLGAoTvoBnpOwa6DPUa3Q0dMK19UTY9mhPkKQg/DYhAGTiBUKcTR0f08iw7kLPqOhIgdV3eVQ==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/logger": "3.9.2", - "@docusaurus/mdx-loader": "3.9.2", - "@docusaurus/theme-common": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-common": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", - "cheerio": "1.0.0-rc.12", - "feed": "^4.2.2", - "fs-extra": "^11.1.1", - "lodash": "^4.17.21", - "schema-dts": "^1.1.2", - "srcset": "^4.0.0", - "tslib": "^2.6.0", - "unist-util-visit": "^5.0.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "@docusaurus/plugin-content-docs": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-docs": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", - "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/logger": "3.9.2", - "@docusaurus/mdx-loader": "3.9.2", - "@docusaurus/module-type-aliases": "3.9.2", - "@docusaurus/theme-common": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-common": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", - "@types/react-router-config": "^5.0.7", - "combine-promises": "^1.1.0", - "fs-extra": "^11.1.1", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "schema-dts": "^1.1.2", - "tslib": "^2.6.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/module-type-aliases": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.9.2.tgz", - "integrity": "sha512-8qVe2QA9hVLzvnxP46ysuofJUIc/yYQ82tvA/rBTrnpXtCjNSFLxEZfd5U8cYZuJIVlkPxamsIgwd5tGZXfvew==", - "license": "MIT", - "dependencies": { - "@docusaurus/types": "3.9.2", - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router-config": "*", - "@types/react-router-dom": "*", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-docs/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-pages": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.9.2.tgz", - "integrity": "sha512-s4849w/p4noXUrGpPUF0BPqIAfdAe76BLaRGAGKZ1gTDNiGxGcpsLcwJ9OTi1/V8A+AzvsmI9pkjie2zjIQZKA==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/mdx-loader": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", - "fs-extra": "^11.1.1", - "tslib": "^2.6.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-pages/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-pages/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-css-cascade-layers": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.9.2.tgz", - "integrity": "sha512-w1s3+Ss+eOQbscGM4cfIFBlVg/QKxyYgj26k5AnakuHkKxH6004ZtuLe5awMBotIYF2bbGDoDhpgQ4r/kcj4rQ==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=20.0" - } - }, - "node_modules/@docusaurus/plugin-css-cascade-layers/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-css-cascade-layers/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-debug": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.9.2.tgz", - "integrity": "sha512-j7a5hWuAFxyQAkilZwhsQ/b3T7FfHZ+0dub6j/GxKNFJp2h9qk/P1Bp7vrGASnvA9KNQBBL1ZXTe7jlh4VdPdA==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "fs-extra": "^11.1.1", - "react-json-view-lite": "^2.3.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-debug/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-debug/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-analytics": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.9.2.tgz", - "integrity": "sha512-mAwwQJ1Us9jL/lVjXtErXto4p4/iaLlweC54yDUK1a97WfkC6Z2k5/769JsFgwOwOP+n5mUQGACXOEQ0XDuVUw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-analytics/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-analytics/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-gtag": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.9.2.tgz", - "integrity": "sha512-YJ4lDCphabBtw19ooSlc1MnxtYGpjFV9rEdzjLsUnBCeis2djUyCozZaFhCg6NGEwOn7HDDyMh0yzcdRpnuIvA==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", - "@types/gtag.js": "^0.0.12", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-gtag/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-gtag/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-tag-manager": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.9.2.tgz", - "integrity": "sha512-LJtIrkZN/tuHD8NqDAW1Tnw0ekOwRTfobWPsdO15YxcicBo2ykKF0/D6n0vVBfd3srwr9Z6rzrIWYrMzBGrvNw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-tag-manager/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-tag-manager/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-sitemap": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.9.2.tgz", - "integrity": "sha512-WLh7ymgDXjG8oPoM/T4/zUP7KcSuFYRZAUTl8vR6VzYkfc18GBM4xLhcT+AKOwun6kBivYKUJf+vlqYJkm+RHw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/logger": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-common": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", - "fs-extra": "^11.1.1", - "sitemap": "^7.1.1", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-sitemap/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-sitemap/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/plugin-svgr": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.9.2.tgz", - "integrity": "sha512-n+1DE+5b3Lnf27TgVU5jM1d4x5tUh2oW5LTsBxJX4PsAPV0JGcmI6p3yLYtEY0LRVEIJh+8RsdQmRE66wSV8mw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", - "@svgr/core": "8.1.0", - "@svgr/webpack": "^8.1.0", - "tslib": "^2.6.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-svgr/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-svgr/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/preset-classic": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.9.2.tgz", - "integrity": "sha512-IgyYO2Gvaigi21LuDIe+nvmN/dfGXAiMcV/murFqcpjnZc7jxFAxW+9LEjdPt61uZLxG4ByW/oUmX/DDK9t/8w==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/plugin-content-blog": "3.9.2", - "@docusaurus/plugin-content-docs": "3.9.2", - "@docusaurus/plugin-content-pages": "3.9.2", - "@docusaurus/plugin-css-cascade-layers": "3.9.2", - "@docusaurus/plugin-debug": "3.9.2", - "@docusaurus/plugin-google-analytics": "3.9.2", - "@docusaurus/plugin-google-gtag": "3.9.2", - "@docusaurus/plugin-google-tag-manager": "3.9.2", - "@docusaurus/plugin-sitemap": "3.9.2", - "@docusaurus/plugin-svgr": "3.9.2", - "@docusaurus/theme-classic": "3.9.2", - "@docusaurus/theme-common": "3.9.2", - "@docusaurus/theme-search-algolia": "3.9.2", - "@docusaurus/types": "3.9.2" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/preset-classic/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/theme-classic": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.9.2.tgz", - "integrity": "sha512-IGUsArG5hhekXd7RDb11v94ycpJpFdJPkLnt10fFQWOVxAtq5/D7hT6lzc2fhyQKaaCE62qVajOMKL7OiAFAIA==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/logger": "3.9.2", - "@docusaurus/mdx-loader": "3.9.2", - "@docusaurus/module-type-aliases": "3.9.2", - "@docusaurus/plugin-content-blog": "3.9.2", - "@docusaurus/plugin-content-docs": "3.9.2", - "@docusaurus/plugin-content-pages": "3.9.2", - "@docusaurus/theme-common": "3.9.2", - "@docusaurus/theme-translations": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-common": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", - "@mdx-js/react": "^3.0.0", - "clsx": "^2.0.0", - "infima": "0.2.0-alpha.45", - "lodash": "^4.17.21", - "nprogress": "^0.2.0", - "postcss": "^8.5.4", - "prism-react-renderer": "^2.3.0", - "prismjs": "^1.29.0", - "react-router-dom": "^5.3.4", - "rtlcss": "^4.1.0", - "tslib": "^2.6.0", - "utility-types": "^3.10.0" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/module-type-aliases": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.9.2.tgz", - "integrity": "sha512-8qVe2QA9hVLzvnxP46ysuofJUIc/yYQ82tvA/rBTrnpXtCjNSFLxEZfd5U8cYZuJIVlkPxamsIgwd5tGZXfvew==", - "license": "MIT", - "dependencies": { - "@docusaurus/types": "3.9.2", - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router-config": "*", - "@types/react-router-dom": "*", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/theme-classic/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/theme-common": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.2.tgz", - "integrity": "sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==", - "license": "MIT", - "dependencies": { - "@docusaurus/mdx-loader": "3.9.2", - "@docusaurus/module-type-aliases": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-common": "3.9.2", - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router-config": "*", - "clsx": "^2.0.0", - "parse-numeric-range": "^1.3.0", - "prism-react-renderer": "^2.3.0", - "tslib": "^2.6.0", - "utility-types": "^3.10.0" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "@docusaurus/plugin-content-docs": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/theme-common/node_modules/@docusaurus/module-type-aliases": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.9.2.tgz", - "integrity": "sha512-8qVe2QA9hVLzvnxP46ysuofJUIc/yYQ82tvA/rBTrnpXtCjNSFLxEZfd5U8cYZuJIVlkPxamsIgwd5tGZXfvew==", - "license": "MIT", - "dependencies": { - "@docusaurus/types": "3.9.2", - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router-config": "*", - "@types/react-router-dom": "*", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/@docusaurus/theme-common/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/theme-common/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/theme-mermaid": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.9.2.tgz", - "integrity": "sha512-5vhShRDq/ntLzdInsQkTdoKWSzw8d1jB17sNPYhA/KvYYFXfuVEGHLM6nrf8MFbV8TruAHDG21Fn3W4lO8GaDw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.9.2", - "@docusaurus/module-type-aliases": "3.9.2", - "@docusaurus/theme-common": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", - "mermaid": ">=11.6.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "@mermaid-js/layout-elk": "^0.1.9", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@mermaid-js/layout-elk": { - "optional": true - } - } - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/@docusaurus/module-type-aliases": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.9.2.tgz", - "integrity": "sha512-8qVe2QA9hVLzvnxP46ysuofJUIc/yYQ82tvA/rBTrnpXtCjNSFLxEZfd5U8cYZuJIVlkPxamsIgwd5tGZXfvew==", - "license": "MIT", - "dependencies": { - "@docusaurus/types": "3.9.2", - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router-config": "*", - "@types/react-router-dom": "*", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/theme-mermaid/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/theme-search-algolia": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.2.tgz", - "integrity": "sha512-GBDSFNwjnh5/LdkxCKQHkgO2pIMX1447BxYUBG2wBiajS21uj64a+gH/qlbQjDLxmGrbrllBrtJkUHxIsiwRnw==", - "license": "MIT", - "dependencies": { - "@docsearch/react": "^3.9.0 || ^4.1.0", - "@docusaurus/core": "3.9.2", - "@docusaurus/logger": "3.9.2", - "@docusaurus/plugin-content-docs": "3.9.2", - "@docusaurus/theme-common": "3.9.2", - "@docusaurus/theme-translations": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-validation": "3.9.2", - "algoliasearch": "^5.37.0", - "algoliasearch-helper": "^3.26.0", - "clsx": "^2.0.0", - "eta": "^2.2.0", - "fs-extra": "^11.1.1", - "lodash": "^4.17.21", - "tslib": "^2.6.0", - "utility-types": "^3.10.0" - }, - "engines": { - "node": ">=20.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/theme-translations": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.9.2.tgz", - "integrity": "sha512-vIryvpP18ON9T9rjgMRFLr2xJVDpw1rtagEGf8Ccce4CkTrvM/fRB8N2nyWYOW5u3DdjkwKw5fBa+3tbn9P4PA==", - "license": "MIT", - "dependencies": { - "fs-extra": "^11.1.1", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=20.0" - } - }, - "node_modules/@docusaurus/types": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.7.0.tgz", - "integrity": "sha512-kOmZg5RRqJfH31m+6ZpnwVbkqMJrPOG5t0IOl4i/+3ruXyNfWzZ0lVtVrD0u4ONc/0NOsS9sWYaxxWNkH1LdLQ==", - "dev": true, - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/types/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "dev": true, - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/utils": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.9.2.tgz", - "integrity": "sha512-lBSBiRruFurFKXr5Hbsl2thmGweAPmddhF3jb99U4EMDA5L+e5Y1rAkOS07Nvrup7HUMBDrCV45meaxZnt28nQ==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.9.2", - "@docusaurus/types": "3.9.2", - "@docusaurus/utils-common": "3.9.2", - "escape-string-regexp": "^4.0.0", - "execa": "5.1.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "github-slugger": "^1.5.0", - "globby": "^11.1.0", - "gray-matter": "^4.0.3", - "jiti": "^1.20.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "micromatch": "^4.0.5", - "p-queue": "^6.6.2", - "prompts": "^2.4.2", - "resolve-pathname": "^3.0.0", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=20.0" - } - }, - "node_modules/@docusaurus/utils-common": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.9.2.tgz", - "integrity": "sha512-I53UC1QctruA6SWLvbjbhCpAw7+X7PePoe5pYcwTOEXD/PxeP8LnECAhTHHwWCblyUX5bMi4QLRkxvyZ+IT8Aw==", - "license": "MIT", - "dependencies": { - "@docusaurus/types": "3.9.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=20.0" - } - }, - "node_modules/@docusaurus/utils-common/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/utils-common/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/utils-validation": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.9.2.tgz", - "integrity": "sha512-l7yk3X5VnNmATbwijJkexdhulNsQaNDwoagiwujXoxFbWLcxHQqNQ+c/IAlzrfMMOfa/8xSBZ7KEKDesE/2J7A==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.9.2", - "@docusaurus/utils": "3.9.2", - "@docusaurus/utils-common": "3.9.2", - "fs-extra": "^11.2.0", - "joi": "^17.9.2", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=20.0" - } - }, - "node_modules/@docusaurus/utils/node_modules/@docusaurus/types": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", - "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/mdast": "^4.0.2", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/utils/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@iconify/types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", - "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", - "license": "MIT" - }, - "node_modules/@iconify/utils": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.0.2.tgz", - "integrity": "sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==", - "license": "MIT", - "dependencies": { - "@antfu/install-pkg": "^1.1.0", - "@antfu/utils": "^9.2.0", - "@iconify/types": "^2.0.0", - "debug": "^4.4.1", - "globals": "^15.15.0", - "kolorist": "^1.8.0", - "local-pkg": "^1.1.1", - "mlly": "^1.7.4" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jsonjoy.com/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/buffers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz", - "integrity": "sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/codegen": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", - "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pack": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.14.0.tgz", - "integrity": "sha512-LpWbYgVnKzphN5S6uss4M25jJ/9+m6q6UJoeN6zTkK4xAGhKsiBRPVeF7OYMWonn5repMQbE5vieRXcMUrKDKw==", - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.1", - "@jsonjoy.com/util": "^1.9.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pointer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", - "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/util": "^1.9.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", - "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "license": "MIT" - }, - "node_modules/@mdx-js/mdx": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.0.tgz", - "integrity": "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdx": "^2.0.0", - "collapse-white-space": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-util-scope": "^1.0.0", - "estree-walker": "^3.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "markdown-extensions": "^2.0.0", - "recma-build-jsx": "^1.0.0", - "recma-jsx": "^1.0.0", - "recma-stringify": "^1.0.0", - "rehype-recma": "^1.0.0", - "remark-mdx": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "source-map": "^0.7.0", - "unified": "^11.0.0", - "unist-util-position-from-estree": "^2.0.0", - "unist-util-stringify-position": "^4.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@mdx-js/react": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", - "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", - "license": "MIT", - "dependencies": { - "@types/mdx": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" - } - }, - "node_modules/@mermaid-js/parser": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.0.tgz", - "integrity": "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==", - "license": "MIT", - "dependencies": { - "langium": "^4.0.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "license": "Apache-2.0", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@pnpm/config.env-replace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", - "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", - "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/@pnpm/network.ca-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", - "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", - "dependencies": { - "graceful-fs": "4.2.10" - }, - "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" - }, - "node_modules/@pnpm/npm-conf": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", - "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", - "dependencies": { - "@pnpm/config.env-replace": "^1.1.0", - "@pnpm/network.ca-file": "^1.0.1", - "config-chain": "^1.1.11" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.28", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", - "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==" - }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "license": "MIT" - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@slorber/remark-comment": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@slorber/remark-comment/-/remark-comment-1.0.0.tgz", - "integrity": "sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==", - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.1.0", - "micromark-util-symbol": "^1.0.1" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "license": "MIT" - }, - "node_modules/@svgr/babel-plugin-add-jsx-attribute": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", - "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", - "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", - "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", - "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-svg-dynamic-title": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", - "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-svg-em-dimensions": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", - "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-transform-react-native-svg": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", - "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-transform-svg-component": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", - "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-preset": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", - "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", - "license": "MIT", - "dependencies": { - "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", - "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", - "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", - "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", - "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", - "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", - "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", - "@svgr/babel-plugin-transform-svg-component": "8.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/core": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", - "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.21.3", - "@svgr/babel-preset": "8.1.0", - "camelcase": "^6.2.0", - "cosmiconfig": "^8.1.3", - "snake-case": "^3.0.4" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/hast-util-to-babel-ast": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", - "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.21.3", - "entities": "^4.4.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/plugin-jsx": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", - "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.21.3", - "@svgr/babel-preset": "8.1.0", - "@svgr/hast-util-to-babel-ast": "8.0.0", - "svg-parser": "^2.0.4" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@svgr/core": "*" - } - }, - "node_modules/@svgr/plugin-svgo": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", - "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", - "license": "MIT", - "dependencies": { - "cosmiconfig": "^8.1.3", - "deepmerge": "^4.3.1", - "svgo": "^3.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@svgr/core": "*" - } - }, - "node_modules/@svgr/webpack": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", - "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.21.3", - "@babel/plugin-transform-react-constant-elements": "^7.21.3", - "@babel/preset-env": "^7.20.2", - "@babel/preset-react": "^7.18.6", - "@babel/preset-typescript": "^7.21.0", - "@svgr/core": "8.1.0", - "@svgr/plugin-jsx": "8.1.0", - "@svgr/plugin-svgo": "8.1.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@types/acorn": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", - "integrity": "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", - "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", - "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", - "license": "MIT", - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "node_modules/@types/d3": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", - "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/d3-axis": "*", - "@types/d3-brush": "*", - "@types/d3-chord": "*", - "@types/d3-color": "*", - "@types/d3-contour": "*", - "@types/d3-delaunay": "*", - "@types/d3-dispatch": "*", - "@types/d3-drag": "*", - "@types/d3-dsv": "*", - "@types/d3-ease": "*", - "@types/d3-fetch": "*", - "@types/d3-force": "*", - "@types/d3-format": "*", - "@types/d3-geo": "*", - "@types/d3-hierarchy": "*", - "@types/d3-interpolate": "*", - "@types/d3-path": "*", - "@types/d3-polygon": "*", - "@types/d3-quadtree": "*", - "@types/d3-random": "*", - "@types/d3-scale": "*", - "@types/d3-scale-chromatic": "*", - "@types/d3-selection": "*", - "@types/d3-shape": "*", - "@types/d3-time": "*", - "@types/d3-time-format": "*", - "@types/d3-timer": "*", - "@types/d3-transition": "*", - "@types/d3-zoom": "*" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "license": "MIT" - }, - "node_modules/@types/d3-axis": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", - "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-brush": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", - "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-chord": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", - "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-contour": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", - "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", - "license": "MIT" - }, - "node_modules/@types/d3-dispatch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", - "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", - "license": "MIT" - }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-dsv": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", - "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-fetch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", - "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", - "license": "MIT", - "dependencies": { - "@types/d3-dsv": "*" - } - }, - "node_modules/@types/d3-force": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", - "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", - "license": "MIT" - }, - "node_modules/@types/d3-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", - "license": "MIT" - }, - "node_modules/@types/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-hierarchy": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-polygon": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", - "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", - "license": "MIT" - }, - "node_modules/@types/d3-quadtree": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", - "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", - "license": "MIT" - }, - "node_modules/@types/d3-random": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", - "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", - "license": "MIT" - }, - "node_modules/@types/d3-selection": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", - "license": "MIT" - }, - "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-time-format": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", - "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", - "license": "MIT", - "dependencies": { - "@types/d3-interpolate": "*", - "@types/d3-selection": "*" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", - "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, - "node_modules/@types/gtag.js": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz", - "integrity": "sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==", - "license": "MIT" - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/history": { - "version": "4.7.11", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", - "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==" - }, - "node_modules/@types/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==" - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "license": "MIT" - }, - "node_modules/@types/http-proxy": { - "version": "1.17.16", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", - "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/mdx": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", - "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" - }, - "node_modules/@types/node": { - "version": "22.13.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", - "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@types/node-forge": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", - "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/prismjs": { - "version": "1.26.5", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", - "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==" - }, - "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==" - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", - "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-router": { - "version": "5.1.20", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", - "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*" - } - }, - "node_modules/@types/react-router-config": { - "version": "5.0.11", - "resolved": "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.11.tgz", - "integrity": "sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==", - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "^5.1.0" - } - }, - "node_modules/@types/react-router-dom": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", - "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "*" - } - }, - "node_modules/@types/retry": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", - "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", - "license": "MIT" - }, - "node_modules/@types/sax": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", - "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-index": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", - "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/sockjs": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", - "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "license": "MIT" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" - }, - "node_modules/@vercel/oidc": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.3.tgz", - "integrity": "sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==", - "license": "Apache-2.0", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/address": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", - "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ai": { - "version": "5.0.95", - "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.95.tgz", - "integrity": "sha512-dsvFdYMeGP08zuUQkhKO1UMMXMCb+nro9ZmDdwaAkkTlCGkP3u1S+xaRUDNayu/c0KVkiTtfEroPG//U+kvXzg==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/gateway": "2.0.11", - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.17", - "@opentelemetry/api": "1.9.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/ai/node_modules/@ai-sdk/gateway": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.11.tgz", - "integrity": "sha512-B0Vt2Xv88Lo9rg861Oyzq/SdTmT4LiqdjkpOxpSPpNk8Ih5TXTgyDAsV/qW14N6asPdK1YI5PosFLnVzfK5LrA==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.17", - "@vercel/oidc": "3.0.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/algoliasearch": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.44.0.tgz", - "integrity": "sha512-f8IpsbdQjzTjr/4mJ/jv5UplrtyMnnciGax6/B0OnLCs2/GJTK13O4Y7Ff1AvJVAaztanH+m5nzPoUq6EAy+aA==", - "license": "MIT", - "dependencies": { - "@algolia/abtesting": "1.10.0", - "@algolia/client-abtesting": "5.44.0", - "@algolia/client-analytics": "5.44.0", - "@algolia/client-common": "5.44.0", - "@algolia/client-insights": "5.44.0", - "@algolia/client-personalization": "5.44.0", - "@algolia/client-query-suggestions": "5.44.0", - "@algolia/client-search": "5.44.0", - "@algolia/ingestion": "1.44.0", - "@algolia/monitoring": "1.44.0", - "@algolia/recommend": "5.44.0", - "@algolia/requester-browser-xhr": "5.44.0", - "@algolia/requester-fetch": "5.44.0", - "@algolia/requester-node-http": "5.44.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/algoliasearch-helper": { - "version": "3.26.1", - "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.26.1.tgz", - "integrity": "sha512-CAlCxm4fYBXtvc5MamDzP6Svu8rW4z9me4DCBY1rQ2UDJ0u0flWmusQ8M3nOExZsLLRcUwUPoRAPMrhzOG3erw==", - "license": "MIT", - "dependencies": { - "@algolia/events": "^4.0.1" - }, - "peerDependencies": { - "algoliasearch": ">= 3.1 < 6" - } - }, - "node_modules/ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "dependencies": { - "string-width": "^4.1.0" - } - }, - "node_modules/ansi-align/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/ansi-align/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/astring": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", - "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", - "bin": { - "astring": "bin/astring" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.22", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", - "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.27.0", - "caniuse-lite": "^1.0.30001754", - "fraction.js": "^5.3.4", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/babel-loader": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", - "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", - "license": "MIT", - "dependencies": { - "find-cache-dir": "^4.0.0", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0", - "webpack": ">=5" - } - }, - "node_modules/babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "license": "MIT", - "dependencies": { - "object.assign": "^4.1.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", - "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.7", - "@babel/helper-define-polyfill-provider": "^0.6.5", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", - "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5", - "core-js-compat": "^3.43.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", - "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "license": "MIT" - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/body-parser/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/bonjour-service": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", - "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" - }, - "node_modules/boxen": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-6.2.1.tgz", - "integrity": "sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==", - "dependencies": { - "ansi-align": "^3.0.1", - "camelcase": "^6.2.0", - "chalk": "^4.1.2", - "cli-boxes": "^3.0.0", - "string-width": "^5.0.1", - "type-fest": "^2.5.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.0.1" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/cacheable-request": { - "version": "10.2.14", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", - "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", - "dependencies": { - "@types/http-cache-semantics": "^4.0.2", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001770", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", - "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/cheerio": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", - "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", - "license": "MIT", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "htmlparser2": "^8.0.1", - "parse5": "^7.0.0", - "parse5-htmlparser2-tree-adapter": "^7.0.0" - }, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/chevrotain": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz", - "integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/cst-dts-gen": "11.1.1", - "@chevrotain/gast": "11.1.1", - "@chevrotain/regexp-to-ast": "11.1.1", - "@chevrotain/types": "11.1.1", - "@chevrotain/utils": "11.1.1", - "lodash-es": "4.17.23" - } - }, - "node_modules/chevrotain-allstar": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", - "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", - "license": "MIT", - "dependencies": { - "lodash-es": "^4.17.21" - }, - "peerDependencies": { - "chevrotain": "^11.0.0" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, - "node_modules/clean-css": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", - "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, - "node_modules/clean-css/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-boxes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-table3": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", - "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, - "node_modules/cli-table3/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/cli-table3/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/collapse-white-space": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", - "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "license": "MIT" - }, - "node_modules/combine-promises": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/combine-promises/-/combine-promises-1.2.0.tgz", - "integrity": "sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/common-path-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "license": "ISC" - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compressible/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "license": "MIT" - }, - "node_modules/config-chain": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "node_modules/configstore": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", - "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==", - "dependencies": { - "dot-prop": "^6.0.1", - "graceful-fs": "^4.2.6", - "unique-string": "^3.0.0", - "write-file-atomic": "^3.0.3", - "xdg-basedir": "^5.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/yeoman/configstore?sponsor=1" - } - }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", - "license": "MIT", - "dependencies": { - "fast-glob": "^3.2.11", - "glob-parent": "^6.0.1", - "globby": "^13.1.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", - "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", - "license": "MIT", - "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.3.0", - "ignore": "^5.2.4", - "merge2": "^1.4.1", - "slash": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/copy-webpack-plugin/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/core-js": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz", - "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", - "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-pure": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz", - "integrity": "sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/cose-base": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", - "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", - "license": "MIT", - "dependencies": { - "layout-base": "^1.0.0" - } - }, - "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "license": "MIT", - "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-random-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", - "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", - "dependencies": { - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/crypto-random-string/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/css-blank-pseudo": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz", - "integrity": "sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/css-declaration-sorter": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.0.tgz", - "integrity": "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==", - "license": "ISC", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/css-has-pseudo": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.3.tgz", - "integrity": "sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/selector-specificity": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-has-pseudo/node_modules/@csstools/selector-specificity": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", - "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^7.0.0" - } - }, - "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/css-loader": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", - "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-minimizer-webpack-plugin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz", - "integrity": "sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "cssnano": "^6.0.1", - "jest-worker": "^29.4.3", - "postcss": "^8.4.24", - "schema-utils": "^4.0.1", - "serialize-javascript": "^6.0.1" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@parcel/css": { - "optional": true - }, - "@swc/css": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "csso": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "lightningcss": { - "optional": true - } - } - }, - "node_modules/css-prefers-color-scheme": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz", - "integrity": "sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssdb": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.4.2.tgz", - "integrity": "sha512-PzjkRkRUS+IHDJohtxkIczlxPPZqRo0nXplsYXOMBRPjcVRjj1W4DfvRgshUYTVuUigU7ptVYkFJQ7abUB0nyg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - } - ], - "license": "MIT-0" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", - "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", - "license": "MIT", - "dependencies": { - "cssnano-preset-default": "^6.1.2", - "lilconfig": "^3.1.1" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/cssnano-preset-advanced": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", - "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", - "license": "MIT", - "dependencies": { - "autoprefixer": "^10.4.19", - "browserslist": "^4.23.0", - "cssnano-preset-default": "^6.1.2", - "postcss-discard-unused": "^6.0.5", - "postcss-merge-idents": "^6.0.3", - "postcss-reduce-idents": "^6.0.3", - "postcss-zindex": "^6.0.2" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/cssnano-preset-default": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", - "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^4.0.2", - "postcss-calc": "^9.0.1", - "postcss-colormin": "^6.1.0", - "postcss-convert-values": "^6.1.0", - "postcss-discard-comments": "^6.0.2", - "postcss-discard-duplicates": "^6.0.3", - "postcss-discard-empty": "^6.0.3", - "postcss-discard-overridden": "^6.0.2", - "postcss-merge-longhand": "^6.0.5", - "postcss-merge-rules": "^6.1.1", - "postcss-minify-font-values": "^6.1.0", - "postcss-minify-gradients": "^6.0.3", - "postcss-minify-params": "^6.1.0", - "postcss-minify-selectors": "^6.0.4", - "postcss-normalize-charset": "^6.0.2", - "postcss-normalize-display-values": "^6.0.2", - "postcss-normalize-positions": "^6.0.2", - "postcss-normalize-repeat-style": "^6.0.2", - "postcss-normalize-string": "^6.0.2", - "postcss-normalize-timing-functions": "^6.0.2", - "postcss-normalize-unicode": "^6.1.0", - "postcss-normalize-url": "^6.0.2", - "postcss-normalize-whitespace": "^6.0.2", - "postcss-ordered-values": "^6.0.2", - "postcss-reduce-initial": "^6.1.0", - "postcss-reduce-transforms": "^6.0.2", - "postcss-svgo": "^6.0.3", - "postcss-unique-selectors": "^6.0.4" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/cssnano-utils": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", - "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "license": "MIT", - "dependencies": { - "css-tree": "~2.2.0" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "license": "CC0-1.0" - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - }, - "node_modules/cytoscape": { - "version": "3.33.1", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", - "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/cytoscape-cose-bilkent": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", - "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", - "license": "MIT", - "dependencies": { - "cose-base": "^1.0.0" - }, - "peerDependencies": { - "cytoscape": "^3.2.0" - } - }, - "node_modules/cytoscape-fcose": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", - "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", - "license": "MIT", - "dependencies": { - "cose-base": "^2.2.0" - }, - "peerDependencies": { - "cytoscape": "^3.2.0" - } - }, - "node_modules/cytoscape-fcose/node_modules/cose-base": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", - "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", - "license": "MIT", - "dependencies": { - "layout-base": "^2.0.0" - } - }, - "node_modules/cytoscape-fcose/node_modules/layout-base": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", - "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", - "license": "MIT" - }, - "node_modules/d3": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", - "license": "ISC", - "dependencies": { - "d3-array": "3", - "d3-axis": "3", - "d3-brush": "3", - "d3-chord": "3", - "d3-color": "3", - "d3-contour": "4", - "d3-delaunay": "6", - "d3-dispatch": "3", - "d3-drag": "3", - "d3-dsv": "3", - "d3-ease": "3", - "d3-fetch": "3", - "d3-force": "3", - "d3-format": "3", - "d3-geo": "3", - "d3-hierarchy": "3", - "d3-interpolate": "3", - "d3-path": "3", - "d3-polygon": "3", - "d3-quadtree": "3", - "d3-random": "3", - "d3-scale": "4", - "d3-scale-chromatic": "3", - "d3-selection": "3", - "d3-shape": "3", - "d3-time": "3", - "d3-time-format": "4", - "d3-timer": "3", - "d3-transition": "3", - "d3-zoom": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-axis": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-brush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "3", - "d3-transition": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-chord": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "license": "ISC", - "dependencies": { - "d3-path": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-contour": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", - "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", - "license": "ISC", - "dependencies": { - "d3-array": "^3.2.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "license": "ISC", - "dependencies": { - "delaunator": "5" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "license": "ISC", - "dependencies": { - "commander": "7", - "iconv-lite": "0.6", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json.js", - "csv2tsv": "bin/dsv2dsv.js", - "dsv2dsv": "bin/dsv2dsv.js", - "dsv2json": "bin/dsv2json.js", - "json2csv": "bin/json2dsv.js", - "json2dsv": "bin/json2dsv.js", - "json2tsv": "bin/json2dsv.js", - "tsv2csv": "bin/dsv2dsv.js", - "tsv2json": "bin/dsv2json.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-fetch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "license": "ISC", - "dependencies": { - "d3-dsv": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-force": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-polygon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-random": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-sankey": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", - "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "1 - 2", - "d3-shape": "^1.2.0" - } - }, - "node_modules/d3-sankey/node_modules/d3-array": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", - "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", - "license": "BSD-3-Clause", - "dependencies": { - "internmap": "^1.0.0" - } - }, - "node_modules/d3-sankey/node_modules/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", - "license": "BSD-3-Clause" - }, - "node_modules/d3-sankey/node_modules/d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-path": "1" - } - }, - "node_modules/d3-sankey/node_modules/internmap": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", - "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", - "license": "ISC" - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dagre-d3-es": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", - "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", - "license": "MIT", - "dependencies": { - "d3": "^7.9.0", - "lodash-es": "^4.17.21" - } - }, - "node_modules/dayjs": { - "version": "1.11.18", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", - "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", - "license": "MIT" - }, - "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decode-named-character-reference": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", - "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", - "license": "MIT", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "license": "MIT" - }, - "node_modules/detect-port": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz", - "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", - "dependencies": { - "address": "^1.0.1", - "debug": "4" - }, - "bin": { - "detect": "bin/detect-port.js", - "detect-port": "bin/detect-port.js" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", - "license": "MIT", - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "dependencies": { - "utila": "~0.4" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dot-prop/node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "node_modules/emojilib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", - "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", - "license": "MIT" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/emoticon": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz", - "integrity": "sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", - "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esast-util-from-estree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", - "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-visit": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/esast-util-from-js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", - "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "acorn": "^8.0.0", - "esast-util-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-goat": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", - "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-util-attach-comments": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", - "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", - "dependencies": { - "@types/estree": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-build-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", - "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-walker": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-scope": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", - "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-to-js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", - "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "astring": "^1.8.0", - "source-map": "^0.7.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-value-to-estree": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz", - "integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/remcohaszing" - } - }, - "node_modules/estree-util-visit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", - "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eta": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz", - "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==", - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "url": "https://github.com/eta-dev/eta?sponsor=1" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eval": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", - "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", - "dependencies": { - "@types/node": "*", - "require-like": ">= 0.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/express/node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", - "license": "MIT" - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ] - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fault": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", - "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", - "license": "MIT", - "dependencies": { - "format": "^0.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/feed": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", - "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", - "license": "MIT", - "dependencies": { - "xml-js": "^1.6.11" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/file-loader/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/file-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/file-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/find-cache-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", - "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", - "license": "MIT", - "dependencies": { - "common-path-prefix": "^3.0.0", - "pkg-dir": "^7.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "license": "MIT", - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", - "engines": { - "node": ">= 14.17" - } - }, - "node_modules/format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-own-enumerable-property-symbols": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", - "license": "ISC" - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/github-slugger": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", - "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", - "license": "ISC" - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob-to-regex.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.0.1.tgz", - "integrity": "sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg==", - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" - }, - "node_modules/global-dirs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", - "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/global-dirs/node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", - "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", - "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/got/node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/gray-matter": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", - "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", - "license": "MIT", - "dependencies": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/gray-matter/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/hachure-fill": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", - "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", - "license": "MIT" - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-yarn": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", - "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-util-from-parse5": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", - "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "hastscript": "^9.0.0", - "property-information": "^7.0.0", - "vfile": "^6.0.0", - "vfile-location": "^5.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-parse5/node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-raw": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", - "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "hast-util-from-parse5": "^8.0.0", - "hast-util-to-parse5": "^8.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "parse5": "^7.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-estree": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.1.tgz", - "integrity": "sha512-IWtwwmPskfSmma9RpzCappDUitC8t5jhAynHhc1m2+5trOgsrp7txscUSavc5Ic8PATyAjfrCK1wgtxh2cICVQ==", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-attach-comments": "^3.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^1.0.0", - "unist-util-position": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz", - "integrity": "sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", - "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", - "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript/node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "bin": { - "he": "bin/he" - } - }, - "node_modules/history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", - "dependencies": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" - }, - "node_modules/html-minifier-terser": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", - "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", - "license": "MIT", - "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "~5.3.2", - "commander": "^10.0.0", - "entities": "^4.4.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.15.1" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": "^14.13.1 || >=16.0.0" - } - }, - "node_modules/html-minifier-terser/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/html-tags": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", - "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/html-webpack-plugin": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz", - "integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==", - "dependencies": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.20.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/html-webpack-plugin/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "engines": { - "node": ">= 12" - } - }, - "node_modules/html-webpack-plugin/node_modules/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", - "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "^5.2.2", - "commander": "^8.3.0", - "he": "^1.2.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.10.0" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "license": "MIT" - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", - "license": "MIT", - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/http2-wrapper": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", - "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/hyperdyperid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", - "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", - "license": "MIT", - "engines": { - "node": ">=10.18" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/image-size": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", - "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", - "license": "MIT", - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-lazy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/infima": { - "version": "0.2.0-alpha.45", - "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.45.tgz", - "integrity": "sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/inline-style-parser": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", - "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-inside-container/node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", - "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-network-error": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", - "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-npm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz", - "integrity": "sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-yarn-global": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", - "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", - "engines": { - "node": ">=12" - } - }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/joi": { - "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/katex": { - "version": "0.16.22", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", - "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "license": "MIT", - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/katex/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/khroma": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", - "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/kolorist": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", - "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", - "license": "MIT" - }, - "node_modules/langium": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.1.tgz", - "integrity": "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==", - "license": "MIT", - "dependencies": { - "chevrotain": "~11.1.1", - "chevrotain-allstar": "~0.3.1", - "vscode-languageserver": "~9.0.1", - "vscode-languageserver-textdocument": "~1.0.11", - "vscode-uri": "~3.1.0" - }, - "engines": { - "node": ">=20.10.0", - "npm": ">=10.2.3" - } - }, - "node_modules/latest-version": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", - "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", - "dependencies": { - "package-json": "^8.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/launch-editor": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz", - "integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==", - "license": "MIT", - "dependencies": { - "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" - } - }, - "node_modules/layout-base": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", - "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", - "license": "MIT" - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "license": "MIT", - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/local-pkg": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", - "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", - "license": "MIT", - "dependencies": { - "mlly": "^1.7.4", - "pkg-types": "^2.3.0", - "quansync": "^0.2.11" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "license": "MIT", - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "license": "MIT" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "license": "MIT" - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/markdown-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", - "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdown-table": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/marked": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-16.3.0.tgz", - "integrity": "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdast-util-directive": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", - "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", - "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/mdast-util-frontmatter": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", - "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "escape-string-regexp": "^5.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-extension-frontmatter": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdast-util-gfm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", - "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", - "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", - "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "license": "CC0-1.0" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "4.46.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.46.1.tgz", - "integrity": "sha512-2wjHDg7IjP+ufAqqqSxjiNePFDrvWviA79ajUwG9lkHhk3HzZOLBzzoUG8cx9vCagj6VvBQD7oXuLuFz5LaAOQ==", - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/json-pack": "^1.11.0", - "@jsonjoy.com/util": "^1.9.0", - "glob-to-regex.js": "^1.0.1", - "thingies": "^2.5.0", - "tree-dump": "^1.0.3", - "tslib": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/mermaid": { - "version": "11.12.3", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.3.tgz", - "integrity": "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==", - "license": "MIT", - "dependencies": { - "@braintree/sanitize-url": "^7.1.1", - "@iconify/utils": "^3.0.1", - "@mermaid-js/parser": "^1.0.0", - "@types/d3": "^7.4.3", - "cytoscape": "^3.29.3", - "cytoscape-cose-bilkent": "^4.1.0", - "cytoscape-fcose": "^2.2.0", - "d3": "^7.9.0", - "d3-sankey": "^0.12.3", - "dagre-d3-es": "7.0.13", - "dayjs": "^1.11.18", - "dompurify": "^3.2.5", - "katex": "^0.16.22", - "khroma": "^2.1.0", - "lodash-es": "^4.17.23", - "marked": "^16.2.1", - "roughjs": "^4.6.6", - "stylis": "^4.3.6", - "ts-dedent": "^2.2.0", - "uuid": "^11.1.0" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromark": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.1.tgz", - "integrity": "sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.2.tgz", - "integrity": "sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-extension-directive": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", - "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "parse-entities": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-directive/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-directive/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-directive/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-frontmatter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", - "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", - "license": "MIT", - "dependencies": { - "fault": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", - "license": "MIT", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", - "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-mdx-expression": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.0.tgz", - "integrity": "sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-expression/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-extension-mdx-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.1.tgz", - "integrity": "sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg==", - "dependencies": { - "@types/acorn": "^4.0.0", - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-extension-mdx-md": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", - "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", - "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", - "dependencies": { - "acorn": "^8.0.0", - "acorn-jsx": "^5.0.0", - "micromark-extension-mdx-expression": "^3.0.0", - "micromark-extension-mdx-jsx": "^3.0.0", - "micromark-extension-mdx-md": "^2.0.0", - "micromark-extension-mdxjs-esm": "^3.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs-esm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", - "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-destination/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-destination/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-factory-mdx-expression": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.2.tgz", - "integrity": "sha512-5E5I2pFzJyg2CtemqAbcyCktpHXuJbABnsb32wX2U8IQKhhVFBqkcZR5LRm1WVoFqa4kTueZK4abep7wdo9nrw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - } - }, - "node_modules/micromark-factory-mdx-expression/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-factory-space": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", - "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-space/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-character/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-events-to-acorn": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.2.tgz", - "integrity": "sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/acorn": "^4.0.0", - "@types/estree": "^1.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "estree-util-visit": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" - } - }, - "node_modules/micromark-util-events-to-acorn/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-normalize-identifier/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.4.tgz", - "integrity": "sha512-N6hXjrin2GTJDe3MVjf5FuXpm12PGm80BrUAeub9XFXca8JZbP+oIwY4LJSVwFUCL1IPm/WwSVUN7goFHmSGGQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz", - "integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "dependencies": { - "mime-db": "~1.33.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mini-css-extract-plugin": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", - "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==", - "license": "MIT", - "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "license": "ISC" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", - "license": "MIT", - "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" - } - }, - "node_modules/mlly/node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "license": "MIT" - }, - "node_modules/mlly/node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "license": "MIT", - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-emoji": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", - "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.6.0", - "char-regex": "^1.0.2", - "emojilib": "^2.4.0", - "skin-tone": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/node-forge": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", - "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", - "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nprogress": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", - "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", - "license": "MIT" - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/null-loader": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz", - "integrity": "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==", - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/null-loader/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/null-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/null-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/null-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "license": "MIT" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "bin": { - "opener": "bin/opener-bin.js" - } - }, - "node_modules/p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "license": "MIT", - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-queue": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", - "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-retry": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", - "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.2", - "is-network-error": "^1.0.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "license": "MIT", - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/package-json": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", - "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", - "dependencies": { - "got": "^12.1.0", - "registry-auth-token": "^5.0.1", - "registry-url": "^6.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-manager-detector": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", - "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", - "license": "MIT" - }, - "node_modules/param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-numeric-range": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", - "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", - "license": "ISC" - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", - "license": "MIT", - "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/path-data-parser": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", - "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", - "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", - "dependencies": { - "isarray": "0.0.1" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkg-dir": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", - "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", - "license": "MIT", - "dependencies": { - "find-up": "^6.3.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" - } - }, - "node_modules/points-on-curve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", - "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", - "license": "MIT" - }, - "node_modules/points-on-path": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", - "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", - "license": "MIT", - "dependencies": { - "path-data-parser": "0.1.0", - "points-on-curve": "0.2.0" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-attribute-case-insensitive": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz", - "integrity": "sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-calc": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", - "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.11", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.2" - } - }, - "node_modules/postcss-clamp": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", - "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=7.6.0" - }, - "peerDependencies": { - "postcss": "^8.4.6" - } - }, - "node_modules/postcss-color-functional-notation": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.12.tgz", - "integrity": "sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-color-hex-alpha": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz", - "integrity": "sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-color-rebeccapurple": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz", - "integrity": "sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-colormin": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", - "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0", - "colord": "^2.9.3", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-convert-values": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", - "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-custom-media": { - "version": "11.0.6", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz", - "integrity": "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.5", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/media-query-list-parser": "^4.0.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-custom-properties": { - "version": "14.0.6", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz", - "integrity": "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.5", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-custom-selectors": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz", - "integrity": "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.5", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-dir-pseudo-class": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz", - "integrity": "sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-discard-comments": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", - "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", - "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-empty": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", - "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", - "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-unused": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz", - "integrity": "sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-double-position-gradients": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.4.tgz", - "integrity": "sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-focus-visible": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz", - "integrity": "sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-focus-within": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz", - "integrity": "sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-font-variant": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", - "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", - "license": "MIT", - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-gap-properties": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz", - "integrity": "sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-image-set-function": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz", - "integrity": "sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-lab-function": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.12.tgz", - "integrity": "sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-loader": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", - "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", - "license": "MIT", - "dependencies": { - "cosmiconfig": "^8.3.5", - "jiti": "^1.20.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - } - }, - "node_modules/postcss-logical": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.1.0.tgz", - "integrity": "sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-merge-idents": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz", - "integrity": "sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g==", - "license": "MIT", - "dependencies": { - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", - "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^6.1.1" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-merge-rules": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", - "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^4.0.2", - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", - "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-gradients": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", - "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", - "license": "MIT", - "dependencies": { - "colord": "^2.9.3", - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-params": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", - "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-selectors": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", - "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", - "license": "MIT", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "license": "ISC", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-nesting": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", - "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/selector-resolve-nested": "^3.1.0", - "@csstools/selector-specificity": "^5.0.0", - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", - "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^7.0.0" - } - }, - "node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", - "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^7.0.0" - } - }, - "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", - "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", - "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-positions": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", - "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", - "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-string": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", - "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", - "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-unicode": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", - "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-url": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", - "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-whitespace": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", - "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-opacity-percentage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", - "integrity": "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==", - "funding": [ - { - "type": "kofi", - "url": "https://ko-fi.com/mrcgrtz" - }, - { - "type": "liberapay", - "url": "https://liberapay.com/mrcgrtz" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-ordered-values": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", - "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", - "license": "MIT", - "dependencies": { - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-overflow-shorthand": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz", - "integrity": "sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-page-break": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", - "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", - "license": "MIT", - "peerDependencies": { - "postcss": "^8" - } - }, - "node_modules/postcss-place": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-10.0.0.tgz", - "integrity": "sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-preset-env": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.4.0.tgz", - "integrity": "sha512-2kqpOthQ6JhxqQq1FSAAZGe9COQv75Aw8WbsOvQVNJ2nSevc9Yx/IKZGuZ7XJ+iOTtVon7LfO7ELRzg8AZ+sdw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/postcss-alpha-function": "^1.0.1", - "@csstools/postcss-cascade-layers": "^5.0.2", - "@csstools/postcss-color-function": "^4.0.12", - "@csstools/postcss-color-function-display-p3-linear": "^1.0.1", - "@csstools/postcss-color-mix-function": "^3.0.12", - "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.2", - "@csstools/postcss-content-alt-text": "^2.0.8", - "@csstools/postcss-contrast-color-function": "^2.0.12", - "@csstools/postcss-exponential-functions": "^2.0.9", - "@csstools/postcss-font-format-keywords": "^4.0.0", - "@csstools/postcss-gamut-mapping": "^2.0.11", - "@csstools/postcss-gradients-interpolation-method": "^5.0.12", - "@csstools/postcss-hwb-function": "^4.0.12", - "@csstools/postcss-ic-unit": "^4.0.4", - "@csstools/postcss-initial": "^2.0.1", - "@csstools/postcss-is-pseudo-class": "^5.0.3", - "@csstools/postcss-light-dark-function": "^2.0.11", - "@csstools/postcss-logical-float-and-clear": "^3.0.0", - "@csstools/postcss-logical-overflow": "^2.0.0", - "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", - "@csstools/postcss-logical-resize": "^3.0.0", - "@csstools/postcss-logical-viewport-units": "^3.0.4", - "@csstools/postcss-media-minmax": "^2.0.9", - "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", - "@csstools/postcss-nested-calc": "^4.0.0", - "@csstools/postcss-normalize-display-values": "^4.0.0", - "@csstools/postcss-oklab-function": "^4.0.12", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/postcss-random-function": "^2.0.1", - "@csstools/postcss-relative-color-syntax": "^3.0.12", - "@csstools/postcss-scope-pseudo-class": "^4.0.1", - "@csstools/postcss-sign-functions": "^1.1.4", - "@csstools/postcss-stepped-value-functions": "^4.0.9", - "@csstools/postcss-text-decoration-shorthand": "^4.0.3", - "@csstools/postcss-trigonometric-functions": "^4.0.9", - "@csstools/postcss-unset-value": "^4.0.0", - "autoprefixer": "^10.4.21", - "browserslist": "^4.26.0", - "css-blank-pseudo": "^7.0.1", - "css-has-pseudo": "^7.0.3", - "css-prefers-color-scheme": "^10.0.0", - "cssdb": "^8.4.2", - "postcss-attribute-case-insensitive": "^7.0.1", - "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^7.0.12", - "postcss-color-hex-alpha": "^10.0.0", - "postcss-color-rebeccapurple": "^10.0.0", - "postcss-custom-media": "^11.0.6", - "postcss-custom-properties": "^14.0.6", - "postcss-custom-selectors": "^8.0.5", - "postcss-dir-pseudo-class": "^9.0.1", - "postcss-double-position-gradients": "^6.0.4", - "postcss-focus-visible": "^10.0.1", - "postcss-focus-within": "^9.0.1", - "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^6.0.0", - "postcss-image-set-function": "^7.0.0", - "postcss-lab-function": "^7.0.12", - "postcss-logical": "^8.1.0", - "postcss-nesting": "^13.0.2", - "postcss-opacity-percentage": "^3.0.0", - "postcss-overflow-shorthand": "^6.0.0", - "postcss-page-break": "^3.0.4", - "postcss-place": "^10.0.0", - "postcss-pseudo-class-any-link": "^10.0.1", - "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^8.0.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-pseudo-class-any-link": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz", - "integrity": "sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-reduce-idents": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz", - "integrity": "sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-reduce-initial": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", - "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", - "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-replace-overflow-wrap": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", - "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", - "license": "MIT", - "peerDependencies": { - "postcss": "^8.0.3" - } - }, - "node_modules/postcss-selector-not": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz", - "integrity": "sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-sort-media-queries": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz", - "integrity": "sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA==", - "license": "MIT", - "dependencies": { - "sort-css-media-queries": "2.2.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.4.23" - } - }, - "node_modules/postcss-svgo": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", - "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^3.2.0" - }, - "engines": { - "node": "^14 || ^16 || >= 18" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-unique-selectors": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", - "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT" - }, - "node_modules/postcss-zindex": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-6.0.2.tgz", - "integrity": "sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/pretty-error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", - "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^3.0.0" - } - }, - "node_modules/pretty-time": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", - "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/prism-react-renderer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", - "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", - "dependencies": { - "@types/prismjs": "^1.26.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.0.0" - } - }, - "node_modules/prismjs": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", - "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pupa": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz", - "integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==", - "dependencies": { - "escape-goat": "^4.0.0" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/quansync": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", - "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/antfu" - }, - { - "type": "individual", - "url": "https://github.com/sponsors/sxzz" - } - ], - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/raw-body/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.3" - } - }, - "node_modules/react-fast-compare": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" - }, - "node_modules/react-helmet-async": { - "name": "@slorber/react-helmet-async", - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz", - "integrity": "sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "invariant": "^2.2.4", - "prop-types": "^15.7.2", - "react-fast-compare": "^3.2.0", - "shallowequal": "^1.1.0" - }, - "peerDependencies": { - "react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/react-json-view-lite": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz", - "integrity": "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-loadable": { - "name": "@docusaurus/react-loadable", - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", - "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", - "dependencies": { - "@types/react": "*" - }, - "peerDependencies": { - "react": "*" - } - }, - "node_modules/react-loadable-ssr-addon-v5-slorber": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz", - "integrity": "sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==", - "dependencies": { - "@babel/runtime": "^7.10.3" - }, - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "react-loadable": "*", - "webpack": ">=4.41.1 || 5.x" - } - }, - "node_modules/react-router": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", - "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", - "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" - }, - "peerDependencies": { - "react": ">=15" - } - }, - "node_modules/react-router-config": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/react-router-config/-/react-router-config-5.1.1.tgz", - "integrity": "sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==", - "dependencies": { - "@babel/runtime": "^7.1.2" - }, - "peerDependencies": { - "react": ">=15", - "react-router": ">=5" - } - }, - "node_modules/react-router-dom": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", - "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", - "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.3.4", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" - }, - "peerDependencies": { - "react": ">=15" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/recma-build-jsx": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", - "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-util-build-jsx": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/recma-jsx": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.0.tgz", - "integrity": "sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==", - "dependencies": { - "acorn-jsx": "^5.0.0", - "estree-util-to-js": "^2.0.0", - "recma-parse": "^1.0.0", - "recma-stringify": "^1.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/recma-parse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", - "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", - "dependencies": { - "@types/estree": "^1.0.0", - "esast-util-from-js": "^2.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/recma-stringify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", - "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-util-to-js": "^2.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", - "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regexpu-core": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", - "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.2", - "regjsgen": "^0.8.0", - "regjsparser": "^0.13.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.2.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/registry-auth-token": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz", - "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", - "dependencies": { - "@pnpm/npm-conf": "^2.1.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/registry-url": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", - "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", - "dependencies": { - "rc": "1.2.8" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "license": "MIT" - }, - "node_modules/regjsparser": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", - "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~3.1.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/rehype-raw": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", - "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-raw": "^9.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-recma": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", - "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "hast-util-to-estree": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/remark-directive": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz", - "integrity": "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-directive": "^3.0.0", - "micromark-extension-directive": "^3.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-emoji": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-4.0.1.tgz", - "integrity": "sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.2", - "emoticon": "^4.0.1", - "mdast-util-find-and-replace": "^3.0.1", - "node-emoji": "^2.1.0", - "unified": "^11.0.4" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/remark-frontmatter": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", - "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-frontmatter": "^2.0.0", - "micromark-extension-frontmatter": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-gfm": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", - "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-mdx": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.0.tgz", - "integrity": "sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==", - "dependencies": { - "mdast-util-mdx": "^3.0.0", - "micromark-extension-mdxjs": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", - "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/renderkid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", - "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", - "dependencies": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^6.0.1" - } - }, - "node_modules/renderkid/node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/renderkid/node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-like": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", - "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==", - "engines": { - "node": "*" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pathname": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" - }, - "node_modules/responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", - "dependencies": { - "lowercase-keys": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", - "license": "Unlicense" - }, - "node_modules/roughjs": { - "version": "4.6.6", - "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", - "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", - "license": "MIT", - "dependencies": { - "hachure-fill": "^0.5.2", - "path-data-parser": "^0.1.0", - "points-on-curve": "^0.2.0", - "points-on-path": "^0.2.1" - } - }, - "node_modules/rtlcss": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", - "integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==", - "license": "MIT", - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0", - "postcss": "^8.4.21", - "strip-json-comments": "^3.1.1" - }, - "bin": { - "rtlcss": "bin/rtlcss.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/sax": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", - "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", - "license": "BlueOak-1.0.0" - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/schema-dts": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", - "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", - "license": "Apache-2.0" - }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/search-insights": { - "version": "2.17.3", - "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", - "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", - "license": "MIT", - "peer": true - }, - "node_modules/section-matter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", - "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "license": "MIT" - }, - "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", - "license": "MIT", - "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", - "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-handler": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", - "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", - "dependencies": { - "bytes": "3.0.0", - "content-disposition": "0.5.2", - "mime-types": "2.1.18", - "minimatch": "3.1.2", - "path-is-inside": "1.0.2", - "path-to-regexp": "3.3.0", - "range-parser": "1.2.0" - } - }, - "node_modules/serve-handler/node_modules/path-to-regexp": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", - "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==" - }, - "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "license": "ISC" - }, - "node_modules/serve-index/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "license": "ISC" - }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "license": "MIT" - }, - "node_modules/sitemap": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.2.tgz", - "integrity": "sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw==", - "license": "MIT", - "dependencies": { - "@types/node": "^17.0.5", - "@types/sax": "^1.2.1", - "arg": "^5.0.0", - "sax": "^1.2.4" - }, - "bin": { - "sitemap": "dist/cli.js" - }, - "engines": { - "node": ">=12.0.0", - "npm": ">=5.6.0" - } - }, - "node_modules/sitemap/node_modules/@types/node": { - "version": "17.0.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", - "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", - "license": "MIT" - }, - "node_modules/skin-tone": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", - "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", - "license": "MIT", - "dependencies": { - "unicode-emoji-modifier-base": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/snake-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", - "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "license": "MIT", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "license": "MIT", - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, - "node_modules/sockjs/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/sort-css-media-queries": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz", - "integrity": "sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA==", - "license": "MIT", - "engines": { - "node": ">= 6.3.0" - } - }, - "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, - "node_modules/srcset": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", - "integrity": "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/stringify-object": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", - "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", - "license": "BSD-2-Clause", - "dependencies": { - "get-own-enumerable-property-symbols": "^3.0.0", - "is-obj": "^1.0.1", - "is-regexp": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-to-object": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", - "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", - "dependencies": { - "inline-style-parser": "0.2.4" - } - }, - "node_modules/stylehacks": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", - "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/stylis": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", - "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", - "license": "MIT" - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svg-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", - "license": "MIT" - }, - "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", - "license": "MIT", - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.3.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/swr": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", - "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/terser-webpack-plugin/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/thingies": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", - "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", - "license": "MIT", - "engines": { - "node": ">=10.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "^2" - } - }, - "node_modules/throttleit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", - "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "license": "MIT" - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" - }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" - }, - "node_modules/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", - "license": "MIT" - }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/tree-dump": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", - "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-dedent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", - "license": "MIT", - "engines": { - "node": ">=6.10" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-emoji-modifier-base": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", - "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", - "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", - "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unique-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", - "dependencies": { - "crypto-random-string": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position-from-estree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", - "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/update-notifier": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz", - "integrity": "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==", - "dependencies": { - "boxen": "^7.0.0", - "chalk": "^5.0.1", - "configstore": "^6.0.0", - "has-yarn": "^3.0.0", - "import-lazy": "^4.0.0", - "is-ci": "^3.0.1", - "is-installed-globally": "^0.4.0", - "is-npm": "^6.0.0", - "is-yarn-global": "^0.4.0", - "latest-version": "^7.0.0", - "pupa": "^3.1.0", - "semver": "^7.3.7", - "semver-diff": "^4.0.0", - "xdg-basedir": "^5.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/yeoman/update-notifier?sponsor=1" - } - }, - "node_modules/update-notifier/node_modules/boxen": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", - "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", - "dependencies": { - "ansi-align": "^3.0.1", - "camelcase": "^7.0.1", - "chalk": "^5.2.0", - "cli-boxes": "^3.0.0", - "string-width": "^5.1.2", - "type-fest": "^2.13.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/update-notifier/node_modules/camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/update-notifier/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url-loader": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", - "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "mime-types": "^2.1.27", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "file-loader": "*", - "webpack": "^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "file-loader": { - "optional": true - } - } - }, - "node_modules/url-loader/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/url-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/url-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/url-loader/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/url-loader/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/url-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" - }, - "node_modules/utility-types": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", - "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/value-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", - "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", - "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vscode-languageserver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", - "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", - "license": "MIT", - "dependencies": { - "vscode-languageserver-protocol": "3.17.5" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", - "license": "MIT" - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "license": "MIT" - }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "license": "MIT" - }, - "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "license": "MIT", - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/web-namespaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", - "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/webpack": { - "version": "5.105.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.2.tgz", - "integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==", - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.19.0", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.5.1", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-bundle-analyzer": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", - "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", - "dependencies": { - "@discoveryjs/json-ext": "0.5.7", - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "commander": "^7.2.0", - "debounce": "^1.2.1", - "escape-string-regexp": "^4.0.0", - "gzip-size": "^6.0.0", - "html-escaper": "^2.0.2", - "opener": "^1.5.2", - "picocolors": "^1.0.0", - "sirv": "^2.0.3", - "ws": "^7.3.1" - }, - "bin": { - "webpack-bundle-analyzer": "lib/bin/analyzer.js" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/webpack-dev-middleware": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", - "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", - "license": "MIT", - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^4.43.1", - "mime-types": "^3.0.1", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - } - } - }, - "node_modules/webpack-dev-middleware/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-middleware/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-middleware/node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", - "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", - "license": "MIT", - "dependencies": { - "@types/bonjour": "^3.5.13", - "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "^4.17.21", - "@types/express-serve-static-core": "^4.17.21", - "@types/serve-index": "^1.9.4", - "@types/serve-static": "^1.15.5", - "@types/sockjs": "^0.3.36", - "@types/ws": "^8.5.10", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.2.1", - "chokidar": "^3.6.0", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "express": "^4.21.2", - "graceful-fs": "^4.2.6", - "http-proxy-middleware": "^2.0.9", - "ipaddr.js": "^2.1.0", - "launch-editor": "^2.6.1", - "open": "^10.0.3", - "p-retry": "^6.2.0", - "schema-utils": "^4.2.0", - "selfsigned": "^2.4.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^7.4.2", - "ws": "^8.18.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/webpack-dev-server/node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", - "license": "MIT", - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/webpack-merge": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", - "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", - "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpackbar": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.1.tgz", - "integrity": "sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==", - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "consola": "^3.2.3", - "figures": "^3.2.0", - "markdown-table": "^2.0.0", - "pretty-time": "^1.1.0", - "std-env": "^3.7.0", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=14.21.3" - }, - "peerDependencies": { - "webpack": "3 || 4 || 5" - } - }, - "node_modules/webpackbar/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/webpackbar/node_modules/markdown-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", - "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", - "license": "MIT", - "dependencies": { - "repeat-string": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/webpackbar/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/webpackbar/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/widest-line": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", - "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", - "dependencies": { - "string-width": "^5.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==" - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", - "license": "MIT", - "dependencies": { - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/wsl-utils/node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/xdg-basedir": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", - "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/xml-js": { - "version": "1.6.11", - "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", - "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", - "license": "MIT", - "dependencies": { - "sax": "^1.2.4" - }, - "bin": { - "xml-js": "bin/cli.js" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", - "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/erc7824-docs/package.json b/erc7824-docs/package.json deleted file mode 100644 index 2ddd7daa3..000000000 --- a/erc7824-docs/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "website", - "version": "0.0.0", - "private": true, - "scripts": { - "docusaurus": "docusaurus", - "start": "docusaurus start", - "build": "docusaurus build", - "swizzle": "docusaurus swizzle", - "deploy": "docusaurus deploy", - "clear": "docusaurus clear", - "serve": "docusaurus serve", - "write-translations": "docusaurus write-translations", - "write-heading-ids": "docusaurus write-heading-ids" - }, - "dependencies": { - "@docusaurus/core": "^3.9.2", - "@docusaurus/preset-classic": "^3.9.2", - "@docusaurus/theme-mermaid": "^3.9.2", - "@mdx-js/react": "^3.1.1", - "clsx": "^2.0.0", - "prism-react-renderer": "^2.3.0", - "react": "^19.2.3", - "react-dom": "^19.2.3" - }, - "devDependencies": { - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/types": "3.7.0" - }, - "browserslist": { - "production": [ - ">0.5%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 3 chrome version", - "last 3 firefox version", - "last 5 safari version" - ] - }, - "engines": { - "node": ">=18.0" - } -} diff --git a/erc7824-docs/sidebars.js b/erc7824-docs/sidebars.js deleted file mode 100644 index f77355c3e..000000000 --- a/erc7824-docs/sidebars.js +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-check - -// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) - -/** - * Creating a sidebar enables you to: - - create an ordered group of docs - - render a sidebar for each doc of that group - - provide next/previous navigation - - The sidebars can be generated from the filesystem, or explicitly defined here. - - Create as many sidebars as you want. - - @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} - */ -const sidebars = { - // By default, Docusaurus generates a sidebar from the docs folder structure - tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], - - // But you can create a sidebar manually - /* - tutorialSidebar: [ - 'intro', - 'hello', - { - type: 'category', - label: 'Tutorial', - items: ['tutorial-basics/create-a-document'], - }, - ], - */ -}; - -export default sidebars; diff --git a/erc7824-docs/src/components/Card/index.js b/erc7824-docs/src/components/Card/index.js deleted file mode 100644 index 9fc809f8c..000000000 --- a/erc7824-docs/src/components/Card/index.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import Link from '@docusaurus/Link'; -import clsx from 'clsx'; -import styles from './styles.module.css'; - -export function Card({ title, description, to, icon }) { - return ( - -
- {icon &&
{icon}
} -
-

{title}

-
- {description && ( -

{description}

- )} -
- - ); -} - -export function CardGrid({ children, cols = 2 }) { - return ( -
- {children} -
- ); -} \ No newline at end of file diff --git a/erc7824-docs/src/components/Card/styles.module.css b/erc7824-docs/src/components/Card/styles.module.css deleted file mode 100644 index eddff449b..000000000 --- a/erc7824-docs/src/components/Card/styles.module.css +++ /dev/null @@ -1,79 +0,0 @@ -.cardGrid { - display: grid; - gap: 1rem; - margin-bottom: 2rem; -} - -.cols2 { - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); -} - -.cols3 { - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); -} - -.cols4 { - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); -} - -.card { - border: 1px solid var(--ifm-color-emphasis-200); - border-radius: 8px; - transition: all 0.2s ease; - overflow: hidden; - text-decoration: none !important; - color: var(--ifm-font-color-base); - display: flex; - flex-direction: column; - height: 100%; - background-color: var(--ifm-card-background-color); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.card:hover { - border-color: var(--ifm-color-primary); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - transform: translateY(-2px); -} - -.cardContent { - padding: 1.5rem; - flex-grow: 1; - display: flex; - flex-direction: column; -} - -.cardHeader { - margin-bottom: 0.5rem; -} - -.cardTitle { - font-size: 1.2rem; - margin-bottom: 0.5rem; - color: var(--ifm-heading-color); -} - -.cardDescription { - font-size: 0.9rem; - color: var(--ifm-color-emphasis-700); - margin: 0; - flex-grow: 1; -} - -.cardIcon { - margin-bottom: 1rem; - display: flex; - align-items: center; - justify-content: center; - height: 2.5rem; - width: 2.5rem; - border-radius: 8px; - background-color: var(--ifm-color-primary-lightest); - color: var(--ifm-color-primary-dark); -} - -@media (max-width: 768px) { - .cardGrid { - grid-template-columns: 1fr; - } -} \ No newline at end of file diff --git a/erc7824-docs/src/components/HomepageFeatures/index.js b/erc7824-docs/src/components/HomepageFeatures/index.js deleted file mode 100644 index acc762199..000000000 --- a/erc7824-docs/src/components/HomepageFeatures/index.js +++ /dev/null @@ -1,64 +0,0 @@ -import clsx from 'clsx'; -import Heading from '@theme/Heading'; -import styles from './styles.module.css'; - -const FeatureList = [ - { - title: 'Easy to Use', - Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, - description: ( - <> - Docusaurus was designed from the ground up to be easily installed and - used to get your website up and running quickly. - - ), - }, - { - title: 'Focus on What Matters', - Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, - description: ( - <> - Docusaurus lets you focus on your docs, and we'll do the chores. Go - ahead and move your docs into the docs directory. - - ), - }, - { - title: 'Powered by React', - Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, - description: ( - <> - Extend or customize your website layout by reusing React. Docusaurus can - be extended while reusing the same header and footer. - - ), - }, -]; - -function Feature({Svg, title, description}) { - return ( -
-
- -
-
- {title} -

{description}

-
-
- ); -} - -export default function HomepageFeatures() { - return ( -
-
-
- {FeatureList.map((props, idx) => ( - - ))} -
-
-
- ); -} diff --git a/erc7824-docs/src/components/HomepageFeatures/styles.module.css b/erc7824-docs/src/components/HomepageFeatures/styles.module.css deleted file mode 100644 index b248eb2e5..000000000 --- a/erc7824-docs/src/components/HomepageFeatures/styles.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.features { - display: flex; - align-items: center; - padding: 2rem 0; - width: 100%; -} - -.featureSvg { - height: 200px; - width: 200px; -} diff --git a/erc7824-docs/src/components/MethodCard.tsx b/erc7824-docs/src/components/MethodCard.tsx deleted file mode 100644 index 351d5fb6f..000000000 --- a/erc7824-docs/src/components/MethodCard.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react'; - -interface Param { - name: string; - type: string; -} - -interface MethodCardProps { - name: string; - description: string; - params?: Param[]; - returns?: string; - example?: string; -} - -const MethodCard: React.FC = ({ - name, - description, - params = [], - returns, - example, -}) => { - return ( -
-
-

- {name} -

-

- {description} -

-
- -
- {params.length > 0 && ( -
-

- Parameters -

-
    - {params.map((param, index) => ( -
  • - - {param.name}: - {' '} - - {param.type} - -
  • - ))} -
-
- )} - - {returns && ( -
-

- Returns -

- - {returns} - -
- )} -
- - {example && ( -
-

- Example -

-
-            {example}
-          
-
- )} -
- ); -}; - -export default MethodCard; \ No newline at end of file diff --git a/erc7824-docs/src/components/MethodDetails.tsx b/erc7824-docs/src/components/MethodDetails.tsx deleted file mode 100644 index 23424bda2..000000000 --- a/erc7824-docs/src/components/MethodDetails.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import React from "react"; -import Link from "@docusaurus/Link"; -import CodeBlock from "@theme/CodeBlock"; - -// Common custom types that should link to the types documentation -// Map type names to their corresponding anchors in the types.md page - must match the actual heading IDs -const TYPE_ANCHORS = { - CreateChannelParams: "#2-channel-creation", - CloseChannelParams: "#4-channel-closing", - CheckpointChannelParams: "#3-channel-operations", - ChallengeChannelParams: "#3-channel-operations", - ResizeChannelParams: "#3-channel-operations", - ChannelId: "#channelid", - State: "#state", - StateIntent: "#stateintent", - Allocation: "#allocation", - Signature: "#signature", - AccountInfo: "#accountinfo", - Hash: "#core-types", - PreparedTransaction: "#preparedtransaction", - ContractAddresses: "#contractaddresses", - NitroliteClientConfig: "#nitroliteclientconfig", -}; - -// Get all type names for checking -const CUSTOM_TYPES = Object.keys(TYPE_ANCHORS); - -interface Param { - name: string; - type: string; -} - -interface MethodDetailsProps { - name: string; - description: string; - params?: Param[]; - returns?: string; - example?: string; -} - -const MethodDetails: React.FC = ({ name, description, params = [], returns, example }) => { - // Function to render a type with a link if it's a custom type - const renderType = (type: string) => { - // Extract the base type from arrays, promises, etc. - let baseType = type; - - // Extract from Promise - const promiseMatch = type.match(/Promise<(.+)>/); - if (promiseMatch) baseType = promiseMatch[1]; - - // Handle complex nested types - baseType = baseType.replace(/\{.*\}/, ""); // Remove object literals - baseType = baseType.replace(/\[.*\]/, ""); // Remove array literals - - // Check if any custom type is found in the type string - const foundCustomType = CUSTOM_TYPES.find((customType) => baseType.includes(customType)); - - if (foundCustomType) { - const anchor = TYPE_ANCHORS[foundCustomType]; - return ( - - {type.split(foundCustomType).map((part, i, parts) => ( - - {part} - {i < parts.length - 1 && ( - - {foundCustomType} - - )} - - ))} - - ); - } - - return {type}; - }; - - return ( -
- {name} -
-

{description}

- - {params.length > 0 && ( -
-

Parameters

-
    - {params.map((param, index) => ( -
  • - {param.name}: - {renderType(param.type)} -
  • - ))} -
-
- )} - - {returns && ( -

- Returns: - {renderType(returns)} -

- )} - - {example && ( -
-

Example

-
- - {example} - -
-
- )} -
-
- ); -}; - -// Add styles for the summary icon rotation and code formatting -const styles = ` - details[open] .summary-icon { - transform: rotate(90deg); - } - - .method-example-code pre { - tab-size: 2; - -moz-tab-size: 2; - white-space: pre !important; - } - - .method-example-code code { - white-space: pre !important; - } -`; - -// Add the styles to the document -if (typeof document !== "undefined") { - const styleElement = document.createElement("style"); - styleElement.textContent = styles; - document.head.appendChild(styleElement); -} - -export default MethodDetails; diff --git a/erc7824-docs/src/css/custom.css b/erc7824-docs/src/css/custom.css deleted file mode 100644 index 362e65749..000000000 --- a/erc7824-docs/src/css/custom.css +++ /dev/null @@ -1,213 +0,0 @@ -/** - * Global CSS overrides for Docusaurus. - * Infima is bundled by default. - */ - -:root { - --ifm-color-primary: #000; - --ifm-color-primary-dark: #111; - --ifm-color-primary-darker: #222; - --ifm-color-primary-darkest: #333; - --ifm-color-primary-light: #444; - --ifm-color-primary-lighter: #555; - --ifm-color-primary-lightest: #666; - --ifm-code-font-size: 95%; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.05); - --ifm-link-color: #3531ff; - --ifm-footer-link-hover-color: #3531ff; -} - -[data-theme="dark"] { - --ifm-color-primary: #fff; - --ifm-color-primary-dark: #eee; - --ifm-color-primary-darker: #ddd; - --ifm-color-primary-darkest: #ccc; - --ifm-color-primary-light: #bbb; - --ifm-color-primary-lighter: #aaa; - --ifm-color-primary-lightest: #999; - --docusaurus-highlighted-code-line-bg: rgba(255, 255, 255, 0.1); - --ifm-link-color: #a09fff; - --ifm-link-hover-color: #c3c1ff; - --ifm-footer-link-hover-color: #c3c1ff; - --ifm-background-color: #090909 !important; -} - -[data-theme="light"] .footer { - border-top: 1px solid var(--ifm-toc-border-color); - --ifm-footer-background-color: #fff !important; - --ifm-footer-link-color: var(--ifm-color-gray-700); - --ifm-footer-title-color: var(--ifm-color-dark); -} - -[data-theme="dark"] .navbar { - background-color: #090909 !important; - --ifm-navbar-background-color: #090909 !important; -} - -h1 { - font-size: 2.25em; -} - -h2 { - font-size: 1.5em; -} - -a:not([class]):hover { - text-decoration: underline; -} - -.footer--dark { - border-top: 1px solid #333; - --ifm-footer-background-color: #000; -} - -[data-theme="light"] .logo [fill="#000"] { - fill: #000; -} - -[data-theme="dark"] .logo [fill="#000"] { - fill: #fff; -} - -.logo { - width: -webkit-fill-available; - height: 70px; - padding-top: 16px; - margin-top: 8px; -} - -.alert--info { - --ifm-alert-background-color: #f5f5f5; - --ifm-alert-background-color-highlight: rgba(0, 0, 0, 0.05); - --ifm-alert-foreground-color: #333333; - --ifm-alert-border-color: var(--ifm-toc-border-color); - - background-color: var(--ifm-alert-background-color); - color: var(--ifm-alert-foreground-color); -} - -[data-theme="dark"] .alert--info { - --ifm-alert-background-color: #1a1a1a; - --ifm-alert-background-color-highlight: rgba(255, 255, 255, 0.1); - --ifm-alert-foreground-color: #f5f5f5; - --ifm-alert-border-color: var(--ifm-toc-border-color); - - background-color: var(--ifm-alert-background-color); - color: var(--ifm-alert-foreground-color); -} - -.tabs__item { - margin-top: var(--ifm-list-item-margin); -} - -@media (min-width: 1416px) { - html.docs-doc-page .main-wrapper { - align-self: center; - max-width: 82rem; - width: 82rem; - } -} - -.navbar .navbar__inner { - margin: 0 auto; - max-width: 82rem; -} - -details { - margin-bottom: 1rem; - border: 1px solid #ddd; - padding: 0.5rem; - border-radius: 4px; -} - -summary { - cursor: pointer; - font-weight: bold; -} - -details[open] summary { - color: var(--ifm-color-primary); -} - -/* Custom button styling */ -.custom-button { - background-color: #000; - color: #fff !important; - border: none; - padding: 8px 16px; - border-radius: 4px; - font-weight: 600; - cursor: pointer; - transition: all 0.3s ease; - text-decoration: none; - display: inline-block; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.custom-button:hover { - opacity: 0.85; - transform: translateY(-1px); - text-decoration: none; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); -} - -.custom-button:active { - transform: translateY(0); -} - -/* Dark mode button styling */ -[data-theme="dark"] .custom-button { - background-color: #fff; - color: #000 !important; - box-shadow: 0 2px 4px rgba(255, 255, 255, 0.1); -} - -[data-theme="dark"] .custom-button:hover { - box-shadow: 0 4px 8px rgba(255, 255, 255, 0.15); -} - -/* Position navbar items with flexbox order */ -.navbar__items--right { - display: flex; -} - -/* Give the theme toggle a lower order */ -.navbar__items--right > .dropdown { - order: 10; -} - -/* Make sure search is first */ -.navbar__items--right > *:first-child { - order: 5; -} - -/* Give our custom button the highest order to ensure it's last */ -.navbar__items--right > *:nth-child(3) { - order: 100; -} - -.git-diff-remove { - background-color: rgba(255, 0, 0, 0.125); - border-left: 3px solid rgba(255, 0, 0, 0.5); - display: block; - margin: 0 calc(-1 * var(--ifm-pre-padding)); - padding: 0 var(--ifm-pre-padding); -} - -.git-diff-add { - background-color: rgba(26, 255, 0, 0.125); - border-left: 3px solid rgba(26, 255, 0, 0.5); - display: block; - margin: 0 calc(-1 * var(--ifm-pre-padding)); - padding: 0 var(--ifm-pre-padding); -} - -[data-theme="dark"] .git-diff-remove { - background-color: rgba(255, 0, 0, 0.15); - border-left: 3px solid rgba(255, 0, 0, 0.6); -} - -[data-theme="dark"] .git-diff-add { - background-color: rgba(26, 255, 0, 0.15); - border-left: 3px solid rgba(26, 255, 0, 0.6); -} diff --git a/erc7824-docs/src/pages/index.module.css b/erc7824-docs/src/pages/index.module.css deleted file mode 100644 index 9f71a5da7..000000000 --- a/erc7824-docs/src/pages/index.module.css +++ /dev/null @@ -1,23 +0,0 @@ -/** - * CSS files with the .module.css suffix will be treated as CSS modules - * and scoped locally. - */ - -.heroBanner { - padding: 4rem 0; - text-align: center; - position: relative; - overflow: hidden; -} - -@media screen and (max-width: 996px) { - .heroBanner { - padding: 2rem; - } -} - -.buttons { - display: flex; - align-items: center; - justify-content: center; -} diff --git a/erc7824-docs/src/pages/markdown-page.md b/erc7824-docs/src/pages/markdown-page.md deleted file mode 100644 index 9756c5b66..000000000 --- a/erc7824-docs/src/pages/markdown-page.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Markdown page example ---- - -# Markdown page example - -You don't need React to write simple standalone pages. diff --git a/erc7824-docs/static/.nojekyll b/erc7824-docs/static/.nojekyll deleted file mode 100644 index e69de29bb..000000000 diff --git a/erc7824-docs/static/img/erc7824.png b/erc7824-docs/static/img/erc7824.png deleted file mode 100644 index 49a38d00072c8c4363514d883f48735ad75b98fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3592 zcmbVMc{o&iAD?r~SjIB8Ze9slGP31Tu4Nc|2vb=ut|bZ$Mq^)+Lkn(n%X)82b0iF2 zq#;b9ZskhFTqawo+$>`-%AW4$ARn2!586owY~O*!-gm1QZ3q zf|}hm;MeCrJ3(tac@d&2Dl;fn^Y87a3KCMSk&Kv-?!*5C?qOJ@_@HSWIWXXLEw626 z(?S#l2!sHPf;&-514exi<6cCs1TO|8zPXrz!uF2{N9wn)3f{GN`gxWEzYF6ouboRz z-1Q7U(A2aKMN$R``#|h_9h*75mZjTk#Wz?FN%tL9;cW>J8`LpYqo?J5pk-H`tnF$B zw52YJhh8btV#c7_P3zER@_x&==#_zAlcM5cEN5oS{R_LLDrjTz=<+P?zaT#SI1;IUv@jtr+Z50&+Bu-!v)f&qVyT_#a zpCk?=;mnmUyQ#1ndPrF@z-sXGS!U_4yw%uxX%cG4jFz|w1x)%>d`T)457)cro0flS z07%$HDDO}IZTvsSvK}k!;i!Q zg}Il3$%PXUF$VFWA>F#vP$3Z!ffQx>6n75yetQp)V4-H1E@Xu|G_nsAqYCgQM*C>}`TYXO4# zxvysO+-hwSmTu#I8yEZmL_MBi389;hft!9hNHXUgtCLJ=b3a4VDr8?d_RDi!4Rl_t z6=}i7YFCuqWwYR2klinz@6;2Hi ziGTuI%*m~%3m3js?q!Z{_OlA78=c{GDYIeI*uCEiHk`B3@=(SsSVmetMy!a8VdBD> zopc_vQ(OH!0gfLU?#i-Y=K6Vn*oTl?TaVZ-vG6;R+!!m7uF#i^RN>}cGxoKq4N_4d z1a4gLYuk1qmTV>;JLI1c@D|Payt|Db}*?<3A`A_E&_qty~mF-f*2nykbM zZ0jv&c*8Y$aofgl-h&QjM8BsdaB5u~@GtaQV?Hd+?tY`5opWbF?U^3W9lc_nn%K<2 zOJIUFlvJ;goc+?a6UVRYrFy0umd?Dm^{%DDwfk8h=n5Pa<%*+?Enb{g-VcAUjX5)* ztP*3rtp=kkW!Qrv9DX%IZ&8R5X(r#=+K1FY44_fXJ&9={{G$lR#BfrItGQHYk5u1= z7`9=A;mn5}91^_AJU=1wjN7@jaxtQkj;P_#aSn}ISjMaV!`#^V$*Hx!hk*Ok@}dz} z3bG00072hu4oF@d6!2clcg+=3qx+Ao>^DIgP1_QVYH2#=F3D5Lx4SGdh#AmLQ`pPu{%Kpcu&ifJ z`U#zxehK5w#IIo|3;AlT#VTo(bkBKvKkKbGB}}9Ys_cYFh$2RqMp~uIH|IZikap#V zt=zPo07DL4!mQ+vxW7B%=6Wz-eyEI^(>b3j_KoM{11!AoOO+{$?7f;~;Pd(E35{s$ z8DtRTK*ck45rmP>eYR0hD@kp}YO4e)T^{LB?hGY*MHiKgg`#4s1hHj>w3|*Dz-qn~ ze4+GDeTiSAU!31B%$f6jwFKS!uSnnZMXUrPIzcpidHW+rTks&&Rg%##T_VD%$%DWj zWWLP4a-sr~IBj(})at@kFwqf20#Q&~=R1psC-o=;9gb+@Cc1spge(cfK9J6j_)L~mv@*zt2(IMNyFgFc;OW14#Kr@We6h(h38lV^`9?LpQa>Z)tv@k(Li zG`Zoq>EmFE;qKV4@hFw~BFizXw9OvfY&0CRL!u!hQaiV-_9B!peaDOCL|N=oWWQEf zw}AU>-vp?XGr=3TM*A0TZOnRn zbC*I(K|ETPP8<+95{%|_AST_9f79#5tbW)-O3i*v2&<`=A=S}mZr@jU;?ig(z2h#w zbn}xWBTe@x^KYHd)#6DN*OOLE%EVa9X)7@iuF&%|G(Y9GjDQ;<)m3P+# zvz3CKOe73?`WK0;Y#?4)N^oCmsF0a_M$X+Nl6NszevUSrx~ExY5ze& zOii*hxS6gBqdHZuT5re0Q14TPzd8fSLmg8GkJ=Eo<>C8LNluky#gU!%6d|w^p`AK2wCC>mh)|Y0#k?xKvV!V+l7eB zhCqhv3szN0qbiJ8tTp1+a-6?GSXvKH1?87EX&d(#3@(02w@Po&fQO}RSmSNHnHbR; z*ngsGQuD;}i5_3PiS27-t)u2!g^J&3=-sifY_PKOl4ax7#upsw=n+4^%}rQH`6Z#{ z6kF|shSk%$T3Hfm9$0v6_8Z-~@V9yq+d zz1?C=o+S?AZv|l^Iy7+i9`)R5vRGw|m#Imm3dcRe#rn;@)zU)CzfX;26+mkYfIu-^ zRA7_MD_`GBiWA0uWR~&OVSlno){noK3$IXh^FACNn&M4kmP!vv(F4i zz^1{GO3doV&leS^kj!@(=IFp_WRpLBY$XWYn9i>JBp*k>h1lud6vDIxJFQEzEf2w_ zY6+6ure=a5RjJ}eJ_P>MwB??NaF)-Gwx3bp(f2GJoGd(_{HP?uQu6VDS9`4X={_eS z&~PA&<3^MrL09+WpydHf(lRH21lSV+vrsAq`RC#p5YiocRYZ1?kjM%K3Jtz;JMk`V zt@f+e_UKWME!FkUMSpr!_&Ii~qKglb33-;KY*F`$}}Oce87-3CjEr<_j7( diff --git a/erc7824-docs/static/img/erc7824_social_card.png b/erc7824-docs/static/img/erc7824_social_card.png deleted file mode 100644 index 06b3a9ce25f531e59e251869098a81e30e2553fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7762 zcmeHMXH-*Ln+}2kA|Mw9l%{eO8zq8(lt4fMH8epwK|zR+ODB}jE~uAIt|GmI^b(O? z5(ENBO@v^O5)=eNC=o~qgf{V;_06pL@%@`wYi6zaan?Ee&B@NryWjJ?&-0vEQ)2`E zljlzY0092`hIbzU037sR2hRz1Wbn&u9Q(xUZD`{M0PqR_IyeBC*<$Pxp1p^y_0f1T*-~Q9%0D$Du{kvd`Ada;elZ3~loNpWR8HQh#Zx?+{c;^1va6{}t zoK++Eyo)PD{7nBFGft?r{)dZv+z-WEIU1eb>ziLZSt9!6-M9MQC-6@fg8vjstxEd% zK_-~9(F#*`j8UliIQJe*WQ$@1n-A|Ws4#MNB2@;`!kCBaOEM`{hPV=P-X3OY+7BQ0 zEkt|gd)jdTr9R>@;Lj0Ze}JJ5f#U)PnEOvIE=j;|r~g0jKbDV3+8KRUSJ#0WNs}}q zl(yxf3dr6SCVjIW-!o`6P+*mmdVQ!S;(h35+pNeWOtINPM8u0f!ZX#6c35&#PA*Ml zvVFPs1EXJ7|0n@JHZ;_#i%CtzUVS*|`1WD9b0@<@+iMNxd>$!H=h@9&%DWNbII%X! zBPTT5Gn^$Qd4LO4GX7zl6JB|no5r&fd9bG+p`Cg5MfHwN@R|uW*wuejRqWJ*Sq5?- zEFd6k_uBz;pfimbd#8S)MsjM9+lnln8%mQO3-A`(50vPe4t|q z8^sB3xAG6>-t94Dq77u`-D1^F{N)_#*OPe*7JOtAtNZIPy+})sUSL zG2}em@re2^T`yWPY|Xofd%fDkQ)o*dW}X%6IFg2?IuM*AjjEiuTW^%iW~qDkb$v~z zmWn%h=!t)^h3KK9q+s+AC}l~U|0^!CEw-GA!E3|x(a!5P{d2E{jaBALPaK&$LJZ%n zyZsd7N_VH_rgz3pz1}NP1)~%el|@DYj96j1NB7DUA{Gpo@rQZ64o@5n5vCiV+F)mH zRczg|{o{h3YR}cMB}YvM);m?Os(N(xoGnsPrZ#2Ge!0fOZY2xjRs`h=a}mFPp;TSp zsn$Sa#*)+zwPReOWI?jP?tXsbvvKaNTG7K=t<#h7`OIbhSD*?R>~`g+r{*YEScs56 zO83?Dx@K#AYx#If3;!z#R=q%>G}w`LP4n-Wp`pB#imE0t`9x4oHpAI$S{obJm_5gYqK&&lW1ITj@V;0-M`jV&%#ILQz{<(YOpc$Ny|A3W%R zwNUYXqPw9M280{loAeA{>afx)PaE6A!hK4tv?%p#{F@}HJN7H)pIfgO|DHF438bG} zZ3_z6jXaPtvpa6SE^KSvetGdH|0P?6bq%%sn(3R_xut=JWuqBEJawI zIkI~!v~AZ}_GF;`l9l#;+0PU>6!n0I^|B^>$A<$u!@l!eY|5oYrjSw&#F!wfEOzST zd-aEdgT=I)FZWuDFXzQy=V5V$0waA|0uB+-WOw$+)peD5N=Jh|3pGeUNoTe>^v)e- zfywi+c~p|f9^id|0+#N`mn=l*T?=~}*g}sWHl&SGiVlx752{`!%3{4W}ikdepoS~DJS&8@u((L@oJA`)thT{h9OUat%pXR%EkASCGkn(P(H1Zui@}7?{budG^{vJ+K z-tYB82Ov!tocHnLR$77C4WR0{fFrvE%2tqqKM}Q#A%CUgtNf;$NvkYwzGOfCFkPV0 zugx|^HOtAZ-^TmGDXUdMGjKNGJoSwN6;zvm%;a0Rb3$;$p9~YfYV^eZj&nmVg)C?9 zjE{^7ZbV$4YV^Fe)knS>>f$-y{i>gr^)g^>LY@+81mTlLEJ4CHHrQs%=h6_-x_w!Y?T-cvJvp)Gf@upJx|P3RjwLnHhz3$BP7!m zJOAzH&bdCbE3lW)$QKGU@kC;|BcwmGCOfy888VgURqjF6(noV|A1NP%NOUZ*kc40_oVfbPBc`@c7?b{77a*)ONVENKTrn^g76moZ#5J}%xMJUD1ijI z60-B<2nWj&a^$$M|C2v%n9~q(iQk!S-UwcfhjJZEItYdWDnvQ5kM3dG+HPwxJkbn^1VVD`i(!UTPI0HQ zJ!boJN06%<14`tV|^}L z)X09(-JN>y!<#5eU2C|NJh~*iz}R+!7YYnNpSxvcUP-=($(d6-B#z)2x+RnsT559g)z zi=rpYp8ITk(au}I4^`c$H_=oqs*t-gua&essgCO`_G+^TOAdOuAbt2VdNc26EaE02 z){VLI$Nm8=!jZbo>^O>8m~LAdOf<1_m6EzTd$1m8piR7C{LWJM@QY)P+|f$c(e^$| z8~LBK4rIPjjNu1HM@3(uhde@qcIpZwCOXhVBOq() zEwO2N(P&y{gP^uNU%aZpQI>|Q>z73GiyVzy`7G&(sxQofc;j4D$7cNVI z+{5h=Dh;B+M^)YteDO{C+Cozpg;jY2M-BT55m}39=$%KYsX34tlk906OgY?@q;0*u z`qg0@)X_$FXqVsWP=m@tksSo7l4&=4oTjTTE|f8X^}}j1=t|w3%lQ_UjnB-5o!%Oc zCAl3gdA&6+t(1HnFg(Dt8k#a7xDf|Bb?)|x7BTeDSZIYe>{)odA~w1E+F~6oS0U_7 zUgyqXueO^sxYP_dEWZd3WK0?e_dWA(aaJ21nd~vtGLB4_5T#e_v_oK(CmAu8BP?Spio$bFGm<`&$Nsy!IgiX3&{ zo3)3uD82srmEpqf7}yQ`bYt@g+tMW2VxU9RNanpEh3tqB`k4wZr7z>N zaEnj(2sX*%y%Ns_1q}v_2((*G8{d8Bc0!jwq}Z8Im79F@b{QGTiG7;JmjqUi+?HF_ z(gT7k{8p0f1{$kP-qzSzH$NU>P|DqW_MTTg3ycVcMun$kWFTBfNo&QGmLu4eGeFeq z;C4$Rjg7(G?+li~!;_8ye&S!$%8>`3zDp=6X*{rL53UqtF7-iI5qT8+&=7=9xNrrQ z67ri7q}WJl(a%qO6BqiNG~$`7=84?@Y>{RYupZR}H(^BSkN2a*-E@} zj%J$9n(P%1ZF%qL{J^`y;(9xy+wmScZE{JxKFcK|!nM}@;dEHEdYhN-EDw@fxt4G_ z;A!Q`!>l!_O%qF{)895gxqUxnavz@6pm|QQkgy<&X)x1-<$Kn8b(&A*m4{hg zw)|k?7Z1B4Lyy6S#9A#@=XU1eJ9&?w0pz^ z&(sNq)qZFpG^Z(wcrH#hwetrE%)Y4TFgNlLalRY^V$(VsM<=QHeO$Y`5W`3Mrmyo! z!I%B(PkO|GJ(4^|tAbB1hZskJ?%ilAc!p|88E2$x2RKz}d$nS0AEQR*-!-lG<(|0l zb?epw=(5o&@oZL7{r7nB8(Ktv1!G;ghL^ZP-cYiXDmC@F7sjW^fd|>() zFRr?mZ6C%~?x`Rp-MlDJkEXrqxShD{!{PYH-)sM_s`wl%!DpXTcruFo{)!Wp(Jxn> z`N&RX{NZni)$gXv2LUWSf;jQ+P~cin>;8B~at#XHqtDk*j=;-7>r>$r{@A*ot$`tN z>3M30TW@UsnpvDv@@90egGgHXEb8Wq@V`CY%u-Z0rinZzO+G&Y(RBhVOPQVr^8bv`P zcb&b6toJX}8?#mmC!Q*Jay&!M;Tp2Hf3$~$ZEkoS&nahXK1n3Quv*aav>(8-`!@Y{ zT%r>-Dfo|)H&1Dlgr;nc`a~ht`MX2T)TWWr-|r1MKg)e;RfAY}I8m09kGen9$EN>z zUl}WWGXZRP@BcQsz`PL*>a=wf%#z||S!V|AyEUeBo0M0eURX0-f})<3QP-nyqWhGa z|H3|S!m=S-^-P5*u*v#2>x-01!rdVmq_lniuJu4nk{z;Q=`L?`R(?c$iIbGfR@GRk z{u{ODD(U@B$xCf{%oq&hGtsPH!)^{UuWc!fF07 z)!I`Z@+x3`B6oB1)cb&_aA~0}v5Y=BcSLMDY$7dGf=yB^%6h@5k>l$ZDa_`e-3M#H z*}blHa*rl%?P^J^Y@qiMsL1adoa;bdXiE<*QGI*SM-jxD{61<`>vnfKw{P;7X1epl zUtl^q|Hw>JPR)zNJiOIk6>+9dl#{0}iQkJby@R(ORzQn2=v&#*{^DZaAh@l0Z_JLm$;nw$$yinp0}= zQ0u|M#QaD@kzQLLnc+XX6(0KjS6XPk{;w!4rrCoZ@IguUKScNcC2#YaMzO5G>a3Lj zwjFKp{YMsTpnMGWK-6L*B;u&R!14fQB9W)i7Vw7xyrF*o-tOm!-HnRF)kPyz$}eA| zY6O+L<6~pXa(WyQu=G=FfAD)qf0Lbc`{SC~ovQ2*rfN4Su!ZR2>RPfIeAx1NDV!~y zyN@t6^si66L+F!^t)a#C5{FM-Vy_5&;<>DYL(urz{^9wZ2n;OO+uzgP-9a1l<+6>$ zwXh+*_7fYnxC?X1vj>esBQ*+aj{^QX_2bMXMYQxC`(2;!-+s;p(i^%j9rCm3u$p>7 z=2UY5+vPCPAx4Ju_!LyUMkWjiwy%oSo%iVyPW#5vd?ZTejaBzXtI0#or|ukSG$zOv zhR>xPIP6`doIQ>B=K1*L!pFnBOjpMj%{5Sx?O%<;^tc#x?x7Rxn&h>^1lP^1oQ5vOkE=1Ad7ye4aMrere*;$d%0=68o*bZS?z@bdEH>;d@H zm0EKSUHu4`UNVFP7;3I*ungVItde@eSWnC zGq7t87~Zc{E%6<-F1ChD^pL_cAuT4c%Yn~3*i2bbVnN_R8U%i#%pEqXAH+fyA z*VglvF}isq=kP(X-TqDIoNm%=(fufpOQFEpmxdL>i>aA*&v10Ja!(Lf*eOKLZk#$G z`8PUHvu#hdP(bGfFH6jcYDDCCE(4ZMF5@KVM`2H}<5N7W^Remxk4qHc(}+)AB{0;t zcWqo@x`hJ4Jt`f3;?nYj<55r!v*=J)7%7b{=J@CnNrGP#M{`;}iCukXH`L6ZN9B|5vRC=$ADddduyD-iG(9s`oj%poRsHMwM2tF} zG-m`ZJr_kjZ5DkPd)$XDHZAjcqsSM`qQhGb%hSgsz+UNPdJ);kEl6Yu59>_TT};iM z`8~-Jg#dqh+WAcmqg%w|5~gXYL~m&`Z#fs7s*GIMC6D6L*gP?=YZ%K15y%x-rUaVS5opA6AvE6 zyzhx=@KEtW)_`g>3i?2=!Gp!A-04RGDMiIADjvuxMJceCkeO`qXT%SY z5p_u|-{#e@@W2dWffY(1qGNk)OiE^IxLYhBOfT!j9`kH3oBYX8P0`k2*fo3;gnkhB zJa=bdpVBs|-*GtJ@5}aHiYwTi^)Su> znX?QD8piA|usz81o$~=_&1q~4V9u6xo{sRB;?iQ*F?Biv8G(VgkCrocpn2*CmQNXT z82AdOU*-kM{TOwNCAK#^FfOQlJvq~#s+F7mg|s~HQ;iEP?_+N8AukmQjNhRTe71yO z@ak0OF7y+|j3 z^cH%D8^3?w+?n_0-MPt}*(bB>BqwXH_1gdd!oB~!KmZHC$_xP1xcj#{TFSSH=!kH) zw^daX^#0ZUy9n@bg@sqK!@p1NURnmAfn3BV0AQL{Rgg9CpW4ohG^SFXh`bVPKS=v7 zM4Vg9Wz+rY_SZS`*f#p0t+wYebU|<1@|5hN*^lM6r2J%M6%@YcDbBmkv%;Cz+RXV8 zyE}7NePRQTIMHo_wSog$*>ct)=C~P-^T{6L=Ks$sa0>+h2ml~)1sU!F1Aq*!_+P1n zAT|Ea3Eg%7rt3Y4%p9p?zw+bZBtt6)@6}7cG|y##LW~O0IU7&pn{r$XW;$Nko_Fw= zeNs!QPzd|lo_w4DVr7CI8VPVN^LS)I($ydlF(FH#`mKER=ap;~<{fF37HwLj%un|3 zF0OqIH`$POy7%%rx#PtXN?uwOUGql$;7hw~)r+f(M?PDp+;kF8d7y@+ra9n&&|g#? zn$uc!Zt|3tZGPdluuq)IA2Ra^6ANiXwS(n=OPy}b45gnx2Y%9YMv^i+{YL#v0MvR{ z?vIHkla_B@xisvsuVP)Me?^QWFz zz@~S=x8W)i_TPgaWmxmhFBf9XhVxMV+5CrOGS^?cjso{Y1omUky~v01>qmvvu#=Le zD1Wh+mgO;R5S`H~2G@5}D+f>}a1V14mpXAjqFX6g?17|eKDOuM zCv&BbQ~+n4(X0|B&CNaVmoz~1vLQHj!;9Rgs=An^ooS`t+BTaS_z*?PASp-kS_U|# z00s^X2ueOr>@G{vi`(+G1)cP-9{im-i?*ekh^hvvh>`fdMwM2M(ckWXg6XVU{Z-@j zI;^<0QIE^5YQItk>_6D!7LIaqQ-SBi-PI4hAZ2(3yx;*s@(>$S<|5jt{S+=Lg^L$@ zTEinFC6JQo08>j5Nq`L5vgtvPPxi7{#76^F-XFt(=91G~_Ti>(R z(#j|_Z`>t1drmInPJp&g+*>vE?lMnkA-y~?TBJ{omh^w|-OTydOKR6?L|!+cHVKW4 zR)5eZ5xa&pEm~8pQ?c=q`Bb;r=35|t+|%}E!S|LDED6|WJG$JhSuVRxgI?+;w4FuX zz2)35f^waxqXJ`J1GaX|Z(VB~rj>7Bhqh-SEp|<+636m!H)enO_E8e&e$Q1PRgccv^X8G4C4LAvW8-K;muYR5@at0&!RoI{AVw z#kH@HG!OtJ1MrKen#P;?vUSnEB*W1!M~Lf8hIB>T+7Ek}EsxVUh|RzO5O~OxkA>y5vjDs==xSdBMirAXpX)nI0%6g*wPB(=rrg?F z_mP%C$MM$dHeKLdVlc+5-x)(6HfA$g_-KR}b9O-a&S^&NMe=A((>K0i?l?a(V19*u z2vuJit7}99GKjruO;ydYlq7?|t8-yz$$7nv&7z@$ zB+5Xpwxh-uv$FA;Q%*Y)_QH!T(pT%4`Sa+?JlqU0J5fJYDZQEtF%ei^r@51|^f zK}R$+t|IP7;ci0D$FxYMoTV@c1M(OS*Vw7sit|R#3w)FTQ&UqRY|-5}DHPY&7&YPb z%ZOiO$Ra!tv(&qyBl-q{vP!F#p~qO?3D~rcPB~x(_1K-eMO0#uPDHau0o8IpC#BPJ zc3v-C>hAWg6`g?%?l2KwW1nUiXyuZ?ILnOOb2^pZ>-!x`5TKX4SVYP4ZNMnzJXGqB zM152COWwgBeN$RZ%V8BfZho^b0v<(WLOia&v71YdGGrQo1NDI$%XKTx1h?7tJk^kn z3KO%oPV|>J^y`Y1ldYr@Z6gm{Ll($f**Ss+oT&egC!H%#WoPQhJXLA zjuuZMYG?}2V*nHj_S~NUZ3gy`lxTk$iO+hpL*$Van=NF?z_PfxaDEg@(lUZ317UmH z^Yb0akX;ar6Crlbo|ZPP*$;Y(U`wx~=9PUZRc=vlRp-t{`|6t=?%>0u>z1WF+PtVFY96fo)e zntN67#8k_lk6p4R;`2atTs$;tSKhqc3ufaCUaKt(@G?FN%&8>MW?^ zCPdOGGm83~P8|!rk<`N}s-Q4_@F1R7f1peORQvrqTd=OPwxb$+|6ORBKALgM?lli-#G-!mTw17~g$VY%GXBYOQ#B^bNe z)kRgZv*N;VN0@A{d0GHOw9W{X%BXv3v5xTP>E~;_2(a^z%8*m#gzy_n^IS>xx z$6pr23RpGE5fBjEFb5vfa+!e}$j}kdDSO}VlI;`aH1=H-F|sOnW1=7hCD8YxE@dJV6Zb0;aU;vuXN^i~K3;qNN%Y{}ulLf&gVT-9 zrNmD)e#?74Ei)bd`p1Ek?s0RlAVla);j)Gq-zWv?pH^D)dI)cnTFj56LkC zTKsk?q~V)fFl*Osz6>cu7@a}`*)7rRG$+4sN3(ayB>A5^yY*Z)h`Yf4xPP+e%Vd@A z6bE(X06Iw@N_J`ZvG|#MdVZx`a4=XHCu)oAg-V6tyV@B-z8v$&NpMTW8);W9O8H`|_^XrD>)L zSvCkS{UoEfh&w-I2YB+6g+dr7l8liSwCx#zD!B|$1$FfQL;~N&16TyPiU|K5&`j%531C!A21GY$~NeOg0k@RpWt23PK%3WaV*7qz4E)CG-y1Mgk zA_1#b${@(8W7Zq#6y*>2w`mKSC;O8NHO@+Lo<3=mFZe_?I2nQtojR1I$i*6kKOn6}EX80|_xJebb}u`r3w6EdkUE&1+j`XKIyWRt$l;PQFe^cgnC3>wIu~l=272b; zFuYj6+tZ;Dwb9FL#+z}6S*-GO(xn-YfAisUN|lvvV4!$G!MePX5}swdm*y5H!Z2qID4sJAL$@rECn2F0%Q z(d(*Sso|GYUXB&?h90gF8fI5L0Ip4cB5hno8U%56DpDLVMz~(P839j(#kfGREF4CZ zTUl-RYc$qrKChJ`V@OPIV1FkqM-5KO@=uEVrR}fEvsW7BZbFir;wv< zY12)dNc?sWjD3fvgm%mOuer(94<8I>LEA`I4&a~yjIVX=IEUa}zlwu;hIg_2q7~17 zpKM9VbY1b9*g_eND>?c)iYRGXz-JX)@F=Ik2pLIx|K00bec@=y!aVzRkx-h|w6c*h z7Jz~X33rQO5Y#Qbv^S({rxboZ*PvK4QO&>63BL?hb|3 z2gY*kIVWtaCdJ2d7m5c2>TcUG-%M91OIOpN4UvEb2QuGrzzN%NaOpzhRpA}emY8*;yXdh2=t-qKyiqIxB3gbMQXL(_V6y`=TB(*On>c=w|Z0t z#v%OKm*fGZEVKk@1#nb!kcMT;2^Fb=Ywb;opXJ(hcq&+vtuM2~+-J!#(s5M)_mAwCqWXZh z#DDw4s)C4X-uk4+ulR^+$K8->zD)dUi?7nFY=4r>crOD&8J}z%3ZD9~T3r!=C|Nw% zSKv(_4J|D5-=Se|Ug1@feV0oQ<1wz<^ufk7iluWiGB2enI+G2(!ZCx&Q;W)ZlOrmY)xj& z!pr=AZwttMu6cXhfkjLC45M>3ypS+OY1YDev@NZ`@S}u-K`ijw<@Bc;3N$+bI@9B5 z>UOoPqU-W1$4<`>1s8jlH@<4IDd9w=JiPkn@_2j!n{?heCiElV=@LO8&76>JPT8A>8K`J6-{Dfi-Zv&8IqJ3`C*~-?zoQc@@g`&HK21O*?QI?r>5@d2=$0N zo*p@1Tv)#em&K*3YOv^isfmqxI;W|ou7fY`bFE_qIVJ00?Aa|oW-0is0>@{AlioCn zP=upk932Q7#?_khW19YTMxk-=*EUzRKr|N%7}pBL;0^bTw_1 zBe%Hkx+bIV>l9R}82Rs6egM&Qs1;5p?z6*A4esjAAS3{t#sQ}f-)FWQ_qzFB`8!uF z5eT#i#9_Acdtvdzg%b+;Iv{^L`9wO@D{Rfm=HnJn05zJP{?llsi0{!!@Bli10PTwz zG5zBnmFq=SS~Azvl^l9B6{|<_Ncp6FCBdE-Rvj`I4>y$Ce5?G{SCf(HABTcnJS#!DDpY65da6D0ZJz~kDQ?{}@t zc%-xEyJ^JyYxgA!f*?5$0H)Ny{Rg@hjeU+I z-1o2MYX3ePfypjYg-TX*cB`a?y?)JEIxg25cr+#CY1Emg?Do;+07g>WAcL z-5E(Zl7K+uP6IjeQ#d0T*%sv~($t&)iCFWTU;AJgLFG9>{-3U~@3oOqXCuf6vf>)6$1hchoPNO=x-e53U&C zjwkIUZ{gjOvxjt`?BOlG5ADgrPk)?J^U`uH78O`vkM=IzbvtC1Ms#W}Lya+$dcb>i zsS{>NjelH6J*7%KBDCS@h!HMXPv@m|Wsgm>i-5AV9{ie{x;iK#9t~|-2e5br-Ylv zTIFnDc{^qDpvy8rsXJSs>+IzPe+h{ng}{Atq8uxKeKn2X(v5GgaT|!P$FdnyB4FGf zax3z!#FYiKJ@B2t*l+Xrvy09Rcp>N8w>5j2T!zK1G?LiqA|(?DyQHenas*}BcsYUx zsi+2Ap2ev4= zxN}b}EiH||LqJL@^FE!#U75BYiy?AN4?~6zwV^Cj2JIJvF<&Q-Fcw@ieAj)4jqBKg z@{oe?Oh5e7o3j+c&r(4r=_1#vxXtqDB$H(Oj)@!~2M&^C0}-GR5UdC+w)tdBpfdXA zQ0zj#G3$y82AdsqKJ}iLee?tZ@>d0uA$JLo@;H>FbulbC*x-7hHPW4J4*JJzSMln> zeC$XZq8{7PgUERdPqOGG0l-y=e&gZAzX*<$RsjMepGc6!14uD|H z!BHvu^$sTAIR!<0H{hRf43RJX4Q5ISnSk|@J!mwU6#zRJfEDKKkTi1FrX z=*2DqWd>W<%8*i(v9W)IElUARdKJ}^Mqt-72`;k@iIm+mWJn`ioEf*%Ky!gm>Bcw( z8`gw^_)Hsyh1Auh2v?VqoI83xLJ?3jjM$27rNnWWWaymuP67U->VI7so_JXtq0^`d zP1Jz`uHuB3Q1qF!bML_6b=xX5j#cW0`Qj#jb4`L6Y5wSBM;Zy#5!sBUz8$Sswhqko z`uW9-PzErfKx;5UAATnW7lT14He`GPfP+os3AlmQhM-7*ZxGC7 zk^lOX9!9QX_J{yf9IBOZCy$HoYq(R{AGc9^GBWTNB(QJvpn`m&>3LOZ$OXz)2H0>K zZiU4%N+((j^ApSUfu*!`?nK_o=nHMuPzL6#z8=_=juuiDWuUCrWCWNOm zeGxKbNGKHZ9^zlCVuf4j2IR}&ef2O3a&l?0R$LN%3Zhg258ny{3>pBw`#?Ya6Hh|$ zmv|h~Qt^=oU#9(gQf+WXaIw6tHQkv7peI0E5+=nn5oAx}gG{Ue+?j@u*Wqhzo<|TH z3Xlih4VU4{vxxY`R`Ll{0B>xzXzb(1EI=7Cl5~QkL{DO#MF3*M1`;LOM1N(YEQ)4W z$UGePKjqRo*3ltAUxG15N3K(Te$5}Ol7W00bT_A{9rqX9`nkT`5>k=jmH~Rfn3a=g z8i6h$3pLQfKu>?aQ7}Kwaug_g?)hM=*hX8&Ncl;Jbl8wvTL>ZlHLmmd`|JvH>!9v% zbFRy8VSo=MvWOi3aSK6gH80`x5~HVMK42!m0)JQYR# zHB0)Q({f#?n)~rT+WX9NzH5EbdZ99XhRnCHYkH8)o+5U@vohNrg zScS{ZhDnW`R|Xb#-qKbHZa>tFuU{%`FX(E};{H6n}TZ6c4> za*vul+_{)Ro6)V#es8QZ2*r!jJ5iA6od=nM2-ddHo4M^uU{x818K@L(eNsC-xcTwN z^Z~{8A%0v#6Of15s{)?`svYP$(H9ke4OzFLl=y|q2z3MDx~waH{0f@681ih6pAP+N zo{3#053@GRU1%wK63*=_tMF6igC2VJl6)Y>^v_<@%>V3u6=ViPf9$m;4Z+djSqP?Q z_c$|9r}SI=lyTK8*}*om0KT8($*?`|P%BjZd)D20PR&im$AYzMqw9i{Oq1WRc{8p2 zKnU0JtI{Ey!*-sEnH2IpFV{}JK4AFZcqde#<1UV&FYgCI1s#UI{}e2i>!Ld&g5Mw} ziN>71uMzg&gedXfO%Ro&a=-3RCbd~s%pKe>7^@@2>%*C4La zJ|g|3bs@TAV666&q;DY#@3-Ok*$vGRF(vW)(+t&=fmwQ$z2&<%b1(7r0~nW`T|lBt z@0MoZ#;ae~?UlN}QLtOgiV4u{4*0w~akTz;c#hb2?3H4EpV+8{$6t;uZ`aX{k9o56 zP#}Xt&>*A}caqC>8wC7!o&rY||L-f2xYqxZN5Vd diff --git a/erc7824-docs/static/img/logo.svg b/erc7824-docs/static/img/logo.svg deleted file mode 100644 index 57826ed27..000000000 --- a/erc7824-docs/static/img/logo.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/erc7824-docs/static/img/logo_dark.svg b/erc7824-docs/static/img/logo_dark.svg deleted file mode 100644 index f921bf8f8..000000000 --- a/erc7824-docs/static/img/logo_dark.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/erc7824-docs/static/img/undraw_docusaurus_mountain.svg b/erc7824-docs/static/img/undraw_docusaurus_mountain.svg deleted file mode 100644 index af961c49a..000000000 --- a/erc7824-docs/static/img/undraw_docusaurus_mountain.svg +++ /dev/null @@ -1,171 +0,0 @@ - - Easy to Use - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/erc7824-docs/static/img/undraw_docusaurus_react.svg b/erc7824-docs/static/img/undraw_docusaurus_react.svg deleted file mode 100644 index 94b5cf08f..000000000 --- a/erc7824-docs/static/img/undraw_docusaurus_react.svg +++ /dev/null @@ -1,170 +0,0 @@ - - Powered by React - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/erc7824-docs/static/img/undraw_docusaurus_tree.svg b/erc7824-docs/static/img/undraw_docusaurus_tree.svg deleted file mode 100644 index d9161d339..000000000 --- a/erc7824-docs/static/img/undraw_docusaurus_tree.svg +++ /dev/null @@ -1,40 +0,0 @@ - - Focus on What Matters - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/go.mod b/go.mod index 79cccd3e5..29b0bf057 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/erc7824/nitrolite +module github.com/layer-3/nitrolite go 1.25.0 diff --git a/pkg/app/app_session_v1.go b/pkg/app/app_session_v1.go index 74b771fca..f7d3a500d 100644 --- a/pkg/app/app_session_v1.go +++ b/pkg/app/app_session_v1.go @@ -4,10 +4,10 @@ import ( "fmt" "time" - "github.com/erc7824/nitrolite/pkg/core" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/nitrolite/pkg/core" "github.com/shopspring/decimal" ) diff --git a/pkg/app/session_key_v1.go b/pkg/app/session_key_v1.go index 1d6ef2f20..15db89adf 100644 --- a/pkg/app/session_key_v1.go +++ b/pkg/app/session_key_v1.go @@ -5,10 +5,10 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/sign" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/nitrolite/pkg/sign" ) // AppSessionKeyStateV1 represents the state of a session key. diff --git a/pkg/app/session_key_v1_test.go b/pkg/app/session_key_v1_test.go index 070dba55d..b01748b24 100644 --- a/pkg/app/session_key_v1_test.go +++ b/pkg/app/session_key_v1_test.go @@ -5,9 +5,9 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/sign" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/nitrolite/pkg/sign" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/blockchain/evm/client.go b/pkg/blockchain/evm/client.go index d1cbc71db..1620b2c97 100644 --- a/pkg/blockchain/evm/client.go +++ b/pkg/blockchain/evm/client.go @@ -10,8 +10,8 @@ import ( "github.com/pkg/errors" "github.com/shopspring/decimal" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/sign" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/sign" ) var _ core.Client = &Client{} diff --git a/pkg/blockchain/evm/client_test.go b/pkg/blockchain/evm/client_test.go index 691129fed..98a7d479b 100644 --- a/pkg/blockchain/evm/client_test.go +++ b/pkg/blockchain/evm/client_test.go @@ -5,10 +5,10 @@ import ( "strings" "testing" - "github.com/erc7824/nitrolite/pkg/sign" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/nitrolite/pkg/sign" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" diff --git a/pkg/blockchain/evm/interface.go b/pkg/blockchain/evm/interface.go index 84810ec7a..662e00ba2 100644 --- a/pkg/blockchain/evm/interface.go +++ b/pkg/blockchain/evm/interface.go @@ -3,10 +3,10 @@ package evm import ( "context" - "github.com/erc7824/nitrolite/pkg/core" ethereum "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/core/types" + "github.com/layer-3/nitrolite/pkg/core" ) type HandleEvent func(ctx context.Context, eventLog types.Log) diff --git a/pkg/blockchain/evm/listener.go b/pkg/blockchain/evm/listener.go index 474689030..b3a4853bf 100644 --- a/pkg/blockchain/evm/listener.go +++ b/pkg/blockchain/evm/listener.go @@ -7,13 +7,13 @@ import ( "sync/atomic" "time" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/event" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" ) const ( diff --git a/pkg/blockchain/evm/listener_test.go b/pkg/blockchain/evm/listener_test.go index dd4d5a3f1..f0e339a99 100644 --- a/pkg/blockchain/evm/listener_test.go +++ b/pkg/blockchain/evm/listener_test.go @@ -8,11 +8,11 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" ethereum "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" diff --git a/pkg/blockchain/evm/reactor.go b/pkg/blockchain/evm/reactor.go index be3bce69f..f0036183a 100644 --- a/pkg/blockchain/evm/reactor.go +++ b/pkg/blockchain/evm/reactor.go @@ -10,8 +10,8 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/pkg/errors" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" ) var contractAbi *abi.ABI diff --git a/pkg/blockchain/evm/utils.go b/pkg/blockchain/evm/utils.go index dff1d8764..c118d5311 100644 --- a/pkg/blockchain/evm/utils.go +++ b/pkg/blockchain/evm/utils.go @@ -5,13 +5,13 @@ import ( "math/big" "time" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/log" - "github.com/erc7824/nitrolite/pkg/sign" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/sign" "github.com/pkg/errors" "github.com/shopspring/decimal" ) diff --git a/pkg/blockchain/evm/utils_test.go b/pkg/blockchain/evm/utils_test.go index 9e93dddda..90a487754 100644 --- a/pkg/blockchain/evm/utils_test.go +++ b/pkg/blockchain/evm/utils_test.go @@ -10,7 +10,7 @@ import ( "github.com/shopspring/decimal" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" ) // ========= hexToBytes32 Tests ========= diff --git a/pkg/core/README.md b/pkg/core/README.md index 96d3efb31..539c1b3a7 100644 --- a/pkg/core/README.md +++ b/pkg/core/README.md @@ -82,7 +82,7 @@ The `Ledger` tracks balances and "Net Flow" (total funds inflow(+)/outflow(-) of ## Usage Example: Generating IDs ```go -import "github.com/erc7824/nitrolite/core" +import "github.com/layer-3/nitrolite/core" // Generate a Home Channel ID channelID, err := core.GetHomeChannelID( diff --git a/pkg/core/channel_signer.go b/pkg/core/channel_signer.go index dd6283589..36f9360b3 100644 --- a/pkg/core/channel_signer.go +++ b/pkg/core/channel_signer.go @@ -4,9 +4,9 @@ import ( "fmt" "strings" - "github.com/erc7824/nitrolite/pkg/sign" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/layer-3/nitrolite/pkg/sign" ) type ChannelSignerType uint8 diff --git a/pkg/core/session_key.go b/pkg/core/session_key.go index 39184c591..0f0b91468 100644 --- a/pkg/core/session_key.go +++ b/pkg/core/session_key.go @@ -6,11 +6,11 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/sign" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/nitrolite/pkg/sign" ) // ChannelSessionKeyStateV1 represents the state of a session key. diff --git a/pkg/core/session_key_test.go b/pkg/core/session_key_test.go index f7d89442e..c21d999d1 100644 --- a/pkg/core/session_key_test.go +++ b/pkg/core/session_key_test.go @@ -5,10 +5,10 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/sign" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/nitrolite/pkg/sign" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/log/context_test.go b/pkg/log/context_test.go index a0ef839fc..ec40058e1 100644 --- a/pkg/log/context_test.go +++ b/pkg/log/context_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/trace" - "github.com/erc7824/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/log" ) // TestContextLogger tests the context-based logger functionality. diff --git a/pkg/log/mock_logger_test.go b/pkg/log/mock_logger_test.go index 4b5e0b7d0..35b12a72d 100644 --- a/pkg/log/mock_logger_test.go +++ b/pkg/log/mock_logger_test.go @@ -1,6 +1,6 @@ package log_test -import "github.com/erc7824/nitrolite/pkg/log" +import "github.com/layer-3/nitrolite/pkg/log" var _ log.Logger = &MockLogger{} diff --git a/pkg/log/span_logger_test.go b/pkg/log/span_logger_test.go index 1cdc0850c..37d42d618 100644 --- a/pkg/log/span_logger_test.go +++ b/pkg/log/span_logger_test.go @@ -3,7 +3,7 @@ package log_test import ( "testing" - "github.com/erc7824/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/log" "github.com/stretchr/testify/assert" ) diff --git a/pkg/log/zap_logger_test.go b/pkg/log/zap_logger_test.go index 6a70c6a95..37d46da77 100644 --- a/pkg/log/zap_logger_test.go +++ b/pkg/log/zap_logger_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/log" ) // TestZapLogger comprehensively tests the ZapLogger implementation. diff --git a/pkg/rpc/README.md b/pkg/rpc/README.md index c0a6bbb0f..2b1e95ed5 100644 --- a/pkg/rpc/README.md +++ b/pkg/rpc/README.md @@ -117,7 +117,7 @@ if err := db.Save(); err != nil { ## Installation ```go -import "github.com/erc7824/nitrolite/pkg/rpc" +import "github.com/layer-3/nitrolite/pkg/rpc" ``` ## Server Usage @@ -126,8 +126,8 @@ import "github.com/erc7824/nitrolite/pkg/rpc" ```go import ( - "github.com/erc7824/nitrolite/pkg/rpc" - "github.com/erc7824/nitrolite/pkg/log" + "github.com/layer-3/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/log" ) // Create server configuration @@ -255,7 +255,7 @@ func rateLimitMiddleware(c *rpc.Context) { ### Quick Start ```go -import "github.com/erc7824/nitrolite/pkg/rpc" +import "github.com/layer-3/nitrolite/pkg/rpc" // Create client dialer := rpc.NewWebsocketDialer(rpc.DefaultWebsocketDialerConfig) @@ -597,4 +597,4 @@ Test coverage includes: ## See Also - [API Documentation](api.yaml) - OpenAPI specification for V1 API -- Package documentation: `go doc github.com/erc7824/nitrolite/pkg/rpc` +- Package documentation: `go doc github.com/layer-3/nitrolite/pkg/rpc` diff --git a/pkg/rpc/api.go b/pkg/rpc/api.go index 5bb5f8194..72b364191 100644 --- a/pkg/rpc/api.go +++ b/pkg/rpc/api.go @@ -6,7 +6,7 @@ package rpc import ( - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" ) // ============================================================================ diff --git a/pkg/rpc/client_test.go b/pkg/rpc/client_test.go index ec5b3d319..658e35e1e 100644 --- a/pkg/rpc/client_test.go +++ b/pkg/rpc/client_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/rpc" ) // Test helpers for V1 client diff --git a/pkg/rpc/connection.go b/pkg/rpc/connection.go index c943d0044..273e01701 100644 --- a/pkg/rpc/connection.go +++ b/pkg/rpc/connection.go @@ -7,8 +7,8 @@ import ( "sync" "time" - "github.com/erc7824/nitrolite/pkg/log" "github.com/gorilla/websocket" + "github.com/layer-3/nitrolite/pkg/log" ) // Default values are carefully chosen to balance resource consumption and operational flexibility. diff --git a/pkg/rpc/connection_test.go b/pkg/rpc/connection_test.go index 6e45c2559..294073dc7 100644 --- a/pkg/rpc/connection_test.go +++ b/pkg/rpc/connection_test.go @@ -10,7 +10,7 @@ import ( "github.com/gorilla/websocket" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/rpc" ) func TestNewWebsocketConnection(t *testing.T) { diff --git a/pkg/rpc/dialer.go b/pkg/rpc/dialer.go index 118d98b7e..9adaf61f7 100644 --- a/pkg/rpc/dialer.go +++ b/pkg/rpc/dialer.go @@ -8,8 +8,8 @@ import ( "sync" "time" - "github.com/erc7824/nitrolite/pkg/log" "github.com/gorilla/websocket" + "github.com/layer-3/nitrolite/pkg/log" ) // Dialer is the interface for RPC client connections. diff --git a/pkg/rpc/dialer_test.go b/pkg/rpc/dialer_test.go index c28d9a554..20df92851 100644 --- a/pkg/rpc/dialer_test.go +++ b/pkg/rpc/dialer_test.go @@ -9,8 +9,8 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/rpc" "github.com/gorilla/websocket" + "github.com/layer-3/nitrolite/pkg/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/rpc/doc.go b/pkg/rpc/doc.go index c565b834c..67813e091 100644 --- a/pkg/rpc/doc.go +++ b/pkg/rpc/doc.go @@ -346,5 +346,5 @@ // // Run tests with: // -// go test github.com/erc7824/nitrolite/pkg/rpc +// go test github.com/layer-3/nitrolite/pkg/rpc package rpc diff --git a/pkg/rpc/mock_dialer_test.go b/pkg/rpc/mock_dialer_test.go index b19771fbf..b56a3b1c4 100644 --- a/pkg/rpc/mock_dialer_test.go +++ b/pkg/rpc/mock_dialer_test.go @@ -3,7 +3,7 @@ package rpc_test import ( "context" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/rpc" ) // MockCallHandler is a function type that handles RPC calls in the mock dialer. diff --git a/pkg/rpc/node.go b/pkg/rpc/node.go index 2679ab0dc..10202c559 100644 --- a/pkg/rpc/node.go +++ b/pkg/rpc/node.go @@ -8,9 +8,9 @@ import ( "sync" "time" - "github.com/erc7824/nitrolite/pkg/log" "github.com/google/uuid" "github.com/gorilla/websocket" + "github.com/layer-3/nitrolite/pkg/log" ) const ( diff --git a/pkg/rpc/payload_test.go b/pkg/rpc/payload_test.go index 3de85421c..3447f71f7 100644 --- a/pkg/rpc/payload_test.go +++ b/pkg/rpc/payload_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/rpc/types.go b/pkg/rpc/types.go index 685012d69..3e054584e 100644 --- a/pkg/rpc/types.go +++ b/pkg/rpc/types.go @@ -6,8 +6,8 @@ package rpc import ( "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" ) // ============================================================================ diff --git a/pkg/sign/README.md b/pkg/sign/README.md index 09eb52ae5..07a1a919e 100644 --- a/pkg/sign/README.md +++ b/pkg/sign/README.md @@ -17,4 +17,4 @@ This library separates generic interfaces from specific blockchain implementatio See the Go package documentation and examples by running: ```bash -go doc -all github.com/erc7824/nitrolite/pkg/sign \ No newline at end of file +go doc -all github.com/layer-3/nitrolite/pkg/sign \ No newline at end of file diff --git a/pkg/sign/example_test.go b/pkg/sign/example_test.go index f36ecdaa3..775dfc33c 100644 --- a/pkg/sign/example_test.go +++ b/pkg/sign/example_test.go @@ -4,8 +4,8 @@ import ( "fmt" "log" - "github.com/erc7824/nitrolite/pkg/sign" ethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/nitrolite/pkg/sign" ) // ExampleNewEthereumRawSigner demonstrates creating an Ethereum signer and signing a message. diff --git a/pkg/sign/kms/gcp/gcp_test.go b/pkg/sign/kms/gcp/gcp_test.go index b55f80cc2..4606e4a19 100644 --- a/pkg/sign/kms/gcp/gcp_test.go +++ b/pkg/sign/kms/gcp/gcp_test.go @@ -21,7 +21,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/pkg/sign" + "github.com/layer-3/nitrolite/pkg/sign" ) const envKMSKeyName = "GCP_KMS_KEY_NAME" diff --git a/pkg/sign/kms/gcp/signer.go b/pkg/sign/kms/gcp/signer.go index 439585299..1ac9699fd 100644 --- a/pkg/sign/kms/gcp/signer.go +++ b/pkg/sign/kms/gcp/signer.go @@ -21,8 +21,8 @@ import ( "github.com/ethereum/go-ethereum/crypto/secp256k1" - "github.com/erc7824/nitrolite/pkg/sign" - kmssign "github.com/erc7824/nitrolite/pkg/sign/kms" + "github.com/layer-3/nitrolite/pkg/sign" + kmssign "github.com/layer-3/nitrolite/pkg/sign/kms" ) // crc32cTable is pre-computed once to avoid re-creating it on every Sign() call. diff --git a/pkg/sign/kms/gcp/signer_test.go b/pkg/sign/kms/gcp/signer_test.go index f373ebc40..98981803a 100644 --- a/pkg/sign/kms/gcp/signer_test.go +++ b/pkg/sign/kms/gcp/signer_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/erc7824/nitrolite/pkg/sign" + "github.com/layer-3/nitrolite/pkg/sign" ) // buildSecp256k1PEM creates a PEM-encoded SubjectPublicKeyInfo for a secp256k1 key, diff --git a/sdk/go/README.md b/sdk/go/README.md index 620c8c423..27421c63b 100644 --- a/sdk/go/README.md +++ b/sdk/go/README.md @@ -1,4 +1,4 @@ -[![Go Reference](https://pkg.go.dev/badge/github.com/erc7824/nitrolite/sdk/go.svg)](https://pkg.go.dev/github.com/erc7824/nitrolite/sdk/go) +[![Go Reference](https://pkg.go.dev/badge/github.com/layer-3/nitrolite/sdk/go.svg)](https://pkg.go.dev/github.com/layer-3/nitrolite/sdk/go) # Clearnode Go SDK @@ -95,9 +95,9 @@ package main import ( "context" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/sign" - sdk "github.com/erc7824/nitrolite/sdk/go" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/sign" + sdk "github.com/layer-3/nitrolite/sdk/go" "github.com/shopspring/decimal" ) diff --git a/sdk/go/app_registry.go b/sdk/go/app_registry.go index be38dc51c..baa3e8e6f 100644 --- a/sdk/go/app_registry.go +++ b/sdk/go/app_registry.go @@ -6,10 +6,10 @@ import ( "strconv" "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" - "github.com/erc7824/nitrolite/pkg/sign" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/sign" ) // ============================================================================ diff --git a/sdk/go/app_session.go b/sdk/go/app_session.go index a9dd23937..e4f56e108 100644 --- a/sdk/go/app_session.go +++ b/sdk/go/app_session.go @@ -4,10 +4,10 @@ import ( "context" "fmt" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" "github.com/shopspring/decimal" ) diff --git a/sdk/go/asset_cache.go b/sdk/go/asset_cache.go index c077ced83..4b710dd81 100644 --- a/sdk/go/asset_cache.go +++ b/sdk/go/asset_cache.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/erc7824/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/core" ) // clientAssetStore implements core.AssetStore by fetching data from the Clearnode API. diff --git a/sdk/go/asset_cache_test.go b/sdk/go/asset_cache_test.go index 8547f166c..465f97410 100644 --- a/sdk/go/asset_cache_test.go +++ b/sdk/go/asset_cache_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/sdk/go/channel.go b/sdk/go/channel.go index e7fb13987..bf7a6ea68 100644 --- a/sdk/go/channel.go +++ b/sdk/go/channel.go @@ -4,10 +4,10 @@ import ( "context" "fmt" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" - "github.com/erc7824/nitrolite/pkg/sign" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/sign" "github.com/shopspring/decimal" ) diff --git a/sdk/go/client.go b/sdk/go/client.go index 9aaf16e2f..bb4e5de74 100644 --- a/sdk/go/client.go +++ b/sdk/go/client.go @@ -7,13 +7,13 @@ import ( "sync" "time" - "github.com/erc7824/nitrolite/pkg/blockchain/evm" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" - "github.com/erc7824/nitrolite/pkg/sign" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/ethclient" + "github.com/layer-3/nitrolite/pkg/blockchain/evm" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/sign" ) // Client provides a unified interface for interacting with Clearnode. diff --git a/sdk/go/client_test.go b/sdk/go/client_test.go index 6b7fdcc24..39e135f9b 100644 --- a/sdk/go/client_test.go +++ b/sdk/go/client_test.go @@ -6,12 +6,12 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" - "github.com/erc7824/nitrolite/pkg/sign" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/sign" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/sdk/go/doc.go b/sdk/go/doc.go index 517e6e433..cbef37553 100644 --- a/sdk/go/doc.go +++ b/sdk/go/doc.go @@ -29,8 +29,8 @@ // "log" // "time" // -// "github.com/erc7824/nitrolite/pkg/sign" -// sdk "github.com/erc7824/nitrolite/sdk/go" +// "github.com/layer-3/nitrolite/pkg/sign" +// sdk "github.com/layer-3/nitrolite/sdk/go" // "github.com/shopspring/decimal" // ) // diff --git a/sdk/go/examples/app_sessions/lifecycle.go b/sdk/go/examples/app_sessions/lifecycle.go index c70f160ae..78dadb041 100644 --- a/sdk/go/examples/app_sessions/lifecycle.go +++ b/sdk/go/examples/app_sessions/lifecycle.go @@ -22,10 +22,10 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/shopspring/decimal" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/sign" - sdk "github.com/erc7824/nitrolite/sdk/go" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/sign" + sdk "github.com/layer-3/nitrolite/sdk/go" ) func main() { diff --git a/sdk/go/examples/challenge/main.go b/sdk/go/examples/challenge/main.go index 24735d9d2..4d128df28 100644 --- a/sdk/go/examples/challenge/main.go +++ b/sdk/go/examples/challenge/main.go @@ -20,9 +20,9 @@ import ( "github.com/ethereum/go-ethereum/crypto" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/sign" - sdk "github.com/erc7824/nitrolite/sdk/go" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/sign" + sdk "github.com/layer-3/nitrolite/sdk/go" "github.com/shopspring/decimal" ) diff --git a/sdk/go/mock_dialer_test.go b/sdk/go/mock_dialer_test.go index c00114438..ef45d6933 100644 --- a/sdk/go/mock_dialer_test.go +++ b/sdk/go/mock_dialer_test.go @@ -5,7 +5,7 @@ import ( "fmt" "sync" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/rpc" ) type MockDialer struct { diff --git a/sdk/go/node.go b/sdk/go/node.go index a7f5dda77..3f199eaf4 100644 --- a/sdk/go/node.go +++ b/sdk/go/node.go @@ -5,8 +5,8 @@ import ( "fmt" "strconv" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) // ============================================================================ diff --git a/sdk/go/user.go b/sdk/go/user.go index 75850c22c..6d53b7144 100644 --- a/sdk/go/user.go +++ b/sdk/go/user.go @@ -5,8 +5,8 @@ import ( "fmt" "strconv" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" ) // ============================================================================ diff --git a/sdk/go/utils.go b/sdk/go/utils.go index fcb218f77..faf992127 100644 --- a/sdk/go/utils.go +++ b/sdk/go/utils.go @@ -6,9 +6,9 @@ import ( "strings" "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" "github.com/shopspring/decimal" ) diff --git a/sdk/go/utils_test.go b/sdk/go/utils_test.go index 1bd46d32e..71d8a0a96 100644 --- a/sdk/go/utils_test.go +++ b/sdk/go/utils_test.go @@ -4,9 +4,9 @@ import ( "testing" "time" - "github.com/erc7824/nitrolite/pkg/app" - "github.com/erc7824/nitrolite/pkg/core" - "github.com/erc7824/nitrolite/pkg/rpc" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/rpc" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/sdk/ts-compat/README.md b/sdk/ts-compat/README.md index 52b990998..9bcb93445 100644 --- a/sdk/ts-compat/README.md +++ b/sdk/ts-compat/README.md @@ -1,6 +1,6 @@ # Nitrolite Compat SDK -[![License](https://img.shields.io/npm/l/@yellow-org/sdk-compat.svg)](https://github.com/erc7824/nitrolite/blob/main/LICENSE) +[![License](https://img.shields.io/npm/l/@yellow-org/sdk-compat.svg)](https://github.com/layer-3/nitrolite/blob/main/LICENSE) Compatibility layer that bridges the Nitrolite SDK **v0.5.3 API** to the **v1.0.0 runtime**, letting existing dApps upgrade to the new protocol with minimal code changes. diff --git a/sdk/ts-compat/docs/migration-offchain.md b/sdk/ts-compat/docs/migration-offchain.md index 5744920ae..0e84bb423 100644 --- a/sdk/ts-compat/docs/migration-offchain.md +++ b/sdk/ts-compat/docs/migration-offchain.md @@ -57,7 +57,7 @@ await sendRequest(msg); v0.5.3 used WebSocket push events (`ChannelUpdate`, `BalanceUpdate`). v1.0.0 uses polling. The compat layer provides `EventPoller`: ```typescript -import { EventPoller } from '@erc7824/nitrolite-compat'; +import { EventPoller } from '@layer-3/nitrolite-compat'; const poller = new EventPoller(client, { onChannelUpdate: (channels) => updateUI(channels), diff --git a/sdk/ts-compat/docs/migration-overview.md b/sdk/ts-compat/docs/migration-overview.md index 614451b0c..08328d465 100644 --- a/sdk/ts-compat/docs/migration-overview.md +++ b/sdk/ts-compat/docs/migration-overview.md @@ -11,17 +11,17 @@ The compat layer centralises this complexity into **~5 file changes** per app. I ## 2. Installation ```bash -npm install @erc7824/nitrolite-compat +npm install @layer-3/nitrolite-compat # Peer dependencies -npm install @erc7824/nitrolite viem +npm install @layer-3/nitrolite viem ``` ## 3. Import Swap | Before (v0.5.3) | After (compat) | |-----------------|----------------| -| `import { createGetChannelsMessage, parseGetChannelsResponse } from '@erc7824/nitrolite'` | `import { NitroliteClient } from '@erc7824/nitrolite-compat'` | -| Types: `AppSession`, `LedgerChannel`, `RPCAppDefinition` | Same types — re-exported from `@erc7824/nitrolite-compat` | +| `import { createGetChannelsMessage, parseGetChannelsResponse } from '@layer-3/nitrolite'` | `import { NitroliteClient } from '@layer-3/nitrolite-compat'` | +| Types: `AppSession`, `LedgerChannel`, `RPCAppDefinition` | Same types — re-exported from `@layer-3/nitrolite-compat` | For **types**, just change the package name. For **functions**, switch to client methods instead of `create*Message` / `parse*Response`. @@ -62,7 +62,7 @@ const channels = await client.getChannels(); ## 7. Quick Start Example ```typescript -import { NitroliteClient, WalletStateSigner, blockchainRPCsFromEnv } from '@erc7824/nitrolite-compat'; +import { NitroliteClient, WalletStateSigner, blockchainRPCsFromEnv } from '@layer-3/nitrolite-compat'; // Create client (replaces new Client(ws, signer)) const client = await NitroliteClient.create({ diff --git a/sdk/ts-compat/examples/lifecycle.ts b/sdk/ts-compat/examples/lifecycle.ts index 448e4fd92..a97d47fa4 100644 --- a/sdk/ts-compat/examples/lifecycle.ts +++ b/sdk/ts-compat/examples/lifecycle.ts @@ -1,7 +1,7 @@ /** * Compat Layer Comprehensive Lifecycle Example * - * Exercises every public API offered by @erc7824/nitrolite-compat: + * Exercises every public API offered by @layer-3/nitrolite-compat: * - NitroliteClient: creation, ping, config, assets, balances, channels, * transfers, app sessions (create/submit/close), asset helpers, error classification * - EventPoller: start/stop with callbacks @@ -574,7 +574,7 @@ async function main() { RPCMethod.CloseAppSession, RPCMethod.SubmitAppState, RPCMethod.GetAppDefinition, RPCMethod.AuthRequest, RPCMethod.AuthChallenge, RPCMethod.AuthVerify, - RPCMethod.GetLedgerTransactions, RPCMethod.GetUserTag, + RPCMethod.GetLedgerTransactions, ]; ok('RPCMethod enum', `${methods.length} methods accessible`); } catch (e: any) { fail('RPCMethod enum', e); } diff --git a/sdk/ts-compat/examples/tsconfig.json b/sdk/ts-compat/examples/tsconfig.json index 27e0e3a03..389390d18 100644 --- a/sdk/ts-compat/examples/tsconfig.json +++ b/sdk/ts-compat/examples/tsconfig.json @@ -7,8 +7,8 @@ "skipLibCheck": true, "strict": true, "paths": { - "@erc7824/nitrolite": ["../../ts/src/index.ts"], - "@erc7824/nitrolite-compat": ["../src/index.ts"] + "@layer-3/nitrolite": ["../../ts/src/index.ts"], + "@layer-3/nitrolite-compat": ["../src/index.ts"] } }, "include": ["./*.ts"] diff --git a/sdk/ts-compat/package.json b/sdk/ts-compat/package.json index 0e59db3c3..835ddde0d 100644 --- a/sdk/ts-compat/package.json +++ b/sdk/ts-compat/package.json @@ -18,7 +18,7 @@ }, "keywords": [ "yellow", - "erc7824", + "layer-3", "nitrolite", "compat", "migration", @@ -28,7 +28,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/erc7824/nitrolite.git", + "url": "https://github.com/layer-3/nitrolite.git", "directory": "sdk/ts-compat" }, "engines": { diff --git a/sdk/ts-compat/src/index.ts b/sdk/ts-compat/src/index.ts index 9c6d1729c..7d6f49e5a 100644 --- a/sdk/ts-compat/src/index.ts +++ b/sdk/ts-compat/src/index.ts @@ -1,7 +1,7 @@ // ============================================================================= -// @erc7824/nitrolite-compat barrel export +// @layer-3/nitrolite-compat barrel export // -// Re-exports everything apps previously imported from '@erc7824/nitrolite' +// Re-exports everything apps previously imported from '@layer-3/nitrolite' // (v0.5.3) but backed by the v1.0.0 SDK. // ============================================================================= @@ -133,7 +133,7 @@ export { EventPoller, type EventPollerCallbacks } from './events'; export { buildClientOptions, blockchainRPCsFromEnv, type CompatClientConfig } from './config'; // NOTE: SDK classes (Client, ChannelDefaultSigner, etc.) are intentionally NOT -// re-exported here. Barrel re-exports from '@erc7824/nitrolite' trigger eager +// re-exported here. Barrel re-exports from '@layer-3/nitrolite' trigger eager // module evaluation of the full SDK, which has side effects that throw during // SSR / module-load time. Apps needing those classes should import directly -// from '@erc7824/nitrolite'. +// from '@layer-3/nitrolite'. diff --git a/sdk/ts-compat/src/types.ts b/sdk/ts-compat/src/types.ts index e895d0038..ed207a80b 100644 --- a/sdk/ts-compat/src/types.ts +++ b/sdk/ts-compat/src/types.ts @@ -29,7 +29,6 @@ export enum RPCMethod { AuthVerify = 'auth_verify', Error = 'error', GetLedgerTransactions = 'get_ledger_transactions', - GetUserTag = 'get_user_tag', TransferNotification = 'tr', } diff --git a/sdk/ts/README.md b/sdk/ts/README.md index 485c4e60f..f2a001f1e 100644 --- a/sdk/ts/README.md +++ b/sdk/ts/README.md @@ -1,7 +1,7 @@ # Yellow TypeScript SDK [![npm version](https://img.shields.io/npm/v/@yellow-org/sdk.svg)](https://www.npmjs.com/package/@yellow-org/sdk) -[![License](https://img.shields.io/npm/l/@yellow-org/sdk.svg)](https://github.com/erc7824/nitrolite/blob/main/LICENSE) +[![License](https://img.shields.io/npm/l/@yellow-org/sdk.svg)](https://github.com/layer-3/nitrolite/blob/main/LICENSE) TypeScript SDK for Clearnode payment channels providing both high-level and low-level operations in a unified client: - **State Operations**: `deposit()`, `withdraw()`, `transfer()`, `closeHomeChannel()`, `acknowledge()` - build and co-sign states off-chain @@ -9,7 +9,7 @@ TypeScript SDK for Clearnode payment channels providing both high-level and low- - **Low-Level Operations**: Direct RPC access for custom flows and advanced use cases - **Full Feature Parity**: 100% compatibility with Go SDK functionality -> If you are migrating from `@erc7824/nitrolite@v0.5.3`, please consider using the [@yellow-org/sdk-compat](https://www.npmjs.com/package/@yellow-org/sdk-compat) package. It is a translation layer that uses this SDK underneath and maps the familiar v0.5.3 API surfaces to this SDK. +> If you are migrating from `@layer-3/nitrolite@v0.5.3`, please consider using the [@yellow-org/sdk-compat](https://www.npmjs.com/package/@yellow-org/sdk-compat) package. It is a translation layer that uses this SDK underneath and maps the familiar v0.5.3 API surfaces to this SDK. ## Method Cheat Sheet @@ -1000,8 +1000,8 @@ Part of the Nitrolite project. See [LICENSE](../../LICENSE) for details. ## Related Projects - [Nitrolite TS Compat](https://www.npmjs.com/package/@yellow-org/sdk-compat) - Compatibility layer for older TypeScript versions -- [Nitrolite Go SDK](https://github.com/erc7824/nitrolite/tree/stable/sdk/go) - Go implementation with same API -- [Nitrolite Smart Contracts](https://github.com/erc7824/nitrolite/tree/stable/contracts) - On-chain contracts +- [Nitrolite Go SDK](https://github.com/layer-3/nitrolite/tree/stable/sdk/go) - Go implementation with same API +- [Nitrolite Smart Contracts](https://github.com/layer-3/nitrolite/tree/stable/contracts) - On-chain contracts --- diff --git a/sdk/ts/docs/TYPECHAIN_AUTOMATION.md b/sdk/ts/docs/TYPECHAIN_AUTOMATION.md index 51b89426e..e7c53b201 100644 --- a/sdk/ts/docs/TYPECHAIN_AUTOMATION.md +++ b/sdk/ts/docs/TYPECHAIN_AUTOMATION.md @@ -55,7 +55,7 @@ export default defineConfig({ ### 3. Usage in Code ```typescript -import { custodyAbi } from '@erc7824/nitrolite'; +import { custodyAbi } from '@layer-3/nitrolite'; // ✅ Full type safety and autocomplete const result = await publicClient.readContract({ diff --git a/sdk/ts/examples/app_sessions/package-lock.json b/sdk/ts/examples/app_sessions/package-lock.json index 91846bfde..1e0eef30d 100644 --- a/sdk/ts/examples/app_sessions/package-lock.json +++ b/sdk/ts/examples/app_sessions/package-lock.json @@ -19,7 +19,7 @@ } }, "../..": { - "name": "@erc7824/nitrolite", + "name": "@layer-3/nitrolite", "version": "1.1.0", "license": "MIT", "dependencies": { diff --git a/sdk/ts/examples/example-app/README.md b/sdk/ts/examples/example-app/README.md index a4db7e15d..e4bfe9b62 100644 --- a/sdk/ts/examples/example-app/README.md +++ b/sdk/ts/examples/example-app/README.md @@ -1,6 +1,6 @@ # Nitrolite SDK Integration Guide -This example app demonstrates how to integrate the `@erc7824/nitrolite` TypeScript SDK into a React application. Use it as a reference for building your own Yellow-Network-powered app. +This example app demonstrates how to integrate the `@layer-3/nitrolite` TypeScript SDK into a React application. Use it as a reference for building your own Yellow-Network-powered app. ## Quick Start @@ -14,10 +14,10 @@ Opens at `http://localhost:3000`. Requires MetaMask. ## Install the SDK ```bash -npm install @erc7824/nitrolite viem decimal.js +npm install @layer-3/nitrolite viem decimal.js ``` -- **`@erc7824/nitrolite`** — the state channel SDK +- **`@layer-3/nitrolite`** — the state channel SDK - **`viem`** — Ethereum wallet and signing primitives - **`decimal.js`** — precise decimal arithmetic for token amounts @@ -85,7 +85,7 @@ import { Client, withBlockchainRPC, ChannelDefaultSigner, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; import { createWalletClient, custom } from 'viem'; import { mainnet } from 'viem/chains'; @@ -233,7 +233,7 @@ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { ChannelSessionKeyStateSigner, getChannelSessionKeyAuthMetadataHashV1, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; // 1. Generate a temporary key pair const privateKey = generatePrivateKey(); @@ -290,7 +290,7 @@ Now `sessionClient` signs off-chain state updates with the session key — no wa Submit a new version with empty assets to revoke a session key: ```ts -import { packChannelKeyStateV1 } from '@erc7824/nitrolite'; +import { packChannelKeyStateV1 } from '@layer-3/nitrolite'; const existing = await client.getLastChannelKeyStates(address, sessionKeyAddress); const latest = existing[0]; @@ -328,7 +328,7 @@ import { withHandshakeTimeout, withPingInterval, withErrorHandler, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; const client = await Client.create( wsUrl, @@ -362,6 +362,5 @@ src/ ## Resources -- [Nitrolite Documentation](https://erc7824.org/quick_start) -- [GitHub Repository](https://github.com/erc7824/nitrolite) +- [GitHub Repository](https://github.com/layer-3/nitrolite) - [Viem Documentation](https://viem.sh) diff --git a/sdk/ts/examples/example-app/src/App.tsx b/sdk/ts/examples/example-app/src/App.tsx index c419e7ed1..db3f2834c 100644 --- a/sdk/ts/examples/example-app/src/App.tsx +++ b/sdk/ts/examples/example-app/src/App.tsx @@ -385,7 +385,7 @@ function App() {
Yellow SDK v1.0.0 =20.0.0" diff --git a/sdk/ts/scripts/generate-docs.ts b/sdk/ts/scripts/generate-docs.ts index 3fd29d618..2c0b807eb 100644 --- a/sdk/ts/scripts/generate-docs.ts +++ b/sdk/ts/scripts/generate-docs.ts @@ -656,7 +656,7 @@ function generateContractDocs(contract: ContractInfo): string { docs += '## Type Safety\n\n'; docs += 'This contract is fully type-safe when used with the generated TypeScript types:\n\n'; docs += '```typescript\n'; - docs += `import { ${contract.name.toLowerCase()}Abi } from '@erc7824/nitrolite';\n\n`; + docs += `import { ${contract.name.toLowerCase()}Abi } from '@layer-3/nitrolite';\n\n`; docs += `// Full type safety with autocomplete\n`; docs += `const result = await publicClient.readContract({\n`; docs += ` address: contractAddress,\n`; @@ -680,11 +680,11 @@ function generateSDKOverview(contracts: ContractInfo[]): string { docs += '## Quick Start\n\n'; docs += '```bash\n'; - docs += 'npm install @erc7824/nitrolite\n'; + docs += 'npm install @layer-3/nitrolite\n'; docs += '```\n\n'; docs += '```typescript\n'; - docs += "import { custodyAbi, NitroliteClient } from '@erc7824/nitrolite';\n\n"; + docs += "import { custodyAbi, NitroliteClient } from '@layer-3/nitrolite';\n\n"; docs += '// Initialize client with full type safety\n'; docs += 'const client = new NitroliteClient({...config});\n\n'; docs += '// Deposit funds for state channels\n'; diff --git a/sdk/ts/scripts/generate-example-tutorials.ts b/sdk/ts/scripts/generate-example-tutorials.ts index 802224266..6261d6d96 100644 --- a/sdk/ts/scripts/generate-example-tutorials.ts +++ b/sdk/ts/scripts/generate-example-tutorials.ts @@ -509,12 +509,11 @@ class ExampleTutorialGenerator { markdown += `## Next Steps\n\n`; markdown += `Congratulations! You've successfully built a ${tutorial.title.toLowerCase()}.\n\n`; markdown += `### Continue Learning\n\n`; - markdown += `- Continue with the [ERC-7824 Quick Start Guide](https://erc7824.org/quick_start/) for advanced features\n`; markdown += `- Explore the [SDK documentation](../README.md)\n`; markdown += `- Check out more [Examples](../../examples/)\n`; - markdown += `- Report issues or contribute at [GitHub](https://github.com/erc7824/nitrolite)\n\n`; + markdown += `- Report issues or contribute at [GitHub](https://github.com/layer-3/nitrolite)\n\n`; markdown += `### Improve This Tutorial\n\n`; - markdown += `Found an issue or want to improve this tutorial? [Edit on GitHub](https://github.com/erc7824/nitrolite/tree/main/examples/${tutorial.metadata.examplePath})\n\n`; + markdown += `Found an issue or want to improve this tutorial? [Edit on GitHub](https://github.com/layer-3/nitrolite/tree/main/examples/${tutorial.metadata.examplePath})\n\n`; return markdown; } diff --git a/test/integration/README.md b/test/integration/README.md index 9fbc6eab6..51b4596d6 100644 --- a/test/integration/README.md +++ b/test/integration/README.md @@ -38,7 +38,7 @@ npm ci npm run build ``` -It's possible to use specific version of the sdk by updating the `package.json` with a specific version of `@erc7824/nitrolite` and change import path in the `tsconfig.json` +It's possible to use specific version of the sdk by updating the `package.json` with a specific version of `@layer-3/nitrolite` and change import path in the `tsconfig.json` ## Running Integration Tests diff --git a/test/integration/common/auth.ts b/test/integration/common/auth.ts index 02f394f92..e8a2f2a15 100644 --- a/test/integration/common/auth.ts +++ b/test/integration/common/auth.ts @@ -4,7 +4,7 @@ import { createAuthVerifyMessage, AuthRequestParams, parseAuthChallengeResponse, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; import { Identity } from './identity'; import { getAuthChallengePredicate, getAuthVerifyPredicate, TestWebSocket } from './ws'; diff --git a/test/integration/common/blockchainUtils.ts b/test/integration/common/blockchainUtils.ts index b133841ee..79078d261 100644 --- a/test/integration/common/blockchainUtils.ts +++ b/test/integration/common/blockchainUtils.ts @@ -17,7 +17,7 @@ import { } from 'viem'; import { chain } from './setup'; import { privateKeyToAccount } from 'viem/accounts'; -import { custodyAbi } from '@erc7824/nitrolite/dist/abis/generated'; +import { custodyAbi } from '@layer-3/nitrolite/dist/abis/generated'; export class BlockchainUtils { private client = null; diff --git a/test/integration/common/databaseUtils.ts b/test/integration/common/databaseUtils.ts index bbd8f562a..5473e4726 100644 --- a/test/integration/common/databaseUtils.ts +++ b/test/integration/common/databaseUtils.ts @@ -1,6 +1,6 @@ import { Pool } from 'pg'; import { CONFIG } from './setup'; -import { createCleanupSessionKeyCacheMessage, createECDSAMessageSigner } from '@erc7824/nitrolite'; +import { createCleanupSessionKeyCacheMessage, createECDSAMessageSigner } from '@layer-3/nitrolite'; import { getCleanupSessionKeyCachePredicate, TestWebSocket } from './ws'; import { generatePrivateKey } from 'viem/accounts'; diff --git a/test/integration/common/identity.ts b/test/integration/common/identity.ts index 44c9d42b6..5a51c5cca 100644 --- a/test/integration/common/identity.ts +++ b/test/integration/common/identity.ts @@ -1,8 +1,8 @@ import { Address, createWalletClient, Hex, http } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { chain } from './setup'; -import { createECDSAMessageSigner } from '@erc7824/nitrolite'; -import { SessionKeyStateSigner } from '@erc7824/nitrolite/dist/client/signer'; +import { createECDSAMessageSigner } from '@layer-3/nitrolite'; +import { SessionKeyStateSigner } from '@layer-3/nitrolite/dist/client/signer'; export class Identity { public walletClient = null; diff --git a/test/integration/common/nitroliteClient.ts b/test/integration/common/nitroliteClient.ts index 29843095f..7d5e04d0e 100644 --- a/test/integration/common/nitroliteClient.ts +++ b/test/integration/common/nitroliteClient.ts @@ -13,7 +13,7 @@ import { RPCChannelStatus, State, StateSigner, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; import { Identity } from './identity'; import { Address, createPublicClient, Hex, http } from 'viem'; import { chain, CONFIG } from './setup'; diff --git a/test/integration/common/testAppSessionHelpers.ts b/test/integration/common/testAppSessionHelpers.ts index de6afa0b1..5a5da39cf 100644 --- a/test/integration/common/testAppSessionHelpers.ts +++ b/test/integration/common/testAppSessionHelpers.ts @@ -9,7 +9,7 @@ import { parseSubmitAppStateResponse, parseCloseAppSessionResponse, RPCChannelStatus, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; import { Hex } from 'viem'; /** diff --git a/test/integration/common/testHelpers.ts b/test/integration/common/testHelpers.ts index 2ba1594db..6c6c925ab 100644 --- a/test/integration/common/testHelpers.ts +++ b/test/integration/common/testHelpers.ts @@ -15,7 +15,7 @@ import { ResizeChannelParams, RPCChannelOperation, State, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; import { Hex } from 'viem'; export function toRaw(amount: bigint, decimals: number = 6): bigint { diff --git a/test/integration/common/testSetup.ts b/test/integration/common/testSetup.ts index e7aaf3b55..69f412397 100644 --- a/test/integration/common/testSetup.ts +++ b/test/integration/common/testSetup.ts @@ -6,7 +6,7 @@ import { createAuthSessionWithClearnode } from '@/auth'; import { createGetAppSessionsMessageV2, parseGetAppSessionsResponse, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; export interface TestSetupResult { alice: Identity; diff --git a/test/integration/common/ws.ts b/test/integration/common/ws.ts index d8df9f157..2dbe72bb3 100644 --- a/test/integration/common/ws.ts +++ b/test/integration/common/ws.ts @@ -1,4 +1,4 @@ -import { parseAnyRPCResponse, RPCChannelStatus, RPCMethod, RPCResponse } from '@erc7824/nitrolite'; +import { parseAnyRPCResponse, RPCChannelStatus, RPCMethod, RPCResponse } from '@layer-3/nitrolite'; import { WebSocket } from 'ws'; interface MessageListener { @@ -233,12 +233,6 @@ export const getGetLedgerTransactionsPredicate = () => { }; }; -export const getGetUserTagPredicate = () => { - return (data: string, reqId?: number): boolean => { - return genericPredicate(data, (r) => r.method === RPCMethod.GetUserTag, reqId); - }; -}; - export const getGetSessionKeysPredicate = () => { return (data: string, reqId?: number): boolean => { return genericPredicate(data, (r) => r.method === RPCMethod.GetSessionKeys, reqId); diff --git a/test/integration/package-lock.json b/test/integration/package-lock.json index 8b11272e0..f6eca7fa8 100644 --- a/test/integration/package-lock.json +++ b/test/integration/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "nitrolite-integration-tests", "dependencies": { - "@erc7824/nitrolite": "file:../sdk", + "@layer-3/nitrolite": "file:../sdk", "pg": "^8.17.1", "viem": "^2.44.4", "ws": "^8.19.0" @@ -21,7 +21,7 @@ } }, "../sdk": { - "name": "@erc7824/nitrolite", + "name": "@layer-3/nitrolite", "version": "0.4.0", "license": "MIT", "dependencies": { @@ -525,7 +525,7 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@erc7824/nitrolite": { + "node_modules/@layer-3/nitrolite": { "resolved": "../sdk", "link": true }, diff --git a/test/integration/package.json b/test/integration/package.json index 50efd7a0a..6e3333c00 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -6,7 +6,7 @@ "test:watch": "jest --watch --runInBand --detectOpenHandles" }, "dependencies": { - "@erc7824/nitrolite": "file:../sdk", + "@layer-3/nitrolite": "file:../sdk", "pg": "^8.17.1", "viem": "^2.44.4", "ws": "^8.19.0" diff --git a/test/integration/tests/backward_compatibility/lifecycle_nitrorpc_v02.test.ts b/test/integration/tests/backward_compatibility/lifecycle_nitrorpc_v02.test.ts deleted file mode 100644 index 19b8a308f..000000000 --- a/test/integration/tests/backward_compatibility/lifecycle_nitrorpc_v02.test.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { BlockchainUtils } from '@/blockchainUtils'; -import { DatabaseUtils } from '@/databaseUtils'; -import { Identity } from '@/identity'; -import { TestNitroliteClient } from '@/nitroliteClient'; -import { CONFIG } from '@/setup'; -import { - TestWebSocket, - getResizeChannelPredicate, - getCloseChannelPredicate, -} from '@/ws'; -import { - createCloseChannelMessage, - createResizeChannelMessage, - parseCloseChannelResponse, - parseResizeChannelResponse, - RPCProtocolVersion, - State, -} from '@erc7824/nitrolite'; -import { Hex } from 'viem'; -import { - setupTestIdentitiesAndConnections, - fetchAndParseAppSessions, -} from '@/testSetup'; -import { - createTestChannels, - authenticateAppWithAllowances, - createTestAppSession, - getLedgerBalances, - toRaw, - composeResizeChannelParams, -} from '@/testHelpers'; -import { - submitAppStateUpdate_v02, - closeAppSessionWithState, -} from '@/testAppSessionHelpers'; -import { on } from 'events'; - -describe('nitrorpc_v02 lifecycle', () => { - const ASSET_SYMBOL = CONFIG.TOKEN_SYMBOL; - - const onChainDepositAmount = BigInt(1000); - const appSessionDepositAmount = BigInt(100); - - let aliceWS: TestWebSocket; - let alice: Identity; - let aliceClient: TestNitroliteClient; - - let aliceAppWS: TestWebSocket; - let aliceAppIdentity: Identity; - - let bobWS: TestWebSocket; - let bob: Identity; - let bobAppIdentity: Identity; - let bobClient: TestNitroliteClient; - - let blockUtils: BlockchainUtils; - let databaseUtils: DatabaseUtils; - - let aliceChannelId: Hex; - let bobChannelId: Hex; - - let initialAliceState: State; - let initialBobState: State; - - let appSessionId: string; - - const GAME_TYPE = 'chess'; - const TIME_CONTROL = { initial: 600, increment: 5 }; - - const SESSION_DATA_WAITING = { - gameType: GAME_TYPE, - timeControl: TIME_CONTROL, - gameState: 'waiting', - }; - - const SESSION_DATA_ACTIVE = { - ...SESSION_DATA_WAITING, - gameState: 'active', - currentMove: 'e2e4', - moveCount: 1, - }; - - const SESSION_DATA_FINISHED = { - ...SESSION_DATA_WAITING, - gameState: 'finished', - winner: 'white', - endCondition: 'checkmate', - }; - - - beforeAll(async () => { - blockUtils = new BlockchainUtils(); - databaseUtils = new DatabaseUtils(); - - const setup = await setupTestIdentitiesAndConnections(); - alice = setup.alice; - aliceWS = setup.aliceWS; - aliceClient = setup.aliceClient; - aliceAppIdentity = setup.aliceAppIdentity; - aliceAppWS = setup.aliceAppWS; - bob = setup.bob; - bobAppIdentity = setup.bobAppIdentity; - bobWS = setup.bobWS; - bobClient = setup.bobClient; - - await blockUtils.makeSnapshot(); - }); - - afterAll(async () => { - aliceWS.close(); - aliceAppWS.close(); - bobWS.close(); - - await databaseUtils.resetClearnodeState(); - await blockUtils.resetSnapshot(); - - await databaseUtils.close(); - }); - - it('should create and init two channels', async () => { - ({channelIds: [aliceChannelId, bobChannelId], states: [initialAliceState, initialBobState]} = await createTestChannels([{client: aliceClient, ws: aliceWS}, {client: bobClient, ws: bobWS}], toRaw(onChainDepositAmount))); - }); - - it('should create app session with allowance for participant to deposit', async () => { - await authenticateAppWithAllowances(aliceAppWS, aliceAppIdentity, ASSET_SYMBOL, appSessionDepositAmount.toString(), 'clearnode'); - }); - - it('should take snapshot of ledger balances', async () => { - const ledgerBalances = await getLedgerBalances(aliceAppIdentity, aliceAppWS); - expect(ledgerBalances).toHaveLength(1); - expect(ledgerBalances[0].amount).toBe((onChainDepositAmount).toString()); - expect(ledgerBalances[0].asset).toBe(ASSET_SYMBOL); - }); - - it('should create app session', async () => { - appSessionId = await createTestAppSession( - aliceAppIdentity, - bobAppIdentity, - aliceAppWS, - RPCProtocolVersion.NitroRPC_0_2, - ASSET_SYMBOL, - appSessionDepositAmount.toString(), - SESSION_DATA_WAITING, - 'clearnode' - ); - }); - - it('should submit state with updated session_data', async () => { - const allocations = [ - { - participant: aliceAppIdentity.walletAddress, - asset: ASSET_SYMBOL, - amount: (appSessionDepositAmount / BigInt(2)).toString(), // 50 USDC - }, - { - participant: bobAppIdentity.walletAddress, - asset: ASSET_SYMBOL, - amount: (appSessionDepositAmount / BigInt(2)).toString(), // 50 USDC - }, - ]; - - await submitAppStateUpdate_v02(aliceAppWS, aliceAppIdentity, appSessionId, allocations, SESSION_DATA_ACTIVE, 2); - }); - - it('should verify sessionData changes after updates', async () => { - const { sessionData } = await fetchAndParseAppSessions(aliceAppWS, aliceAppIdentity, appSessionId); - - expect(sessionData).toEqual(SESSION_DATA_ACTIVE); - }); - - it('should close app session', async () => { - const allocations = [ - { - participant: aliceAppIdentity.walletAddress, - asset: ASSET_SYMBOL, - amount: '0', - }, - { - participant: bobAppIdentity.walletAddress, - asset: ASSET_SYMBOL, - amount: appSessionDepositAmount.toString(), - }, - ]; - - await closeAppSessionWithState(aliceAppWS, aliceAppIdentity, appSessionId, allocations, SESSION_DATA_FINISHED, 3); - }); - - it('should verify sessionData changes after closing', async () => { - const { appSession, sessionData } = await fetchAndParseAppSessions(aliceAppWS, aliceAppIdentity, appSessionId); - - expect(appSession.status).toBe('closed'); - expect(sessionData).toEqual(SESSION_DATA_FINISHED); - }); - - it('should update ledger balances for providing side', async () => { - const ledgerBalances = await getLedgerBalances(aliceAppIdentity, aliceAppWS); - expect(ledgerBalances).toHaveLength(1); - expect(ledgerBalances[0].amount).toBe((appSessionDepositAmount * BigInt(9)).toString()); // 1000 - 100 - expect(ledgerBalances[0].asset).toBe(ASSET_SYMBOL); - }); - - it('should update ledger balances for receiving side', async () => { - const ledgerBalances = await getLedgerBalances(bobAppIdentity, bobWS); - expect(ledgerBalances).toHaveLength(1); - expect(ledgerBalances[0].amount).toBe((appSessionDepositAmount * BigInt(11)).toString()); // 1000 + 100 - expect(ledgerBalances[0].asset).toBe(ASSET_SYMBOL); - }); - - it('should resize Alice channel and withdraw without app funds', async () => { - const msg = await createResizeChannelMessage(alice.messageWalletSigner, { - channel_id: aliceChannelId, - allocate_amount: toRaw(onChainDepositAmount - appSessionDepositAmount), // 1000 - 100 = 900 - resize_amount: -toRaw(onChainDepositAmount - appSessionDepositAmount), // -(1000 - 100) = -900 - funds_destination: alice.walletAddress, - }); - - const resizeResponse = await aliceWS.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); - - expect(resizeResponseParams.state.allocations).toBeDefined(); - expect(resizeResponseParams.state.allocations).toHaveLength(2); - expect(String(resizeResponseParams.state.allocations[0].destination)).toBe(alice.walletAddress); - expect(String(resizeResponseParams.state.allocations[0].amount)).toBe('0'); - expect(String(resizeResponseParams.state.allocations[1].destination)).toBe(CONFIG.ADDRESSES.CLEARNODE_ADDRESS); - expect(String(resizeResponseParams.state.allocations[1].amount)).toBe('0'); - - const {txHash: resizeChannelTxHash} = await aliceClient.resizeChannel({ - ...composeResizeChannelParams( - resizeResponseParams.channelId as Hex, - resizeResponseParams, - initialAliceState - ), - }); - expect(resizeChannelTxHash).toBeDefined(); - - const resizeReceipt = await blockUtils.waitForTransaction(resizeChannelTxHash); - expect(resizeReceipt).toBeDefined(); - - const postCloseAccountBalance = await aliceClient.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(postCloseAccountBalance).toBe(toRaw(onChainDepositAmount - appSessionDepositAmount)); // 875 - }); - - it('should close Alice channel', async () => { - // Use wallet signer for channel operations, not session key - const msg = await createCloseChannelMessage(alice.messageWalletSigner, aliceChannelId, alice.walletAddress); - - const closeResponse = await aliceWS.sendAndWaitForResponse(msg, getCloseChannelPredicate(), 1000); - expect(closeResponse).toBeDefined(); - - const { params: closeResponseParams } = parseCloseChannelResponse(closeResponse); - const closeChannelTxHash = await aliceClient.closeChannel({ - finalState: { - intent: closeResponseParams.state.intent, - channelId: closeResponseParams.channelId, - data: closeResponseParams.state.stateData as Hex, - allocations: closeResponseParams.state.allocations, - version: BigInt(closeResponseParams.state.version), - serverSignature: closeResponseParams.serverSignature, - }, - stateData: closeResponseParams.state.stateData as Hex, - }); - expect(closeChannelTxHash).toBeDefined(); - - const closeReceipt = await blockUtils.waitForTransaction(closeChannelTxHash); - expect(closeReceipt).toBeDefined(); - - const postCloseAccountBalance = await aliceClient.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(postCloseAccountBalance).toBe(toRaw(onChainDepositAmount - appSessionDepositAmount)); // 900 - }); - - it('should resize Bob channel by withdrawing received funds from app to channel', async () => { - // Use wallet signer for channel operations, not session key - const msg = await createResizeChannelMessage(bob.messageWalletSigner, { - channel_id: bobChannelId, - allocate_amount: toRaw(onChainDepositAmount + appSessionDepositAmount), // 1000 + 100 = 1100 - resize_amount: -toRaw(onChainDepositAmount + appSessionDepositAmount), // -(1000 + 100) = -1100 - funds_destination: bob.walletAddress, - }); - - const resizeResponse = await bobWS.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); - - expect(resizeResponseParams.state.allocations).toBeDefined(); - expect(resizeResponseParams.state.allocations).toHaveLength(2); - expect(String(resizeResponseParams.state.allocations[0].destination)).toBe(bob.walletAddress); - expect(String(resizeResponseParams.state.allocations[0].amount)).toBe('0'); - expect(String(resizeResponseParams.state.allocations[1].destination)).toBe(CONFIG.ADDRESSES.CLEARNODE_ADDRESS); - expect(String(resizeResponseParams.state.allocations[1].amount)).toBe('0'); - - const {txHash: resizeChannelTxHash} = await bobClient.resizeChannel({ - ...composeResizeChannelParams( - resizeResponseParams.channelId as Hex, - resizeResponseParams, - initialBobState - ), - }); - expect(resizeChannelTxHash).toBeDefined(); - - const resizeReceipt = await blockUtils.waitForTransaction(resizeChannelTxHash); - expect(resizeReceipt).toBeDefined(); - - const postCloseAccountBalance = await bobClient.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(postCloseAccountBalance).toBe(toRaw(onChainDepositAmount + appSessionDepositAmount)); // 1100 - }); - - it('should close Bob channel', async () => { - // Use wallet signer for channel operations, not session key - const msg = await createCloseChannelMessage(bob.messageWalletSigner, bobChannelId, bob.walletAddress); - - const closeResponse = await bobWS.sendAndWaitForResponse(msg, getCloseChannelPredicate(), 1000); - expect(closeResponse).toBeDefined(); - - const { params: closeResponseParams } = parseCloseChannelResponse(closeResponse); - const closeChannelTxHash = await bobClient.closeChannel({ - finalState: { - intent: closeResponseParams.state.intent, - channelId: closeResponseParams.channelId, - data: closeResponseParams.state.stateData as Hex, - allocations: closeResponseParams.state.allocations, - version: BigInt(closeResponseParams.state.version), - serverSignature: closeResponseParams.serverSignature, - }, - stateData: closeResponseParams.state.stateData as Hex, - }); - expect(closeChannelTxHash).toBeDefined(); - - const closeReceipt = await blockUtils.waitForTransaction(closeChannelTxHash); - expect(closeReceipt).toBeDefined(); - - const postCloseAccountBalance = await bobClient.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(postCloseAccountBalance).toBe(toRaw(onChainDepositAmount + appSessionDepositAmount)); // 1100 - }); - - it('should withdraw funds from channel for providing side', async () => { - const preWithdrawalBalance = await blockUtils.getErc20Balance( - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - alice.walletAddress - ); - - const withdrawalTxHash = await aliceClient.withdrawal( - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - toRaw(onChainDepositAmount - appSessionDepositAmount) // 1000 - 100 - ); - expect(withdrawalTxHash).toBeDefined(); - - const withdrawalReceipt = await blockUtils.waitForTransaction(withdrawalTxHash); - expect(withdrawalReceipt).toBeDefined(); - - const postWithdrawalBalance = await blockUtils.getErc20Balance( - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - alice.walletAddress - ); - expect(postWithdrawalBalance.rawBalance - preWithdrawalBalance.rawBalance).toBe(toRaw(onChainDepositAmount - appSessionDepositAmount)); // + 900 - - const accountBalance = await aliceClient.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(accountBalance).toBe(BigInt(0)); - }); - - it('should withdraw funds from channel for receiving side', async () => { - const preWithdrawalBalance = await blockUtils.getErc20Balance( - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - bob.walletAddress - ); - - const withdrawalTxHash = await bobClient.withdrawal( - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - toRaw(onChainDepositAmount + appSessionDepositAmount) // 1000 + 100 - ); - expect(withdrawalTxHash).toBeDefined(); - - const withdrawalReceipt = await blockUtils.waitForTransaction(withdrawalTxHash); - expect(withdrawalReceipt).toBeDefined(); - - const postWithdrawalBalance = await blockUtils.getErc20Balance( - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - bob.walletAddress - ); - expect(postWithdrawalBalance.rawBalance - preWithdrawalBalance.rawBalance).toBe(toRaw(onChainDepositAmount + appSessionDepositAmount)); // + 1100 - - const accountBalance = await bobClient.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(accountBalance).toBe(BigInt(0)); - }); -}); diff --git a/test/integration/tests/backward_compatibility/nitrorpc_v02.test.ts b/test/integration/tests/backward_compatibility/nitrorpc_v02.test.ts deleted file mode 100644 index 6d872d57c..000000000 --- a/test/integration/tests/backward_compatibility/nitrorpc_v02.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { BlockchainUtils } from '@/blockchainUtils'; -import { DatabaseUtils } from '@/databaseUtils'; -import { Identity } from '@/identity'; -import { TestNitroliteClient } from '@/nitroliteClient'; -import { TestWebSocket } from '@/ws'; -import { RPCProtocolVersion, State } from '@erc7824/nitrolite'; -import { Hex } from 'viem'; -import { setupTestIdentitiesAndConnections } from '@/testSetup'; -import { - createTestChannels, - authenticateAppWithAllowances, - createTestAppSession, - toRaw, -} from '@/testHelpers'; -import { CONFIG } from '@/setup'; - -describe('App session v0.2', () => { - const ASSET_SYMBOL = CONFIG.TOKEN_SYMBOL; - - const onChainDepositAmount = BigInt(1000); - const appSessionDepositAmount = BigInt(100); - - let aliceWS: TestWebSocket; - let alice: Identity; - let aliceClient: TestNitroliteClient; - - let aliceAppWS: TestWebSocket; - let aliceAppIdentity: Identity; - - let bobWS: TestWebSocket; - let bob: Identity; - let bobAppIdentity: Identity; - let bobClient: TestNitroliteClient; - - let blockUtils: BlockchainUtils; - let databaseUtils: DatabaseUtils; - - let aliceChannelId: Hex; - let bobChannelId: Hex; - let appSessionId: string; - - let initialStates: State[]; - - const START_SESSION_DATA = { gameType: 'chess', gameState: 'waiting' }; - - let START_ALLOCATIONS; - - beforeAll(async () => { - blockUtils = new BlockchainUtils(); - databaseUtils = new DatabaseUtils(); - - ({alice, aliceWS, aliceClient, aliceAppIdentity, aliceAppWS, bob, bobWS, bobClient, bobAppIdentity} = await setupTestIdentitiesAndConnections()); - - START_ALLOCATIONS = [ - { - participant: aliceAppIdentity.walletAddress, - asset: ASSET_SYMBOL, - amount: (appSessionDepositAmount).toString(), - }, - { - participant: bobAppIdentity.walletAddress, - asset: ASSET_SYMBOL, - amount: '0', - }, - ]; - }); - - beforeEach(async () => { - await blockUtils.makeSnapshot(); - ({channelIds: [aliceChannelId, bobChannelId], states: initialStates} = await createTestChannels([{client: aliceClient, ws: aliceWS}, {client: bobClient, ws: bobWS}], toRaw(onChainDepositAmount))); - - await authenticateAppWithAllowances(aliceAppWS, aliceAppIdentity, ASSET_SYMBOL, appSessionDepositAmount.toString()); - }); - - afterEach(async () => { - await blockUtils.resetSnapshot(); - await databaseUtils.resetClearnodeState(); - }); - - afterAll(async () => { - aliceWS.close(); - aliceAppWS.close(); - bobWS.close(); - - await databaseUtils.close(); - }); - - describe('session creation', () => { - it('rejects to create app session if one of participants has non-empty channel', async () => { - const resizeAmount = BigInt(1); - await aliceClient.resizeChannelAndWait( - aliceWS, - aliceChannelId, - initialStates[0], - alice.walletAddress, - BigInt(0), // resize only to the user, so that a channel contains on-chain funds - resizeAmount - ); - - const balance = await aliceClient.getChannelBalance(aliceChannelId, CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(balance).toBe(resizeAmount); - - try { - await createTestAppSession( - aliceAppIdentity, - bobAppIdentity, - aliceAppWS, - RPCProtocolVersion.NitroRPC_0_2, - ASSET_SYMBOL, - appSessionDepositAmount.toString(), - START_SESSION_DATA - ); - } catch (e) { - expect((e as Error).message).toMatch(/RPC Error.*operation denied.*non-zero allocation.*detected/i); - return; - } - - throw new Error('App session creation was not rejected as expected.'); - }); - }); -}); diff --git a/test/integration/tests/backward_compatibility/onchain_ops_with_sk.test.ts b/test/integration/tests/backward_compatibility/onchain_ops_with_sk.test.ts deleted file mode 100644 index 18d4bce24..000000000 --- a/test/integration/tests/backward_compatibility/onchain_ops_with_sk.test.ts +++ /dev/null @@ -1,460 +0,0 @@ -import { BlockchainUtils } from '@/blockchainUtils'; -import { DatabaseUtils } from '@/databaseUtils'; -import { Identity } from '@/identity'; -import { TestNitroliteClient } from '@/nitroliteClient'; -import { CONFIG, chain } from '@/setup'; -import { toRaw } from '@/testHelpers'; -import { TestWebSocket, getCreateChannelPredicate } from '@/ws'; -import { - Channel, - State, - StateIntent, - SessionKeyStateSigner, - getChannelId, - ChannelStatus, - createCreateChannelMessage, - parseCreateChannelResponse, - convertRPCToClientState, - convertRPCToClientChannel, -} from '@erc7824/nitrolite'; -import { Hex, parseUnits } from 'viem'; -import { createAuthSessionWithClearnode } from '@/auth'; - -// NOTE: make sure this private key matches the one used to run integration tests -const CLEARNODE_PRIVATE_KEY: Hex = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; - -describe('Backward compatibility: participant as Wallet or Session Key', () => { - const depositAmount = parseUnits('1000', 6); // 1000 USDC - const resizeAmount = parseUnits('500', 6); // 500 USDC - - let ws: TestWebSocket; - let identity: Identity; - let blockUtils: BlockchainUtils; - let databaseUtils: DatabaseUtils; - let clearnodeSigner: SessionKeyStateSigner; - - beforeAll(async () => { - blockUtils = new BlockchainUtils(); - databaseUtils = new DatabaseUtils(); - - identity = new Identity(CONFIG.IDENTITIES[0].WALLET_PK, CONFIG.IDENTITIES[0].SESSION_PK); - clearnodeSigner = new SessionKeyStateSigner(CLEARNODE_PRIVATE_KEY); - - ws = new TestWebSocket(CONFIG.CLEARNODE_URL, CONFIG.DEBUG_MODE); - await ws.connect(); - await createAuthSessionWithClearnode(ws, identity); - }); - - afterAll(async () => { - ws.close(); - await databaseUtils.close(); - }); - - describe('on-chain operations with Session Key as participant', () => { - let client: TestNitroliteClient; - let channelId: Hex; - let channel: Channel; - let currentState: State; - let currentVersion: bigint; - - beforeAll(async () => { - await blockUtils.makeSnapshot(); - - // Create TestNitroliteClient with session key signer - // This simulates how channels work with session keys - client = new TestNitroliteClient(identity, identity.stateSKSigner); - - // Manually create a channel with session key as participant as Clearnode would return - // a signature over a channel with Wallet, not a Session Key, as participant - // This simulates a channel created before v0.5.0 where participant was a Session Key - channel = { - // specify user's Session Key, not Wallet - participants: [identity.sessionKeyAddress, clearnodeSigner.getAddress()], - adjudicator: CONFIG.ADDRESSES.DUMMY_ADJUDICATOR_ADDRESS, - challenge: BigInt(CONFIG.DEFAULT_CHALLENGE_TIMEOUT), - nonce: BigInt(Date.now()), - }; - - channelId = getChannelId(channel, chain.id); - - // Create initial state (unsigned) - const unsignedInitialState: State = { - intent: StateIntent.INITIALIZE, - version: BigInt(0), - data: '0x00' as Hex, - allocations: [ - { - destination: identity.sessionKeyAddress, - token: CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - amount: BigInt(0), - }, - { - destination: CONFIG.ADDRESSES.CLEARNODE_ADDRESS, - token: CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - amount: BigInt(0), - }, - ], - sigs: [], - }; - - const userSignature = await identity.stateSKSigner.signState(channelId, unsignedInitialState); - const clearnodeSignature = await clearnodeSigner.signState(channelId, unsignedInitialState); - - const initialState: State = { - ...unsignedInitialState, - sigs: [userSignature, clearnodeSignature], - }; - - const { channelId: createdChannelId, initialState: createdState } = await client.depositAndCreateChannel( - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - depositAmount, - { - channel, - unsignedInitialState: initialState, - serverSignature: clearnodeSignature, - } - ); - - expect(createdChannelId).toBe(channelId); - currentState = createdState; - currentVersion = createdState.version; - - const channelData = await client.getChannelData(channelId); - expect(channelData.channel.participants[0]).toBe(identity.sessionKeyAddress); - expect(channelData.status).toBe(ChannelStatus.ACTIVE); - }); - - afterAll(async () => { - await databaseUtils.resetClearnodeState(); - await blockUtils.resetSnapshot(); - }); - - it('should resize the channel using Clearnode RPC', async () => { - const { state } = await client.resizeChannelAndWait( - ws, - channelId, - currentState, - identity.walletAddress, - resizeAmount - ); - - const channelData = await client.getChannelData(channelId); - expect(channelData.lastValidState.version).toBe(BigInt(state.version)); - - currentState = state; - currentVersion = BigInt(state.version); - - const accountBalance = await client.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(accountBalance).toBe(resizeAmount); - }); - - it('should challenge the channel with a manually signed state', async () => { - const challengeVersion = currentVersion + BigInt(1); - const unsignedChallengeState: State = { - intent: StateIntent.OPERATE, - version: challengeVersion, - data: '0x01' as Hex, - allocations: currentState.allocations, - sigs: [], - }; - - const userSignature = await identity.stateSKSigner.signState(channelId, unsignedChallengeState); - const clearnodeSignature = await clearnodeSigner.signState(channelId, unsignedChallengeState); - - const signedChallengeState: State = { - ...unsignedChallengeState, - sigs: [userSignature, clearnodeSignature], - }; - - const txHash = await client.challengeChannel({ - channelId, - candidateState: signedChallengeState, - proofStates: [], - }); - - expect(txHash).toBeDefined(); - - const receipt = await blockUtils.waitForTransaction(txHash); - expect(receipt).toBeDefined(); - - const channelData = await client.getChannelData(channelId); - expect(channelData.lastValidState.version).toBe(challengeVersion); - - currentState = signedChallengeState; - currentVersion = challengeVersion; - }); - - it('should checkpoint the channel with a manually signed state', async () => { - const checkpointVersion = currentVersion + BigInt(1); - const unsignedCheckpointState: State = { - intent: StateIntent.OPERATE, - version: checkpointVersion, - data: '0x02' as Hex, - allocations: currentState.allocations, - sigs: [], - }; - - const userSignature = await identity.stateSKSigner.signState(channelId, unsignedCheckpointState); - const clearnodeSignature = await clearnodeSigner.signState(channelId, unsignedCheckpointState); - - const signedCheckpointState: State = { - ...unsignedCheckpointState, - sigs: [userSignature, clearnodeSignature], - }; - - const txHash = await client.checkpointChannel({ - channelId, - candidateState: signedCheckpointState, - }); - - expect(txHash).toBeDefined(); - - const receipt = await blockUtils.waitForTransaction(txHash); - expect(receipt).toBeDefined(); - - const channelData = await client.getChannelData(channelId); - expect(channelData.lastValidState.version).toBe(checkpointVersion); - - currentState = signedCheckpointState; - currentVersion = checkpointVersion; - }); - - it('should close the channel using Clearnode RPC', async () => { - const finalVersion = currentVersion + BigInt(1); - const unsignedFinalState: State = { - intent: StateIntent.FINALIZE, - version: finalVersion, - data: '0x02' as Hex, - allocations: currentState.allocations, - sigs: [], - }; - - // NOTE: we do not call the Clearnode as it will reject the operation with "participant * has challenged channels, - // cannot execute operation". After a corresponding event handler is added to the Clearnode, it can be called directly - const clearnodeSignature = await clearnodeSigner.signState(channelId, unsignedFinalState); - - const txHash = await client.closeChannel({ - finalState: { - ...unsignedFinalState, - channelId, - serverSignature: clearnodeSignature, - }, - stateData: unsignedFinalState.data, - }); - - expect(txHash).toBeDefined(); - - const receipt = await blockUtils.waitForTransaction(txHash); - expect(receipt).toBeDefined(); - - const channelData = await client.getChannelData(channelId); - expect(channelData.status).toBe(ChannelStatus.VOID); // channel should have been deleted after close - - // Withdraw funds - const withdrawalTxHash = await client.withdrawal( - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - toRaw(BigInt(500)) - ); - - expect(withdrawalTxHash).toBeDefined(); - - const withdrawalReceipt = await blockUtils.waitForTransaction(withdrawalTxHash); - expect(withdrawalReceipt).toBeDefined(); - - // Verify account balance is now zero - const finalAccountBalance = await client.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(finalAccountBalance).toBe(BigInt(0)); - }); - }); - - describe('on-chain operations with Wallet as participant', () => { - let client: TestNitroliteClient; - let channelId: Hex; - let currentState: State; - let currentVersion: bigint; - - beforeAll(async () => { - await blockUtils.makeSnapshot(); - - // Create TestNitroliteClient still with Session Key signer - // This is the way developers will use the NitroliteClient to interact with channels created in previous versions - client = new TestNitroliteClient(identity, identity.stateSKSigner); - - // Request channel creation from Clearnode - const msg = await createCreateChannelMessage(identity.messageWalletSigner, { - chain_id: chain.id, - token: CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - }); - - const createResponse = await ws.sendAndWaitForResponse(msg, getCreateChannelPredicate(), 5000); - expect(createResponse).toBeDefined(); - - const { params: createParsedResponseParams } = parseCreateChannelResponse(createResponse); - - // Use depositAndCreateChannel with the channel struct from Clearnode - const { channelId: createdChannelId, initialState } = await client.depositAndCreateChannel( - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - depositAmount, - { - unsignedInitialState: convertRPCToClientState( - createParsedResponseParams.state, - createParsedResponseParams.serverSignature - ), - channel: convertRPCToClientChannel(createParsedResponseParams.channel), - serverSignature: createParsedResponseParams.serverSignature, - } - ); - - channelId = createdChannelId; - currentState = initialState; - currentVersion = initialState.version; - - const channelData = await client.getChannelData(channelId); - expect(channelData.channel.participants[0]).toBe(identity.walletAddress); - expect(channelData.status).toBe(ChannelStatus.ACTIVE); - }); - - afterAll(async () => { - await databaseUtils.resetClearnodeState(); - await blockUtils.resetSnapshot(); - }); - - it('should resize the channel using Clearnode RPC', async () => { - const resizeAmount = toRaw(BigInt(500)); // Resize by 500 USDC - const { state } = await client.resizeChannelAndWait( - ws, - channelId, - currentState, - identity.walletAddress, - resizeAmount - ); - - const channelData = await client.getChannelData(channelId); - expect(channelData.lastValidState.version).toBe(BigInt(state.version)); - - currentState = state; - currentVersion = BigInt(state.version); - - const accountBalance = await client.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(accountBalance).toBe(resizeAmount); - }); - - it('should challenge the channel with a manually signed state', async () => { - const challengeVersion = currentVersion + BigInt(1); - const unsignedChallengeState: State = { - intent: StateIntent.OPERATE, - version: challengeVersion, - data: '0x01' as Hex, - allocations: currentState.allocations, - sigs: [], - }; - - const userSignature = await identity.stateWalletSigner.signState(channelId, unsignedChallengeState); - const clearnodeSignature = await clearnodeSigner.signState(channelId, unsignedChallengeState); - - const signedChallengeState: State = { - ...unsignedChallengeState, - sigs: [userSignature, clearnodeSignature], - }; - - const txHash = await client.challengeChannel({ - channelId, - candidateState: signedChallengeState, - proofStates: [], - }); - - expect(txHash).toBeDefined(); - - const receipt = await blockUtils.waitForTransaction(txHash); - expect(receipt).toBeDefined(); - - const channelData = await client.getChannelData(channelId); - expect(channelData.lastValidState.version).toBe(challengeVersion); - - currentState = signedChallengeState; - currentVersion = challengeVersion; - }); - - it('should checkpoint the channel with a manually signed state', async () => { - const checkpointVersion = currentVersion + BigInt(1); - const unsignedCheckpointState: State = { - intent: StateIntent.OPERATE, - version: checkpointVersion, - data: '0x02' as Hex, - allocations: currentState.allocations, - sigs: [], - }; - - const userSignature = await identity.stateWalletSigner.signState(channelId, unsignedCheckpointState); - const clearnodeSignature = await clearnodeSigner.signState(channelId, unsignedCheckpointState); - - const signedCheckpointState: State = { - ...unsignedCheckpointState, - sigs: [userSignature, clearnodeSignature], - }; - - const txHash = await client.checkpointChannel({ - channelId, - candidateState: signedCheckpointState, - }); - - expect(txHash).toBeDefined(); - - const receipt = await blockUtils.waitForTransaction(txHash); - expect(receipt).toBeDefined(); - - const channelData = await client.getChannelData(channelId); - expect(channelData.lastValidState.version).toBe(checkpointVersion); - - currentState = signedCheckpointState; - currentVersion = checkpointVersion; - }); - - it('should close the channel using Clearnode RPC', async () => { - const finalVersion = currentVersion + BigInt(1); - const unsignedFinalState: State = { - intent: StateIntent.FINALIZE, - version: finalVersion, - data: '0x02' as Hex, - allocations: currentState.allocations, - sigs: [], - }; - - // NOTE: we do not call the Clearnode as it will reject the operation with "participant * has challenged channels, - // cannot execute operation". After a corresponding event handler is added to the Clearnode, it can be called directly - const clearnodeSignature = await clearnodeSigner.signState(channelId, unsignedFinalState); - - const txHash = await client.closeChannel({ - finalState: { - ...unsignedFinalState, - channelId, - serverSignature: clearnodeSignature, - }, - stateData: unsignedFinalState.data, - }); - - expect(txHash).toBeDefined(); - - const receipt = await blockUtils.waitForTransaction(txHash); - expect(receipt).toBeDefined(); - - const channelData = await client.getChannelData(channelId); - expect(channelData.status).toBe(ChannelStatus.VOID); // channel should have been deleted after close - - // Withdraw funds - const withdrawalTxHash = await client.withdrawal( - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - toRaw(BigInt(500)) - ); - - expect(withdrawalTxHash).toBeDefined(); - - const withdrawalReceipt = await blockUtils.waitForTransaction(withdrawalTxHash); - expect(withdrawalReceipt).toBeDefined(); - - // Verify account balance is now zero - const finalAccountBalance = await client.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(finalAccountBalance).toBe(BigInt(0)); - }); - }); -}); diff --git a/test/integration/tests/challenge_channel.test.ts b/test/integration/tests/challenge_channel.test.ts index eeec4c5bd..27988c10c 100644 --- a/test/integration/tests/challenge_channel.test.ts +++ b/test/integration/tests/challenge_channel.test.ts @@ -8,7 +8,7 @@ import { getChannelUpdatePredicateWithStatus, TestWebSocket } from '@/ws'; import { parseChannelUpdateResponse, RPCChannelStatus, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; import { parseUnits } from 'viem'; describe('Challenge channel', () => { diff --git a/test/integration/tests/clearnode_auth.test.ts b/test/integration/tests/clearnode_auth.test.ts index 70c98953b..0ba91ec3a 100644 --- a/test/integration/tests/clearnode_auth.test.ts +++ b/test/integration/tests/clearnode_auth.test.ts @@ -11,7 +11,7 @@ import { createEIP712AuthMessageSigner, parseAuthChallengeResponse, parseAuthVerifyResponse, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; describe('Clearnode Authentication', () => { let ws: TestWebSocket; diff --git a/test/integration/tests/clearnode_connect.test.ts b/test/integration/tests/clearnode_connect.test.ts deleted file mode 100644 index 79acbb937..000000000 --- a/test/integration/tests/clearnode_connect.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -describe('Clearnode Connection', () => { - // TODO: find a public request that can be used to test the connection - test('This test will be skipped', () => { - expect(true).toBe(true); - }); - // let ws: TestWebSocket; - - // beforeEach(() => { - // ws = new TestWebSocket(CONFIG.CLEARNODE_URL, CONFIG.DEBUG_MODE); - // }); - - // afterEach(() => { - // ws.close(); - // }); - - // it('should receive pong response from the Clearnode server', async () => { - // await ws.connect(); - - // const msg = JSON.stringify({ req: [0, 'ping', [], Date.now()], sig: [] }); - // const response = await ws.sendAndWaitForResponse(msg, getPongPredicate(), 1000); - - // expect(response).toBeDefined(); - // }); - - // it('should handle connection timeout', async () => { - // await ws.connect(); - - // await expect(ws.waitForMessage((data) => data === 'nonexistent', undefined, 500)).rejects.toThrow( - // 'Timeout waiting for message after 500ms. Request ID: N/A' - // ); - // }); -}); diff --git a/test/integration/tests/close_channel.test.ts b/test/integration/tests/close_channel.test.ts index 130b14bc5..5c5285e95 100644 --- a/test/integration/tests/close_channel.test.ts +++ b/test/integration/tests/close_channel.test.ts @@ -11,7 +11,7 @@ import { parseResizeChannelResponse, createCloseChannelMessage, parseCloseChannelResponse, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; import { Hex, parseUnits } from 'viem'; describe('Close channel', () => { diff --git a/test/integration/tests/create_channel.test.ts b/test/integration/tests/create_channel.test.ts index db5b31dad..9012f4ab8 100644 --- a/test/integration/tests/create_channel.test.ts +++ b/test/integration/tests/create_channel.test.ts @@ -21,7 +21,7 @@ import { RPCChannelStatus, RPCData, RPCMethod, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; import { Hex, parseUnits } from 'viem'; describe('Create channel', () => { diff --git a/test/integration/tests/get_user_tag.test.ts b/test/integration/tests/get_user_tag.test.ts deleted file mode 100644 index d97502bbc..000000000 --- a/test/integration/tests/get_user_tag.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { createAuthSessionWithClearnode } from '@/auth'; -import { DatabaseUtils } from '@/databaseUtils'; -import { Identity } from '@/identity'; -import { CONFIG } from '@/setup'; -import { getGetUserTagPredicate, TestWebSocket } from "@/ws"; -import { createGetUserTagMessage, parseGetUserTagResponse } from "@erc7824/nitrolite"; - -describe('Get User Tag Integration', () => { - let ws: TestWebSocket; - let identity: Identity; - let databaseUtils: DatabaseUtils; - - beforeAll(async () => { - // Setup database utils for cleanup - databaseUtils = new DatabaseUtils(); - - // Create identity - identity = new Identity(CONFIG.IDENTITIES[0].WALLET_PK, CONFIG.IDENTITIES[0].SESSION_PK); - - // Create WebSocket connection - ws = new TestWebSocket(CONFIG.CLEARNODE_URL, CONFIG.DEBUG_MODE); - await ws.connect(); - - // Create authenticated session - await createAuthSessionWithClearnode(ws, identity); - }); - - afterAll(async () => { - if (ws) { - ws.close(); - } - - // Clean up database - await databaseUtils.resetClearnodeState(); - await databaseUtils.close(); - }); - - describe('createGetUserTagMessage', () => { - it('should successfully request user tag', async () => { - const msg = await createGetUserTagMessage(identity.messageSKSigner); - - const response = await ws.sendAndWaitForResponse(msg, getGetUserTagPredicate(), 5000); - - expect(response).toBeDefined(); - - const parsedResponse = parseGetUserTagResponse(response); - expect(parsedResponse).toBeDefined(); - expect(parsedResponse.params).toBeDefined(); - expect(parsedResponse.params.tag).toBeDefined(); - expect(typeof parsedResponse.params.tag).toBe('string'); - expect(parsedResponse.params.tag.length).toBeGreaterThan(0); - }); - - it('should return consistent user tag across multiple requests', async () => { - // First request - const msg1 = await createGetUserTagMessage(identity.messageSKSigner); - const response1 = await ws.sendAndWaitForResponse(msg1, getGetUserTagPredicate(), 5000); - const parsedResponse1 = parseGetUserTagResponse(response1); - - // Second request - const msg2 = await createGetUserTagMessage(identity.messageSKSigner); - const response2 = await ws.sendAndWaitForResponse(msg2, getGetUserTagPredicate(), 5000); - const parsedResponse2 = parseGetUserTagResponse(response2); - - expect(parsedResponse1.params.tag).toBe(parsedResponse2.params.tag); - }); - - it('should return valid tag format', async () => { - const msg = await createGetUserTagMessage(identity.messageSKSigner); - const response = await ws.sendAndWaitForResponse(msg, getGetUserTagPredicate(), 5000); - const parsedResponse = parseGetUserTagResponse(response); - - // Verify the tag format matches expected pattern (e.g., 'UX123D8C') - expect(parsedResponse.params.tag).toMatch(/^[A-Z0-9]+$/); - expect(parsedResponse.params.tag.length).toBeGreaterThan(3); - }); - }); -}); diff --git a/test/integration/tests/ledger_transactions.test.ts b/test/integration/tests/ledger_transactions.test.ts index d34902bfd..6e5fe7795 100644 --- a/test/integration/tests/ledger_transactions.test.ts +++ b/test/integration/tests/ledger_transactions.test.ts @@ -8,7 +8,7 @@ import { GetLedgerTransactionsFilters, parseGetLedgerTransactionsResponse, RPCTxType, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; describe('Ledger Transactions Integration', () => { let ws: TestWebSocket; diff --git a/test/integration/tests/lifecycle_nitrorpc_v04.test.ts b/test/integration/tests/lifecycle_nitrorpc_v04.test.ts index 7e0e529f8..a90362298 100644 --- a/test/integration/tests/lifecycle_nitrorpc_v04.test.ts +++ b/test/integration/tests/lifecycle_nitrorpc_v04.test.ts @@ -16,7 +16,7 @@ import { RPCProtocolVersion, RPCAppStateIntent, State, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; import { Hex } from 'viem'; import { setupTestIdentitiesAndConnections, diff --git a/test/integration/tests/nitrorpc_v04.test.ts b/test/integration/tests/nitrorpc_v04.test.ts index f1cee165d..a8ed1fc1a 100644 --- a/test/integration/tests/nitrorpc_v04.test.ts +++ b/test/integration/tests/nitrorpc_v04.test.ts @@ -3,7 +3,7 @@ import { DatabaseUtils } from '@/databaseUtils'; import { Identity } from '@/identity'; import { TestNitroliteClient } from '@/nitroliteClient'; import { TestWebSocket } from '@/ws'; -import { RPCAppStateIntent, RPCProtocolVersion, State } from '@erc7824/nitrolite'; +import { RPCAppStateIntent, RPCProtocolVersion, State } from '@layer-3/nitrolite'; import { Hex } from 'viem'; import { fetchAndParseAppSessions, setupTestIdentitiesAndConnections } from '@/testSetup'; import { diff --git a/test/integration/tests/resize_channel.test.ts b/test/integration/tests/resize_channel.test.ts deleted file mode 100644 index 4ec2aad11..000000000 --- a/test/integration/tests/resize_channel.test.ts +++ /dev/null @@ -1,432 +0,0 @@ -import { createAuthSessionWithClearnode } from '@/auth'; -import { BlockchainUtils } from '@/blockchainUtils'; -import { DatabaseUtils } from '@/databaseUtils'; -import { Identity } from '@/identity'; -import { TestNitroliteClient } from '@/nitroliteClient'; -import { CONFIG } from '@/setup'; -import { composeResizeChannelParams, getLedgerBalances, toRaw } from '@/testHelpers'; -import { getGetLedgerBalancesPredicate, getChannelUpdatePredicateWithStatus, getResizeChannelPredicate, TestWebSocket } from '@/ws'; -import { - createResizeChannelMessage, - parseResizeChannelResponse, RPCChannelStatus, - createGetLedgerBalancesMessage, - parseGetLedgerBalancesResponse, -} from '@erc7824/nitrolite'; -import { Hex, parseUnits } from 'viem'; - -describe('Resize channel', () => { - const depositAmount = parseUnits('100', 6); // 100 USDC (decimals = 6) - - let ws: TestWebSocket; - let identity: Identity; - let client: TestNitroliteClient; - let blockUtils: BlockchainUtils; - let databaseUtils: DatabaseUtils; - - beforeAll(async () => { - blockUtils = new BlockchainUtils(); - databaseUtils = new DatabaseUtils(); - identity = new Identity(CONFIG.IDENTITIES[0].WALLET_PK, CONFIG.IDENTITIES[0].SESSION_PK); - ws = new TestWebSocket(CONFIG.CLEARNODE_URL, CONFIG.DEBUG_MODE); - - client = new TestNitroliteClient(identity); - }); - - beforeEach(async () => { - await ws.connect(); - await createAuthSessionWithClearnode(ws, identity); - await blockUtils.makeSnapshot(); - }); - - afterEach(async () => { - ws.close(); - await databaseUtils.resetClearnodeState(); - await blockUtils.resetSnapshot(); - }); - - afterAll(async () => { - await databaseUtils.close(); - }); - - it('should resize channel by adding funds from deposit to channel', async () => { - const { params: createResponseParams, state: createResponseState } = await client.createAndWaitForChannel(ws, { - tokenAddress: CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - amount: depositAmount * BigInt(5), - depositAmount: depositAmount * BigInt(10), // depositing more than initial amount to have resize buffer - }); - - const channelBalance = await client.getChannelBalance( - createResponseParams.channelId, - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS - ); - expect(channelBalance).toBe(BigInt(0)); - - const balances = await getLedgerBalances(identity, ws); - expect(balances).toBeDefined(); - expect(balances).toHaveLength(1); - expect(balances[0].asset).toBe(CONFIG.TOKEN_SYMBOL); - expect(balances[0].amount).toBe('500'); - - const preResizeCustodyBalance = await client.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(preResizeCustodyBalance).toBe(depositAmount * BigInt(5)); // 1000 - 500 - - let msg = await createResizeChannelMessage(identity.messageSKSigner, { - channel_id: createResponseParams.channelId, - resize_amount: depositAmount, - allocate_amount: -depositAmount, - funds_destination: identity.walletAddress, - }); - - const resizeResponse = await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); - expect(resizeResponseParams.channelId).toBe(createResponseParams.channelId); - expect(resizeResponseParams.state.stateData).toBeDefined(); - expect(resizeResponseParams.state.intent).toBe(2); // StateIntent.RESIZE // TODO: add enum to sdk - expect(resizeResponseParams.state.version).toBe(Number(createResponseState.version) + 1); - - expect(resizeResponseParams.serverSignature).toBeDefined(); - - expect(resizeResponseParams.state.allocations).toBeDefined(); - expect(resizeResponseParams.state.allocations).toHaveLength(2); - expect(String(resizeResponseParams.state.allocations[0].destination)).toBe(identity.walletAddress); - expect(String(resizeResponseParams.state.allocations[0].token)).toBe(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(String(resizeResponseParams.state.allocations[0].amount)).toBe('0'); - expect(String(resizeResponseParams.state.allocations[1].destination)).toBe(CONFIG.ADDRESSES.CLEARNODE_ADDRESS); - expect(String(resizeResponseParams.state.allocations[1].token)).toBe(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(String(resizeResponseParams.state.allocations[1].amount)).toBe('0'); - - const {txHash: resizeChannelTxHash} = await client.resizeChannel({ - ...composeResizeChannelParams( - resizeResponseParams.channelId as Hex, - resizeResponseParams, - createResponseState - ), - }); - expect(resizeChannelTxHash).toBeDefined(); - - const resizeReceipt = await blockUtils.waitForTransaction(resizeChannelTxHash); - expect(resizeReceipt).toBeDefined(); - - const postResizeCustodyBalance = await client.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(postResizeCustodyBalance).toBe(depositAmount * BigInt(4)); // 1000 - 500 - 100 - - const postResizeChannelBalance = await client.getChannelBalance( - createResponseParams.channelId, - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS - ); - expect(postResizeChannelBalance).toBe(BigInt(0)); - - const newBalances = await getLedgerBalances(identity, ws); - expect(newBalances).toBeDefined(); - expect(newBalances).toHaveLength(1); - expect(newBalances[0].asset).toBe(CONFIG.TOKEN_SYMBOL); - expect(newBalances[0].amount).toBe('600'); // 500 + 100 - }); - - it('should resize channel by withdrawing funds from channel to deposit', async () => { - const { params: createResponseParams, state: createResponseState } = await client.createAndWaitForChannel(ws, { - tokenAddress: CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - amount: depositAmount * BigInt(5), - depositAmount: depositAmount * BigInt(10), // depositing more than initial amount to have resize buffer - }); - - const preResizeAccountBalance = await client.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(preResizeAccountBalance).toBe(depositAmount * BigInt(5)); // 1000 - 500 - - const preResizeChannelBalance = await client.getChannelBalance( - createResponseParams.channelId, - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS - ); - expect(preResizeChannelBalance).toBe(BigInt(0)); - - let msg = await createGetLedgerBalancesMessage(identity.messageSKSigner, identity.walletAddress); - let lbResponse = await ws.sendAndWaitForResponse(msg, getGetLedgerBalancesPredicate(), 1000); - let { params: lbResponseParams } = parseGetLedgerBalancesResponse(lbResponse); - expect(lbResponseParams.ledgerBalances).toBeDefined(); - expect(lbResponseParams.ledgerBalances).toHaveLength(1); - expect(String(lbResponseParams.ledgerBalances[0].amount)).toBe('500'); // 500 - - msg = await createResizeChannelMessage(identity.messageSKSigner, { - channel_id: createResponseParams.channelId, - resize_amount: -depositAmount, - allocate_amount: depositAmount, - funds_destination: identity.walletAddress, - }); - - const resizeResponse = await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - - const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); - expect(resizeResponseParams.state.allocations).toBeDefined(); - expect(resizeResponseParams.state.allocations).toHaveLength(2); - expect(String(resizeResponseParams.state.allocations[0].destination)).toBe(identity.walletAddress); - expect(String(resizeResponseParams.state.allocations[0].amount)).toBe('0'); - expect(String(resizeResponseParams.state.allocations[1].destination)).toBe(CONFIG.ADDRESSES.CLEARNODE_ADDRESS); - expect(String(resizeResponseParams.state.allocations[1].amount)).toBe('0'); - - const {txHash: resizeChannelTxHash} = await client.resizeChannel({ - ...composeResizeChannelParams( - resizeResponseParams.channelId as Hex, - resizeResponseParams, - createResponseState - ), - }); - expect(resizeChannelTxHash).toBeDefined(); - - const resizeReceipt = await blockUtils.waitForTransaction(resizeChannelTxHash); - expect(resizeReceipt).toBeDefined(); - - const postResizeAccountBalance = await client.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(postResizeAccountBalance).toBe(depositAmount * BigInt(6)); // 1000 - 500 + 100 - - const postResizeChannelBalance = await client.getChannelBalance( - createResponseParams.channelId, - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS - ); - expect(postResizeChannelBalance).toBe(BigInt(0)); - - msg = await createGetLedgerBalancesMessage(identity.messageSKSigner, identity.walletAddress); - lbResponse = await ws.sendAndWaitForResponse(msg, getGetLedgerBalancesPredicate(), 1000); - ({ params: lbResponseParams } = parseGetLedgerBalancesResponse(lbResponse)); - expect(lbResponseParams.ledgerBalances).toBeDefined(); - expect(lbResponseParams.ledgerBalances).toHaveLength(1); - expect(String(lbResponseParams.ledgerBalances[0].amount)).toBe('400'); // 500 - 100 - }); - - it('should resize channel by allocating funds from virtual ledger to channel', async () => { - const { params: createResponseParams, state: createResponseState } = await client.createAndWaitForChannel(ws, { - tokenAddress: CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - amount: depositAmount * BigInt(5), - depositAmount: depositAmount * BigInt(10), // depositing more than initial amount to have resize buffer - }); - - const preResizeAccountBalance = await client.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(preResizeAccountBalance).toBe(depositAmount * BigInt(5)); // 1000 - 500 - - const preResizeChannelBalance = await client.getChannelBalance( - createResponseParams.channelId, - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS - ); - expect(preResizeChannelBalance).toBe(BigInt(0)); - - let msg = await createGetLedgerBalancesMessage(identity.messageSKSigner, identity.walletAddress); - let lbResponse = await ws.sendAndWaitForResponse(msg, getGetLedgerBalancesPredicate(), 1000); - let { params: lbResponseParams } = parseGetLedgerBalancesResponse(lbResponse); - expect(lbResponseParams.ledgerBalances).toBeDefined(); - expect(lbResponseParams.ledgerBalances).toHaveLength(1); - expect(String(lbResponseParams.ledgerBalances[0].amount)).toBe('500'); // 500 - - msg = await createResizeChannelMessage(identity.messageSKSigner, { - channel_id: createResponseParams.channelId, - resize_amount: parseUnits('0', 6), - allocate_amount: depositAmount, - funds_destination: identity.walletAddress, - }); - - const resizeResponse = await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); - expect(resizeResponseParams.state.allocations).toBeDefined(); - expect(resizeResponseParams.state.allocations).toHaveLength(2); - expect(String(resizeResponseParams.state.allocations[0].destination)).toBe(identity.walletAddress); - expect(String(resizeResponseParams.state.allocations[0].amount)).toBe( - (depositAmount * BigInt(1)).toString() // 100 - ); - expect(String(resizeResponseParams.state.allocations[1].destination)).toBe(CONFIG.ADDRESSES.CLEARNODE_ADDRESS); - expect(String(resizeResponseParams.state.allocations[1].amount)).toBe('0'); - - const {txHash: resizeChannelTxHash} = await client.resizeChannel({ - ...composeResizeChannelParams( - resizeResponseParams.channelId as Hex, - resizeResponseParams, - createResponseState - ) - }); - expect(resizeChannelTxHash).toBeDefined(); - - const resizeReceipt = await blockUtils.waitForTransaction(resizeChannelTxHash); - expect(resizeReceipt).toBeDefined(); - - const postResizeAccountBalance = await client.getAccountBalance(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(postResizeAccountBalance).toBe(depositAmount * BigInt(5)); // 1000 - 500 - - const postResizeChannelBalance = await client.getChannelBalance( - createResponseParams.channelId, - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS - ); - expect(postResizeChannelBalance).toBe(depositAmount * BigInt(1)); // 100 - - msg = await createGetLedgerBalancesMessage(identity.messageSKSigner, identity.walletAddress); - lbResponse = await ws.sendAndWaitForResponse(msg, getGetLedgerBalancesPredicate(), 1000); - ({ params: lbResponseParams } = parseGetLedgerBalancesResponse(lbResponse)); - expect(lbResponseParams.ledgerBalances).toBeDefined(); - expect(lbResponseParams.ledgerBalances).toHaveLength(1); - expect(String(lbResponseParams.ledgerBalances[0].amount)).toBe('500'); // 1000 - 500 (100 allocated to the channel are still in virtual ledger) - }); - - it('should subtract resize amount from unified balance after withdrawal resize request', async () => { - const DEPOSIT_AMOUNT = BigInt(10) - const WITHDRAWAL_AMOUNT = BigInt(1) - - const { params: createResponseParams, state: createResponseState } = await client.createAndWaitForChannel(ws, { - tokenAddress: CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - amount: toRaw(DEPOSIT_AMOUNT) - }); - - const preResizeUnifiedBalance = await getLedgerBalances(identity, ws); - expect(preResizeUnifiedBalance.length).toBe(1); - expect(preResizeUnifiedBalance[0].amount).toBe(DEPOSIT_AMOUNT.toString()); - - const msg = await createResizeChannelMessage(identity.messageSKSigner, { - channel_id: createResponseParams.channelId, - resize_amount: -toRaw(WITHDRAWAL_AMOUNT), - allocate_amount: toRaw(WITHDRAWAL_AMOUNT), - funds_destination: identity.walletAddress, - }); - - const resizeResponse = await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); - - // after resize withdrawal is requested, the unified balance should decrease by resize amount - const postResizeReqUnifiedBalance = await getLedgerBalances(identity, ws); - expect(postResizeReqUnifiedBalance.length).toBe(1); - expect(postResizeReqUnifiedBalance[0].amount).toBe((DEPOSIT_AMOUNT - WITHDRAWAL_AMOUNT).toString()); - - const {txHash: resizeChannelTxHash} = await client.resizeChannel({ - ...composeResizeChannelParams( - resizeResponseParams.channelId as Hex, - resizeResponseParams, - createResponseState - ), - }); - expect(resizeChannelTxHash).toBeDefined(); - - const resizeReceipt = await blockUtils.waitForTransaction(resizeChannelTxHash); - expect(resizeReceipt).toBeDefined(); - - const postResizeUnifiedBalance = await getLedgerBalances(identity, ws); - expect(postResizeUnifiedBalance.length).toBe(1); - expect(postResizeUnifiedBalance[0].amount).toBe((DEPOSIT_AMOUNT - WITHDRAWAL_AMOUNT).toString()); - }); - - it('should NOT subtract resize amount from unified balance after top-up resize request', async () => { - const DEPOSIT_AMOUNT = BigInt(10) - const TOP_UP_AMOUNT = BigInt(1) - - const { params: createResponseParams, state: createResponseState } = await client.createAndWaitForChannel(ws, { - tokenAddress: CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - amount: toRaw(DEPOSIT_AMOUNT), - depositAmount: toRaw(DEPOSIT_AMOUNT + TOP_UP_AMOUNT), // deposit more to have top-up buffer - }); - - const preResizeUnifiedBalance = await getLedgerBalances(identity, ws); - expect(preResizeUnifiedBalance.length).toBe(1); - expect(preResizeUnifiedBalance[0].amount).toBe(DEPOSIT_AMOUNT.toString()); - - const msg = await createResizeChannelMessage(identity.messageSKSigner, { - channel_id: createResponseParams.channelId, - resize_amount: toRaw(TOP_UP_AMOUNT), - funds_destination: identity.walletAddress, - }); - - const resizeResponse = await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); - - // after resize deposit is requested, the unified balance should NOT change - const postResizeReqUnifiedBalance = await getLedgerBalances(identity, ws); - expect(postResizeReqUnifiedBalance.length).toBe(1); - expect(postResizeReqUnifiedBalance[0].amount).toBe(DEPOSIT_AMOUNT.toString()); - - const {txHash: resizeChannelTxHash} = await client.resizeChannel({ - ...composeResizeChannelParams( - resizeResponseParams.channelId as Hex, - resizeResponseParams, - createResponseState - ), - }); - expect(resizeChannelTxHash).toBeDefined(); - - const resizeReceipt = await blockUtils.waitForTransaction(resizeChannelTxHash); - expect(resizeReceipt).toBeDefined(); - - const postResizeUnifiedBalance = await getLedgerBalances(identity, ws); - expect(postResizeUnifiedBalance.length).toBe(1); - expect(postResizeUnifiedBalance[0].amount).toBe((DEPOSIT_AMOUNT + TOP_UP_AMOUNT).toString()); - }); - - it('fail on requesting resize after resize was already requested', async () => { - const DEPOSIT_AMOUNT = BigInt(10) - const WITHDRAW_AMOUNT = BigInt(1) - - const { params: createResponseParams } = await client.createAndWaitForChannel(ws, { - tokenAddress: CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - amount: toRaw(DEPOSIT_AMOUNT), - }); - - const preResizeUnifiedBalance = await getLedgerBalances(identity, ws); - expect(preResizeUnifiedBalance.length).toBe(1); - expect(preResizeUnifiedBalance[0].amount).toBe(DEPOSIT_AMOUNT.toString()); - - const msg = await createResizeChannelMessage(identity.messageSKSigner, { - channel_id: createResponseParams.channelId, - resize_amount: -toRaw(WITHDRAW_AMOUNT), - allocate_amount: toRaw(WITHDRAW_AMOUNT), - funds_destination: identity.walletAddress, - }); - - await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - - // do NOT perform the resize again, just send the request and expect error - - const msg2 = await createResizeChannelMessage(identity.messageSKSigner, { - channel_id: createResponseParams.channelId, - resize_amount: -toRaw(WITHDRAW_AMOUNT), - allocate_amount: toRaw(WITHDRAW_AMOUNT), - funds_destination: identity.walletAddress, - }); - - try { - await ws.sendAndWaitForResponse(msg2, getResizeChannelPredicate(), 1000); - } catch (e) { - expect(e).toBeDefined(); - expect(e.message).toMatch(/RPC Error.*operation denied: resize already ongoing/); - } - }); - - it('should release locked funds after close if resize was requested, but not performed', async () => { - const DEPOSIT_AMOUNT = BigInt(10) - const WITHDRAWAL_AMOUNT = BigInt(1) - - const { params: createResponseParams } = await client.createAndWaitForChannel(ws, { - tokenAddress: CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - amount: toRaw(DEPOSIT_AMOUNT) - }); - - const preResizeUnifiedBalance = await getLedgerBalances(identity, ws); - expect(preResizeUnifiedBalance.length).toBe(1); - expect(preResizeUnifiedBalance[0].amount).toBe(DEPOSIT_AMOUNT.toString()); - - const msg = await createResizeChannelMessage(identity.messageSKSigner, { - channel_id: createResponseParams.channelId, - resize_amount: -toRaw(WITHDRAWAL_AMOUNT), - allocate_amount: toRaw(WITHDRAWAL_AMOUNT), - funds_destination: identity.walletAddress, - }); - - await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - - // do NOT perform the resize, just close the channel - const resizeChannelUpdatePromise = ws.waitForMessage( - getChannelUpdatePredicateWithStatus(RPCChannelStatus.Closed), - undefined, - 5000 - ); - - const response = await client.closeAndWithdrawChannel(ws, createResponseParams.channelId); - await resizeChannelUpdatePromise; - - const postResizeUnifiedBalance = await getLedgerBalances(identity, ws); - expect(postResizeUnifiedBalance.length).toBe(1); - // channel does NOT have any amount allocated, thus "close(...)" should NOT withdraw any funds, - // but rather release funds locked after resize request - expect(postResizeUnifiedBalance[0].amount).toBe("10"); - }); -}); diff --git a/test/integration/tests/session_key.test.ts b/test/integration/tests/session_key.test.ts index 21658759e..9fe94b6de 100644 --- a/test/integration/tests/session_key.test.ts +++ b/test/integration/tests/session_key.test.ts @@ -10,7 +10,7 @@ import { createRevokeSessionKeyMessage, createTransferMessage, parseAnyRPCResponse, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; import { createTestChannels, diff --git a/test/integration/tests/transfer.test.ts b/test/integration/tests/transfer.test.ts index 7351032cd..6e22747e0 100644 --- a/test/integration/tests/transfer.test.ts +++ b/test/integration/tests/transfer.test.ts @@ -11,7 +11,7 @@ import { parseAnyRPCResponse, TransferRequestParams, RPCMethod, -} from '@erc7824/nitrolite'; +} from '@layer-3/nitrolite'; describe('Transfer Integration', () => { let ws: TestWebSocket; diff --git a/test/integration/tsconfig.json b/test/integration/tsconfig.json index 5f421040f..9612ab0e7 100644 --- a/test/integration/tsconfig.json +++ b/test/integration/tsconfig.json @@ -5,7 +5,7 @@ "baseUrl": ".", "paths": { "@/*": ["common/*"], - "@erc7824/nitrolite": ["../sdk/dist"] + "@layer-3/nitrolite": ["../sdk/dist"] }, "esModuleInterop": true }, From 424375a8697ca73067a34584985fbbdf51fa25c0 Mon Sep 17 00:00:00 2001 From: Dmytro Steblyna <80773046+dimast-x@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:50:04 +0000 Subject: [PATCH 06/13] YNU-833: Add New Metrics (#605) * **New Features** * Added lifespan metrics storage, total-value-locked metric, and active-user/app-session metrics with day/week/month windows. * **Enhancements** * Standardized/renamed session and channel metrics and added setters; exporter now emits additional metrics and runs less frequently. * Improved parsing for session and channel status inputs; expanded intent constants. * **Performance** * Added indexes to speed metric collection and aggregation. * **Tests** * Added comprehensive tests for metric recording, retrieval, ID behavior, and activity counts. --------- Co-authored-by: Anton Filonenko --- .github/CODEOWNERS | 6 +- .../20251222000000_initial_schema.sql | 24 + clearnode/main.go | 64 ++- clearnode/metrics/exporter.go | 41 +- clearnode/metrics/interface.go | 3 + clearnode/store/database/app_session.go | 20 - clearnode/store/database/channel.go | 21 - clearnode/store/database/database.go | 2 +- clearnode/store/database/interface.go | 22 +- clearnode/store/database/lifespan_metric.go | 439 ++++++++++++++++++ .../store/database/lifespan_metric_test.go | 243 ++++++++++ clearnode/store/database/testing.go | 4 +- pkg/app/app_session_v1.go | 41 ++ pkg/core/types.go | 43 ++ 14 files changed, 906 insertions(+), 67 deletions(-) create mode 100644 clearnode/store/database/lifespan_metric.go create mode 100644 clearnode/store/database/lifespan_metric_test.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 365492e92..b93cc0637 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,8 +1,4 @@ # CODEOWNERS: https://help.github.com/articles/about-codeowners/ # Yellow Network - Research and Development -* @alessio -* @dimast-x -* @nksazonov -* @philanton -* @ihsraham +* @alessio @dimast-x @nksazonov @philanton @ihsraham diff --git a/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql b/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql index c1cd2da03..fd7ba50a3 100644 --- a/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql +++ b/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql @@ -87,6 +87,7 @@ CREATE TABLE transactions ( ); CREATE INDEX idx_transactions_type ON transactions(tx_type); +CREATE INDEX idx_transactions_type_created ON transactions(tx_type, created_at); CREATE INDEX idx_transactions_from_account ON transactions(from_account); CREATE INDEX idx_transactions_to_account ON transactions(to_account); CREATE INDEX idx_transactions_from_to_type ON transactions(from_account, to_account, tx_type); @@ -281,7 +282,29 @@ CREATE TABLE action_log_v1 ( CREATE INDEX idx_action_log_v1_wallet_gated_action_created ON action_log_v1(user_wallet, gated_action, created_at DESC); +-- Lifespan metrics table: Stores accumulated metric counters with labels +CREATE TABLE lifespan_metrics ( + id VARCHAR(66) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + labels JSONB, + value NUMERIC(78, 18) NOT NULL, + last_timestamp TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_lifespan_metrics_name_last_ts ON lifespan_metrics(name, last_timestamp DESC); + +-- Indexes for metric-gathering queries that filter on updated_at +CREATE INDEX idx_channels_updated_at ON channels(updated_at); +CREATE INDEX idx_app_sessions_v1_updated_at ON app_sessions_v1(updated_at); +CREATE INDEX idx_user_balances_updated_at ON user_balances(updated_at); + -- +goose Down +DROP INDEX IF EXISTS idx_user_balances_updated_at; +DROP INDEX IF EXISTS idx_app_sessions_v1_updated_at; +DROP INDEX IF EXISTS idx_channels_updated_at; +DROP INDEX IF EXISTS idx_lifespan_metrics_name_last_ts; +DROP TABLE IF EXISTS lifespan_metrics; DROP INDEX IF EXISTS idx_action_log_v1_wallet_gated_action_created; DROP TABLE IF EXISTS action_log_v1; DROP INDEX IF EXISTS idx_user_staked_v1_user_wallet; @@ -321,6 +344,7 @@ DROP INDEX IF EXISTS idx_transactions_from_comp; DROP INDEX IF EXISTS idx_transactions_from_to_type; DROP INDEX IF EXISTS idx_transactions_to_account; DROP INDEX IF EXISTS idx_transactions_from_account; +DROP INDEX IF EXISTS idx_transactions_type_created; DROP INDEX IF EXISTS idx_transactions_type; DROP TABLE IF EXISTS transactions; DROP INDEX IF EXISTS idx_channel_states_escrow_channel_id; diff --git a/clearnode/main.go b/clearnode/main.go index 03086dbc6..29dd4167a 100644 --- a/clearnode/main.go +++ b/clearnode/main.go @@ -108,7 +108,7 @@ func main() { }) } - go runStoreMetricsExporter(ctx, 10*time.Second, bb.DbStore, bb.StoreMetrics, logger) + go runStoreMetricsExporter(ctx, 30*time.Second, bb.DbStore, bb.StoreMetrics, logger) metricsListenAddr := ":4242" metricsEndpoint := "/metrics" @@ -171,8 +171,11 @@ func runStoreMetricsExporter( ctx context.Context, fetchInterval time.Duration, store interface { - CountAppSessionsByStatus() ([]database.AppSessionCount, error) - CountChannelsByStatus() ([]database.ChannelCount, error) + GetChannelsCountByLabels() ([]database.ChannelCount, error) + GetAppSessionsCountByLabels() ([]database.AppSessionCount, error) + GetTotalValueLocked() ([]database.TotalValueLocked, error) + CountActiveUsers(window time.Duration) ([]database.ActiveCountByLabel, error) + CountActiveAppSessions(window time.Duration) ([]database.ActiveCountByLabel, error) }, metricExported metrics.StoreMetricExporter, logger log.Logger) { logger = logger.WithName("store-metrics") @@ -182,21 +185,62 @@ func runStoreMetricsExporter( for { select { case <-ticker.C: - if counts, err := store.CountAppSessionsByStatus(); err != nil { - logger.Error("failed to count app sessions", "error", err) + timeSpans := []struct { + label string + duration time.Duration + }{ + {"day", 24 * time.Hour}, + {"week", 7 * 24 * time.Hour}, + {"month", 30 * 24 * time.Hour}, + } + + channelCounts, err := store.GetChannelsCountByLabels() + if err != nil { + logger.Error("failed to get channel counts by labels", "error", err) + } else { + for _, c := range channelCounts { + metricExported.SetChannels(c.Asset, c.Status, c.Count) + } + } + + appSessionCounts, err := store.GetAppSessionsCountByLabels() + if err != nil { + logger.Error("failed to get app sessions counts by labels", "error", err) } else { - for _, c := range counts { + for _, c := range appSessionCounts { metricExported.SetAppSessions(c.Application, c.Status, c.Count) } } - if counts, err := store.CountChannelsByStatus(); err != nil { - logger.Error("failed to count channels", "error", err) + tvlCounts, err := store.GetTotalValueLocked() + if err != nil { + logger.Error("failed to get total value locked", "error", err) } else { - for _, c := range counts { - metricExported.SetChannels(c.Asset, c.Status, c.Count) + for _, c := range tvlCounts { + metricExported.SetTotalValueLocked(c.Domain, c.Asset, c.Value.InexactFloat64()) + } + } + + for _, tw := range timeSpans { + if counts, err := store.CountActiveUsers(tw.duration); err != nil { + logger.Error("failed to count active users", "timeframe", tw.label, "error", err) + } else { + for _, c := range counts { + metricExported.SetActiveUsers(c.Label, tw.label, c.Count) + } } } + + for _, tw := range timeSpans { + if counts, err := store.CountActiveAppSessions(tw.duration); err != nil { + logger.Error("failed to count active app sessions", "timeframe", tw.label, "error", err) + } else { + for _, c := range counts { + metricExported.SetActiveAppSessions(c.Label, tw.label, c.Count) + } + } + } + case <-ctx.Done(): logger.Info("stopping store metrics exporter") return diff --git a/clearnode/metrics/exporter.go b/clearnode/metrics/exporter.go index 6610888cc..3148ae142 100644 --- a/clearnode/metrics/exporter.go +++ b/clearnode/metrics/exporter.go @@ -24,28 +24,49 @@ var ( ) type storeMetricExporter struct { - appSessionsTotal *prometheus.GaugeVec - channelsTotal *prometheus.GaugeVec + appSessionsTotal *prometheus.GaugeVec + channelsTotal *prometheus.GaugeVec + usersActive *prometheus.GaugeVec + appSessionsActive *prometheus.GaugeVec + totalValueLocked *prometheus.GaugeVec } func NewStoreMetricExporter(reg prometheus.Registerer) (StoreMetricExporter, error) { m := &storeMetricExporter{ appSessionsTotal: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: MetricNamespace, - Name: "app_sessions_active", + Name: "app_sessions_total", Help: "Current total number of app sessions", }, []string{"application", "status"}), channelsTotal: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: MetricNamespace, - Name: "channels_active", + Name: "channels_total", Help: "Current total number of channels", }, []string{"asset", "status"}), + usersActive: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: MetricNamespace, + Name: "users_active", + Help: "Current total active users", + }, []string{"asset", "timespan"}), + appSessionsActive: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: MetricNamespace, + Name: "app_sessions_active", + Help: "Current total active app sessions", + }, []string{"application", "timespan"}), + totalValueLocked: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: MetricNamespace, + Name: "total_value_locked", + Help: "Total value locked by domain and asset", + }, []string{"domain", "asset"}), } if reg != nil { reg.MustRegister( m.appSessionsTotal, m.channelsTotal, + m.usersActive, + m.appSessionsActive, + m.totalValueLocked, ) } else { return nil, fmt.Errorf("prometheus registerer not provided") @@ -62,6 +83,18 @@ func (m *storeMetricExporter) SetChannels(asset string, status core.ChannelStatu m.channelsTotal.WithLabelValues(asset, status.String()).Set(float64(count)) } +func (m *storeMetricExporter) SetActiveUsers(asset, timeSpanLabel string, count uint64) { + m.usersActive.WithLabelValues(asset, timeSpanLabel).Set(float64(count)) +} + +func (m *storeMetricExporter) SetActiveAppSessions(applicationID, timeSpanLabel string, count uint64) { + m.appSessionsActive.WithLabelValues(applicationID, timeSpanLabel).Set(float64(count)) +} + +func (m *storeMetricExporter) SetTotalValueLocked(domain, asset string, value float64) { + m.totalValueLocked.WithLabelValues(domain, asset).Set(value) +} + // runtimeMetricExporter is the concrete implementation of the Metrics interface. type runtimeMetricExporter struct { // Shared Metrics (Cross-Package) diff --git a/clearnode/metrics/interface.go b/clearnode/metrics/interface.go index 49335459e..db0df3f06 100644 --- a/clearnode/metrics/interface.go +++ b/clearnode/metrics/interface.go @@ -60,4 +60,7 @@ func (noopRuntimeMetricExporter) IncBlockchainEvent(uint64, bool) {} type StoreMetricExporter interface { SetAppSessions(applicationID string, status app.AppSessionStatus, count uint64) SetChannels(asset string, status core.ChannelStatus, count uint64) + SetActiveUsers(asset, timeSpanLabel string, count uint64) + SetActiveAppSessions(applicationID, timeSpanLabel string, count uint64) + SetTotalValueLocked(domain, asset string, value float64) } diff --git a/clearnode/store/database/app_session.go b/clearnode/store/database/app_session.go index 225932fb0..14c946cf4 100644 --- a/clearnode/store/database/app_session.go +++ b/clearnode/store/database/app_session.go @@ -133,26 +133,6 @@ func (s *DBStore) GetAppSessions(appSessionID *string, participant *string, stat return sessions, metadata, nil } -// AppSessionCount holds the result of a COUNT() GROUP BY query on app sessions. -type AppSessionCount struct { - Application string `gorm:"column:application_id"` - Status app.AppSessionStatus `gorm:"column:status"` - Count uint64 `gorm:"column:count"` -} - -// CountAppSessionsByStatus returns app session counts grouped by (application, status). -func (s *DBStore) CountAppSessionsByStatus() ([]AppSessionCount, error) { - var results []AppSessionCount - err := s.db.Model(&AppSessionV1{}). - Select("application_id, status, COUNT(id) as count"). - Group("application_id, status"). - Find(&results).Error - if err != nil { - return nil, fmt.Errorf("failed to count app sessions: %w", err) - } - return results, nil -} - // UpdateAppSession updates existing session data with optimistic locking. func (s *DBStore) UpdateAppSession(session app.AppSessionV1) error { return s.db.Transaction(func(tx *gorm.DB) error { diff --git a/clearnode/store/database/channel.go b/clearnode/store/database/channel.go index 01cb21d54..3979fa95a 100644 --- a/clearnode/store/database/channel.go +++ b/clearnode/store/database/channel.go @@ -152,27 +152,6 @@ func (s *DBStore) GetUserChannels(wallet string, status *core.ChannelStatus, ass return channels, uint32(totalCount), nil } -// ChannelCount holds the result of a COUNT() GROUP BY query on channels. -type ChannelCount struct { - Asset string `gorm:"column:asset"` - Status core.ChannelStatus `gorm:"column:status"` - Count uint64 `gorm:"column:count"` -} - -// CountChannelsByStatus returns channel counts grouped by (asset, status). -func (s *DBStore) CountChannelsByStatus() ([]ChannelCount, error) { - var results []ChannelCount - err := s.db.Raw(` - SELECT asset, status, COUNT(*) as count - FROM channels - GROUP BY asset, status - `).Scan(&results).Error - if err != nil { - return nil, fmt.Errorf("failed to count channels: %w", err) - } - return results, nil -} - // UpdateChannel persists changes to a channel's metadata (status, version, etc). func (s *DBStore) UpdateChannel(channel core.Channel) error { updates := map[string]interface{}{ diff --git a/clearnode/store/database/database.go b/clearnode/store/database/database.go index 4371a6592..82fab6270 100644 --- a/clearnode/store/database/database.go +++ b/clearnode/store/database/database.go @@ -213,7 +213,7 @@ func migratePostgres(cnf DatabaseConfig, embedMigrations embed.FS) error { } func migrateSqlite(db *gorm.DB) error { - if err := db.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &UserBalance{}, &UserStakedV1{}, &ActionLogEntryV1{}); err != nil { + if err := db.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &UserBalance{}, &UserStakedV1{}, &ActionLogEntryV1{}, &LifespanMetric{}); err != nil { return err } return nil diff --git a/clearnode/store/database/interface.go b/clearnode/store/database/interface.go index 0961ffedb..06bb4cbba 100644 --- a/clearnode/store/database/interface.go +++ b/clearnode/store/database/interface.go @@ -192,11 +192,25 @@ type DatabaseStore interface { // --- Metric Aggregation --- - // CountAppSessionsByStatus returns app session counts grouped by (application, status). - CountAppSessionsByStatus() ([]AppSessionCount, error) + // CountActiveUsers returns distinct user counts per asset within the given window. + CountActiveUsers(window time.Duration) ([]ActiveCountByLabel, error) - // CountChannelsByStatus returns channel counts grouped by (asset, status). - CountChannelsByStatus() ([]ChannelCount, error) + // CountActiveAppSessions returns app session counts per application within the given window. + CountActiveAppSessions(window time.Duration) ([]ActiveCountByLabel, error) + + // --- Lifespan Metric Operations --- + + // GetLifetimeMetricLastTimestamp returns the most recent last_timestamp among all metrics with the given name. + GetLifetimeMetricLastTimestamp(name string) (time.Time, error) + + // GetAppSessionsCountByLabels computes app session count deltas, upserts as lifespan metrics, and returns updated totals. + GetAppSessionsCountByLabels() ([]AppSessionCount, error) + + // GetChannelsCountByLabels computes channel count deltas, upserts as lifespan metrics, and returns updated totals. + GetChannelsCountByLabels() ([]ChannelCount, error) + + // GetTotalValueLocked computes TVL deltas by domain (channels, app_sessions) and asset, upserts as lifespan metrics, and returns updated totals. + GetTotalValueLocked() ([]TotalValueLocked, error) // --- User Staked Operations --- diff --git a/clearnode/store/database/lifespan_metric.go b/clearnode/store/database/lifespan_metric.go new file mode 100644 index 000000000..db3498d9d --- /dev/null +++ b/clearnode/store/database/lifespan_metric.go @@ -0,0 +1,439 @@ +package database + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/nitrolite/pkg/app" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/shopspring/decimal" + "gorm.io/datatypes" + "gorm.io/gorm" +) + +type LifespanMetric struct { + ID string `gorm:"column:id;primaryKey;size:66"` + Name string `gorm:"column:name;not null"` + Labels datatypes.JSON `gorm:"column:labels;type:text"` + Value decimal.Decimal `gorm:"column:value;type:varchar(78);not null"` + LastTimestamp time.Time `gorm:"column:last_timestamp;not null"` + UpdatedAt time.Time +} + +func (LifespanMetric) TableName() string { + return "lifespan_metrics" +} + +// ChannelCount holds the result of a COUNT() GROUP BY query on channels. +type ChannelCount struct { + Asset string `gorm:"column:asset"` + Status core.ChannelStatus `gorm:"column:status"` + Count uint64 `gorm:"column:count"` + LastUpdated time.Time `gorm:"column:last_updated"` +} + +// GetChannelsCountByLabels computes channel count deltas since last processed timestamp, +// upserts them as lifespan metrics, and returns the updated totals. +func (s *DBStore) GetChannelsCountByLabels() ([]ChannelCount, error) { + metricName := "channels_total" + + lastProcessedTimestamp, err := s.GetLifetimeMetricLastTimestamp(metricName) + if err != nil { + return nil, fmt.Errorf("failed to get last processed timestamp: %w", err) + } + + var deltas []ChannelCount + err = s.db.Raw(` + SELECT asset, + status AS status, + COUNT(channel_id)::bigint AS count, + MAX(updated_at) AS last_updated + FROM channels + WHERE updated_at > ? + GROUP BY asset, status + `, lastProcessedTimestamp).Scan(&deltas).Error + if err != nil { + return nil, fmt.Errorf("failed to compute channel deltas: %w", err) + } + + if len(deltas) > 0 { + now := time.Now() + valuesSQL := make([]string, 0, len(deltas)) + args := make([]any, 0, len(deltas)*6) + + for i, d := range deltas { + labelsMap := map[string]string{ + "asset": d.Asset, + "status": d.Status.String(), + } + labelsJSON, err := json.Marshal(labelsMap) + if err != nil { + return nil, fmt.Errorf("failed to marshal labels for asset=%s status=%s: %w", d.Asset, d.Status, err) + } + + id, err := getMetricID(metricName, "asset", d.Asset, "status", d.Status.String()) + if err != nil { + return nil, fmt.Errorf("failed to compute metric ID for asset=%s status=%s: %w", d.Asset, d.Status, err) + } + + deltaValue := decimal.NewFromUint64(d.Count) + base := i * 6 + valuesSQL = append(valuesSQL, + fmt.Sprintf("($%d,$%d,$%d,$%d,$%d,$%d)", + base+1, base+2, base+3, base+4, base+5, base+6, + ), + ) + + args = append(args, + id, // $1 + metricName, // $2 + datatypes.JSON(labelsJSON), // $3 + deltaValue, // $4 + d.LastUpdated, // $5 + now, // $6 + ) + } + + upsertSQL := fmt.Sprintf(` + INSERT INTO lifespan_metrics (id, name, labels, value, last_timestamp, updated_at) + VALUES %s + ON CONFLICT (id) DO UPDATE + SET + value = lifespan_metrics.value + EXCLUDED.value, + last_timestamp = GREATEST(lifespan_metrics.last_timestamp, EXCLUDED.last_timestamp), + updated_at = now() + `, strings.Join(valuesSQL, ",")) + + if err := s.db.Exec(upsertSQL, args...).Error; err != nil { + return nil, fmt.Errorf("failed to upsert lifespan metrics: %w", err) + } + } + + var results []ChannelCount + err = s.db.Raw(` + SELECT labels->>'asset' AS asset, + labels->>'status' AS status, + value::bigint AS count, + last_timestamp AS last_updated + FROM lifespan_metrics + WHERE name = ? + `, metricName).Scan(&results).Error + if err != nil { + return nil, fmt.Errorf("failed to read lifespan metrics: %w", err) + } + + return results, nil +} + +// AppSessionCount holds the result of a COUNT() GROUP BY query on app sessions. +type AppSessionCount struct { + Application string `gorm:"column:application_id"` + Status app.AppSessionStatus `gorm:"column:status"` + Count uint64 `gorm:"column:count"` + LastUpdated time.Time `gorm:"column:last_updated"` +} + +// GetAppSessionsCountByLabels computes app session count deltas since last processed timestamp, +// upserts them as lifespan metrics, and returns the updated totals. +func (s *DBStore) GetAppSessionsCountByLabels() ([]AppSessionCount, error) { + metricName := "app_sessions_total" + + lastProcessedTimestamp, err := s.GetLifetimeMetricLastTimestamp(metricName) + if err != nil { + return nil, fmt.Errorf("failed to get last processed timestamp: %w", err) + } + + // 2) Compute deltas since lastProcessedTimestamp. + var deltas []AppSessionCount + err = s.db.Raw(` + SELECT application_id, + status AS status, + COUNT(id)::bigint AS count, + MAX(updated_at) AS last_updated + FROM app_sessions_v1 + WHERE updated_at > ? + GROUP BY application_id, status + `, lastProcessedTimestamp).Scan(&deltas).Error + if err != nil { + return nil, fmt.Errorf("failed to compute app sessions deltas: %w", err) + } + + if len(deltas) > 0 { + now := time.Now() + valuesSQL := make([]string, 0, len(deltas)) + args := make([]any, 0, len(deltas)*6) + + for i, d := range deltas { + labelsMap := map[string]string{ + "application_id": d.Application, + "status": d.Status.String(), + } + labelsJSON, err := json.Marshal(labelsMap) + if err != nil { + return nil, fmt.Errorf("failed to marshal labels for application_id=%s status=%s: %w", d.Application, d.Status, err) + } + + id, err := getMetricID(metricName, "application_id", d.Application, "status", d.Status.String()) + if err != nil { + return nil, fmt.Errorf("failed to compute metric ID for application_id=%s status=%s: %w", d.Application, d.Status, err) + } + + deltaValue := decimal.NewFromUint64(d.Count) + base := i * 6 + valuesSQL = append(valuesSQL, + fmt.Sprintf("($%d,$%d,$%d,$%d,$%d,$%d)", + base+1, base+2, base+3, base+4, base+5, base+6, + ), + ) + + args = append(args, + id, // $1 + metricName, // $2 + datatypes.JSON(labelsJSON), // $3 + deltaValue, // $4 + d.LastUpdated, // $5 + now, // $6 + ) + } + + upsertSQL := fmt.Sprintf(` + INSERT INTO lifespan_metrics (id, name, labels, value, last_timestamp, updated_at) + VALUES %s + ON CONFLICT (id) DO UPDATE + SET + value = lifespan_metrics.value + EXCLUDED.value, + last_timestamp = GREATEST(lifespan_metrics.last_timestamp, EXCLUDED.last_timestamp), + updated_at = now() + `, strings.Join(valuesSQL, ",")) + + if err := s.db.Exec(upsertSQL, args...).Error; err != nil { + return nil, fmt.Errorf("failed to upsert lifespan metrics: %w", err) + } + } + + var results []AppSessionCount + err = s.db.Raw(` + SELECT labels->>'application_id' AS application_id, + labels->>'status' AS status, + value::bigint AS count, + last_timestamp AS last_updated + FROM lifespan_metrics + WHERE name = ? + `, metricName).Scan(&results).Error + if err != nil { + return nil, fmt.Errorf("failed to read lifespan metrics: %w", err) + } + + return results, nil +} + +// TotalValueLocked holds the total value locked for a given asset, along with the last update timestamp. +type TotalValueLocked struct { + Asset string `gorm:"column:asset"` + Domain string `gorm:"column:domain"` + Value decimal.Decimal `gorm:"column:value"` + LastUpdated time.Time `gorm:"column:last_updated"` +} + +func (s *DBStore) GetTotalValueLocked() ([]TotalValueLocked, error) { + metricName := "total_value_locked" + + lastProcessedTimestamp, err := s.GetLifetimeMetricLastTimestamp(metricName) + if err != nil { + return nil, fmt.Errorf("failed to get last processed timestamp: %w", err) + } + + // Compute net TVL deltas since lastProcessedTimestamp: + // - channels: deposits (tx_type=10) minus withdrawals (tx_type=11) + // - app_sessions: commits (tx_type=40) minus releases (tx_type=41) + var deltas []TotalValueLocked + err = s.db.Raw(` + SELECT domain, asset_symbol AS asset, SUM(net) AS value, MAX(created_at) AS last_updated + FROM ( + SELECT 'channels' AS domain, asset_symbol, + CASE WHEN tx_type = ? THEN amount ELSE -amount END AS net, + created_at + FROM transactions + WHERE tx_type IN (?, ?) AND created_at > ? + UNION ALL + SELECT 'app_sessions' AS domain, asset_symbol, + CASE WHEN tx_type = ? THEN amount ELSE -amount END AS net, + created_at + FROM transactions + WHERE tx_type IN (?, ?) AND created_at > ? + ) t + GROUP BY domain, asset_symbol + `, + core.TransactionTypeHomeDeposit, core.TransactionTypeHomeDeposit, core.TransactionTypeHomeWithdrawal, lastProcessedTimestamp, + core.TransactionTypeCommit, core.TransactionTypeCommit, core.TransactionTypeRelease, lastProcessedTimestamp, + ).Scan(&deltas).Error + if err != nil { + return nil, fmt.Errorf("failed to compute TVL deltas: %w", err) + } + + if len(deltas) > 0 { + now := time.Now() + valuesSQL := make([]string, 0, len(deltas)) + args := make([]any, 0, len(deltas)*6) + + for i, d := range deltas { + labelsMap := map[string]string{ + "domain": d.Domain, + "asset": d.Asset, + } + labelsJSON, err := json.Marshal(labelsMap) + if err != nil { + return nil, fmt.Errorf("failed to marshal labels for domain=%s asset=%s: %w", d.Domain, d.Asset, err) + } + + id, err := getMetricID(metricName, "domain", d.Domain, "asset", d.Asset) + if err != nil { + return nil, fmt.Errorf("failed to compute metric ID for domain=%s asset=%s: %w", d.Domain, d.Asset, err) + } + + base := i * 6 + valuesSQL = append(valuesSQL, + fmt.Sprintf("($%d,$%d,$%d,$%d,$%d,$%d)", + base+1, base+2, base+3, base+4, base+5, base+6, + ), + ) + + args = append(args, + id, // $1 + metricName, // $2 + datatypes.JSON(labelsJSON), // $3 + d.Value, // $4 + d.LastUpdated, // $5 + now, // $6 + ) + } + + upsertSQL := fmt.Sprintf(` + INSERT INTO lifespan_metrics (id, name, labels, value, last_timestamp, updated_at) + VALUES %s + ON CONFLICT (id) DO UPDATE + SET + value = lifespan_metrics.value + EXCLUDED.value, + last_timestamp = GREATEST(lifespan_metrics.last_timestamp, EXCLUDED.last_timestamp), + updated_at = now() + `, strings.Join(valuesSQL, ",")) + + if err := s.db.Exec(upsertSQL, args...).Error; err != nil { + return nil, fmt.Errorf("failed to upsert lifespan metrics: %w", err) + } + } + + var results []TotalValueLocked + err = s.db.Raw(` + SELECT labels->>'domain' AS domain, + labels->>'asset' AS asset, + value, + last_timestamp AS last_updated + FROM lifespan_metrics + WHERE name = ? + `, metricName).Scan(&results).Error + if err != nil { + return nil, fmt.Errorf("failed to read lifespan metrics: %w", err) + } + + return results, nil +} + +// CountActiveUsers returns the number of distinct users who had channel state updates +// within the given duration, grouped by asset. If asset is empty, counts across all assets. +// ActiveCountByLabel holds a count grouped by a label (asset or application_id). +type ActiveCountByLabel struct { + Label string `gorm:"column:label"` + Count uint64 `gorm:"column:count"` +} + +// CountActiveUsers returns distinct user counts per asset and an "all" aggregate +// for users with channel state updates within the given window. +func (s *DBStore) CountActiveUsers(window time.Duration) ([]ActiveCountByLabel, error) { + since := time.Now().Add(-window) + + var results []ActiveCountByLabel + err := s.db.Raw(` + SELECT asset AS label, COUNT(DISTINCT user_wallet) AS count + FROM user_balances + WHERE updated_at > ? + GROUP BY asset + `, since).Scan(&results).Error + if err != nil { + return nil, fmt.Errorf("failed to count active users: %w", err) + } + + // "ALL" aggregate: distinct users across all assets. + var total uint64 + err = s.db.Model(&UserBalance{}). + Select("COUNT(DISTINCT user_wallet)"). + Where("updated_at > ?", since). + Scan(&total).Error + if err != nil { + return nil, fmt.Errorf("failed to count total active users: %w", err) + } + + results = append(results, ActiveCountByLabel{Label: "ALL", Count: total}) + return results, nil +} + +// CountActiveAppSessions returns app session counts per application within the given window. +func (s *DBStore) CountActiveAppSessions(window time.Duration) ([]ActiveCountByLabel, error) { + since := time.Now().Add(-window) + + var results []ActiveCountByLabel + err := s.db.Raw(` + SELECT application_id AS label, COUNT(id) AS count + FROM app_sessions_v1 + WHERE updated_at > ? + GROUP BY application_id + `, since).Scan(&results).Error + if err != nil { + return nil, fmt.Errorf("failed to count active app sessions: %w", err) + } + + return results, nil +} + +// GetLifetimeMetricLastTimestamp returns the most recent last_timestamp among all metrics with the given name. +func (s *DBStore) GetLifetimeMetricLastTimestamp(name string) (time.Time, error) { + var metric LifespanMetric + err := s.db.Where("name = ?", name). + Order("last_timestamp DESC"). + First(&metric).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return time.Time{}, nil + } + return time.Time{}, fmt.Errorf("failed to get metric last timestamp: %w", err) + } + + return metric.LastTimestamp, nil +} + +func getMetricID(name string, labels ...string) (string, error) { + var labelsArray = []string{} + labelsArray = append(labelsArray, labels...) + + stringTy, _ := abi.NewType("string", "", nil) + stringSliceTy, _ := abi.NewType("string[]", "", nil) + args := abi.Arguments{ + {Type: stringTy}, // name + {Type: stringSliceTy}, // labels array + } + + packed, err := args.Pack( + name, + labelsArray, + ) + if err != nil { + return "", fmt.Errorf("failed to pack app session request: %w", err) + } + + return hexutil.Encode(crypto.Keccak256(packed)), nil +} diff --git a/clearnode/store/database/lifespan_metric_test.go b/clearnode/store/database/lifespan_metric_test.go new file mode 100644 index 000000000..2ca2521b3 --- /dev/null +++ b/clearnode/store/database/lifespan_metric_test.go @@ -0,0 +1,243 @@ +package database + +import ( + "testing" + "time" + + "github.com/layer-3/nitrolite/pkg/app" + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetMetricID(t *testing.T) { + t.Run("deterministic ID", func(t *testing.T) { + id1, err := getMetricID("test_metric", "key1", "val1") + require.NoError(t, err) + + id2, err := getMetricID("test_metric", "key1", "val1") + require.NoError(t, err) + + assert.Equal(t, id1, id2) + }) + + t.Run("different labels produce different IDs", func(t *testing.T) { + id1, err := getMetricID("test_metric", "key1", "val1") + require.NoError(t, err) + + id2, err := getMetricID("test_metric", "key1", "val2") + require.NoError(t, err) + + assert.NotEqual(t, id1, id2) + }) + + t.Run("different names produce different IDs", func(t *testing.T) { + id1, err := getMetricID("metric_a") + require.NoError(t, err) + + id2, err := getMetricID("metric_b") + require.NoError(t, err) + + assert.NotEqual(t, id1, id2) + }) + + t.Run("no labels", func(t *testing.T) { + id, err := getMetricID("metric_no_labels") + require.NoError(t, err) + assert.NotEmpty(t, id) + }) + + t.Run("ID starts with 0x", func(t *testing.T) { + id, err := getMetricID("test") + require.NoError(t, err) + assert.Equal(t, "0x", id[:2]) + }) +} + +func TestGetLifetimeMetricLastTimestamp(t *testing.T) { + t.Run("no metrics returns zero time", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := &DBStore{db: db} + + ts, err := store.GetLifetimeMetricLastTimestamp("nonexistent") + require.NoError(t, err) + assert.True(t, ts.IsZero()) + }) + + t.Run("returns most recent timestamp", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := &DBStore{db: db} + + ts1 := time.Now().Add(-2 * time.Hour).Truncate(time.Second) + ts2 := time.Now().Add(-1 * time.Hour).Truncate(time.Second) + ts3 := time.Now().Truncate(time.Second) + + db.Create(&LifespanMetric{ID: "id-a", Name: "my_metric", Value: decimal.NewFromInt(1), LastTimestamp: ts1}) + db.Create(&LifespanMetric{ID: "id-b", Name: "my_metric", Value: decimal.NewFromInt(2), LastTimestamp: ts3}) + db.Create(&LifespanMetric{ID: "id-c", Name: "my_metric", Value: decimal.NewFromInt(3), LastTimestamp: ts2}) + + latest, err := store.GetLifetimeMetricLastTimestamp("my_metric") + require.NoError(t, err) + assert.Equal(t, ts3.UTC(), latest.UTC()) + }) + + t.Run("scoped to metric name", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := &DBStore{db: db} + + tsOld := time.Now().Add(-time.Hour).Truncate(time.Second) + tsNew := time.Now().Truncate(time.Second) + + db.Create(&LifespanMetric{ID: "id-1", Name: "metric_a", Value: decimal.NewFromInt(1), LastTimestamp: tsOld}) + db.Create(&LifespanMetric{ID: "id-2", Name: "metric_b", Value: decimal.NewFromInt(1), LastTimestamp: tsNew}) + + latest, err := store.GetLifetimeMetricLastTimestamp("metric_a") + require.NoError(t, err) + assert.Equal(t, tsOld.UTC(), latest.UTC()) + }) +} + +func TestCountActiveUsers(t *testing.T) { + t.Run("no data returns only ALL with zero", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := &DBStore{db: db} + + results, err := store.CountActiveUsers(24 * time.Hour) + require.NoError(t, err) + + require.Len(t, results, 1) + assert.Equal(t, "ALL", results[0].Label) + assert.Equal(t, uint64(0), results[0].Count) + }) + + t.Run("counts distinct users per asset", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := &DBStore{db: db} + + now := time.Now() + db.Create(&UserBalance{UserWallet: "0xuser1", Asset: "USDC", Balance: decimal.NewFromInt(100), UpdatedAt: now}) + db.Create(&UserBalance{UserWallet: "0xuser2", Asset: "USDC", Balance: decimal.NewFromInt(200), UpdatedAt: now}) + db.Create(&UserBalance{UserWallet: "0xuser1", Asset: "ETH", Balance: decimal.NewFromInt(50), UpdatedAt: now}) + + results, err := store.CountActiveUsers(24 * time.Hour) + require.NoError(t, err) + + require.Len(t, results, 3) + + countByLabel := make(map[string]uint64) + for _, r := range results { + countByLabel[r.Label] = r.Count + } + + assert.Equal(t, uint64(2), countByLabel["USDC"]) + assert.Equal(t, uint64(1), countByLabel["ETH"]) + assert.Equal(t, uint64(2), countByLabel["ALL"]) + }) + + t.Run("respects time window", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := &DBStore{db: db} + + old := time.Now().Add(-48 * time.Hour) + recent := time.Now() + db.Create(&UserBalance{UserWallet: "0xold", Asset: "USDC", Balance: decimal.NewFromInt(100), UpdatedAt: old}) + db.Create(&UserBalance{UserWallet: "0xnew", Asset: "USDC", Balance: decimal.NewFromInt(200), UpdatedAt: recent}) + + results, err := store.CountActiveUsers(24 * time.Hour) + require.NoError(t, err) + + countByLabel := make(map[string]uint64) + for _, r := range results { + countByLabel[r.Label] = r.Count + } + + assert.Equal(t, uint64(1), countByLabel["USDC"]) + assert.Equal(t, uint64(1), countByLabel["ALL"]) + }) +} + +func TestCountActiveAppSessions(t *testing.T) { + t.Run("no data returns empty", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := &DBStore{db: db} + + results, err := store.CountActiveAppSessions(24 * time.Hour) + require.NoError(t, err) + assert.Empty(t, results) + }) + + t.Run("counts sessions per application", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := &DBStore{db: db} + + now := time.Now() + db.Create(&AppSessionV1{ID: "s1", ApplicationID: "app1", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 1, UpdatedAt: now}) + db.Create(&AppSessionV1{ID: "s2", ApplicationID: "app1", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 2, UpdatedAt: now}) + db.Create(&AppSessionV1{ID: "s3", ApplicationID: "app2", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 1, UpdatedAt: now}) + + results, err := store.CountActiveAppSessions(24 * time.Hour) + require.NoError(t, err) + + countByLabel := make(map[string]uint64) + for _, r := range results { + countByLabel[r.Label] = r.Count + } + + assert.Equal(t, uint64(2), countByLabel["app1"]) + assert.Equal(t, uint64(1), countByLabel["app2"]) + }) + + t.Run("respects time window", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := &DBStore{db: db} + + old := time.Now().Add(-48 * time.Hour) + recent := time.Now() + db.Create(&AppSessionV1{ID: "s1", ApplicationID: "app1", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 1, UpdatedAt: old}) + db.Create(&AppSessionV1{ID: "s2", ApplicationID: "app1", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 2, UpdatedAt: recent}) + + results, err := store.CountActiveAppSessions(24 * time.Hour) + require.NoError(t, err) + + countByLabel := make(map[string]uint64) + for _, r := range results { + countByLabel[r.Label] = r.Count + } + + assert.Equal(t, uint64(1), countByLabel["app1"]) + }) + + t.Run("multiple applications with mixed statuses", func(t *testing.T) { + db, cleanup := SetupTestDB(t) + defer cleanup() + store := &DBStore{db: db} + + now := time.Now() + db.Create(&AppSessionV1{ID: "s1", ApplicationID: "app1", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 1, UpdatedAt: now}) + db.Create(&AppSessionV1{ID: "s2", ApplicationID: "app1", SessionData: "{}", Status: app.AppSessionStatusClosed, Nonce: 2, UpdatedAt: now}) + db.Create(&AppSessionV1{ID: "s3", ApplicationID: "app2", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 1, UpdatedAt: now}) + db.Create(&AppSessionV1{ID: "s4", ApplicationID: "app2", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 2, UpdatedAt: now}) + db.Create(&AppSessionV1{ID: "s5", ApplicationID: "app2", SessionData: "{}", Status: app.AppSessionStatusClosed, Nonce: 3, UpdatedAt: now}) + + results, err := store.CountActiveAppSessions(24 * time.Hour) + require.NoError(t, err) + + countByLabel := make(map[string]uint64) + for _, r := range results { + countByLabel[r.Label] = r.Count + } + + // CountActiveAppSessions counts all sessions regardless of status + assert.Equal(t, uint64(2), countByLabel["app1"]) + assert.Equal(t, uint64(3), countByLabel["app2"]) + }) +} diff --git a/clearnode/store/database/testing.go b/clearnode/store/database/testing.go index c429c7441..1f54bc26a 100644 --- a/clearnode/store/database/testing.go +++ b/clearnode/store/database/testing.go @@ -54,7 +54,7 @@ func setupTestSqlite(t testing.TB) *gorm.DB { t.Fatalf("Failed to open SQLite database: %v", err) } - err = database.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &AppSessionV1{}, &AppParticipantV1{}, &BlockchainAction{}, &Channel{}, &ContractEvent{}, &State{}, &Transaction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{}, &UserStakedV1{}, &ActionLogEntryV1{}) + err = database.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &AppSessionV1{}, &AppParticipantV1{}, &BlockchainAction{}, &Channel{}, &ContractEvent{}, &State{}, &Transaction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{}, &UserStakedV1{}, &ActionLogEntryV1{}, &LifespanMetric{}) if err != nil { t.Fatalf("Failed to run migrations: %v", err) } @@ -99,7 +99,7 @@ func setupTestPostgres(ctx context.Context, t testing.TB) (*gorm.DB, testcontain t.Fatalf("Failed to open PostgreSQL database: %v", err) } - err = database.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &Transaction{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{}, &UserStakedV1{}, &ActionLogEntryV1{}) + err = database.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &Transaction{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{}, &UserStakedV1{}, &ActionLogEntryV1{}, &LifespanMetric{}) if err != nil { t.Fatalf("Failed to run migrations: %v", err) } diff --git a/pkg/app/app_session_v1.go b/pkg/app/app_session_v1.go index f7d3a500d..f83df6277 100644 --- a/pkg/app/app_session_v1.go +++ b/pkg/app/app_session_v1.go @@ -2,6 +2,8 @@ package app import ( "fmt" + "strconv" + "strings" "time" "github.com/ethereum/go-ethereum/accounts/abi" @@ -72,6 +74,45 @@ func (status AppSessionStatus) String() string { } } +func (s *AppSessionStatus) Scan(src any) error { + switch v := src.(type) { + case int64: + *s = AppSessionStatus(uint8(v)) + return nil + case int32: + *s = AppSessionStatus(uint8(v)) + return nil + case int: + *s = AppSessionStatus(uint8(v)) + return nil + case string: + return s.scanString(v) + default: + return fmt.Errorf("unsupported AppSessionStatus scan type %T", src) + } +} + +func (s *AppSessionStatus) scanString(v string) error { + v = strings.TrimSpace(v) + // if numeric + if n, err := strconv.Atoi(v); err == nil { + *s = AppSessionStatus(uint8(n)) + return nil + } + // else map names + switch strings.ToLower(v) { + case AppSessionStatusVoid.String(): + *s = AppSessionStatusVoid + case AppSessionStatusOpen.String(): + *s = AppSessionStatusOpen + case AppSessionStatusClosed.String(): + *s = AppSessionStatusClosed + default: + return fmt.Errorf("unknown AppSessionStatus %q", v) + } + return nil +} + // AppSessionV1 represents an application session in the V1 API. type AppSessionV1 struct { SessionID string diff --git a/pkg/core/types.go b/pkg/core/types.go index 9d28c8640..eb82f0ce1 100644 --- a/pkg/core/types.go +++ b/pkg/core/types.go @@ -2,6 +2,8 @@ package core import ( "fmt" + "strconv" + "strings" "time" "github.com/ethereum/go-ethereum/common" @@ -46,6 +48,47 @@ func (s ChannelStatus) String() string { } } +func (s *ChannelStatus) Scan(src any) error { + switch v := src.(type) { + case int64: + *s = ChannelStatus(uint8(v)) + return nil + case int32: + *s = ChannelStatus(uint8(v)) + return nil + case int: + *s = ChannelStatus(uint8(v)) + return nil + case string: + return s.scanString(v) + default: + return fmt.Errorf("unsupported ChannelStatus scan type %T", src) + } +} + +func (s *ChannelStatus) scanString(v string) error { + v = strings.TrimSpace(v) + // if numeric + if n, err := strconv.Atoi(v); err == nil { + *s = ChannelStatus(uint8(n)) + return nil + } + // else map names + switch strings.ToLower(v) { + case ChannelStatusVoid.String(): + *s = ChannelStatusVoid + case ChannelStatusOpen.String(): + *s = ChannelStatusOpen + case ChannelStatusChallenged.String(): + *s = ChannelStatusChallenged + case ChannelStatusClosed.String(): + *s = ChannelStatusClosed + default: + return fmt.Errorf("unknown ChannelStatus %q", v) + } + return nil +} + const ( INTENT_OPERATE = 0 INTENT_CLOSE = 1 From fd39408567c4793fa5872f853bf875a45f12a4e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:40:55 +0000 Subject: [PATCH 07/13] chore(gomod): bump the gomod-dependencies group with 3 updates Bumps the gomod-dependencies group with 3 updates: [github.com/ethereum/go-ethereum](https://github.com/ethereum/go-ethereum), [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) and [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go). Updates `github.com/ethereum/go-ethereum` from 1.17.0 to 1.17.1 - [Release notes](https://github.com/ethereum/go-ethereum/releases) - [Commits](https://github.com/ethereum/go-ethereum/compare/v1.17.0...v1.17.1) Updates `go.opentelemetry.io/otel` from 1.40.0 to 1.41.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.40.0...v1.41.0) Updates `go.opentelemetry.io/otel/trace` from 1.40.0 to 1.41.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.40.0...v1.41.0) --- updated-dependencies: - dependency-name: github.com/ethereum/go-ethereum dependency-version: 1.17.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gomod-dependencies - dependency-name: go.opentelemetry.io/otel dependency-version: 1.41.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gomod-dependencies - dependency-name: go.opentelemetry.io/otel/trace dependency-version: 1.41.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: gomod-dependencies ... Signed-off-by: dependabot[bot] --- go.mod | 14 +++++++------- go.sum | 24 ++++++++++++------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index 29b0bf057..0158c35d1 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.0 require ( cloud.google.com/go/kms v1.26.0 github.com/c-bata/go-prompt v0.2.6 - github.com/ethereum/go-ethereum v1.17.0 + github.com/ethereum/go-ethereum v1.17.1 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/joho/godotenv v1.5.1 @@ -14,6 +14,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.40.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 + go.yaml.in/yaml/v2 v2.4.2 golang.org/x/term v0.40.0 google.golang.org/api v0.269.0 gorm.io/driver/postgres v1.6.0 @@ -39,7 +40,6 @@ require ( github.com/pkg/term v1.2.0-beta.2 // indirect github.com/stretchr/objx v0.5.2 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/net v0.50.0 // indirect golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/time v0.14.0 // indirect @@ -73,7 +73,7 @@ require ( github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.8.4 // indirect - github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -123,15 +123,15 @@ require ( github.com/shirou/gopsutil/v4 v4.25.6 // indirect github.com/shopspring/decimal v1.4.0 github.com/sirupsen/logrus v1.9.3 // indirect - github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/supranational/blst v0.3.16 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect - go.opentelemetry.io/otel v1.40.0 - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 + go.opentelemetry.io/otel v1.41.0 + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 golang.org/x/crypto v0.48.0 // indirect diff --git a/go.sum b/go.sum index de1b65258..1ca2f8cda 100644 --- a/go.sum +++ b/go.sum @@ -110,12 +110,12 @@ github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3re github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= -github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= -github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= +github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= -github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes= -github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o= +github.com/ethereum/go-ethereum v1.17.1 h1:IjlQDjgxg2uL+GzPRkygGULPMLzcYWncEI7wbaizvho= +github.com/ethereum/go-ethereum v1.17.1/go.mod h1:7UWOVHL7K3b8RfVRea022btnzLCaanwHtBuH1jUCH/I= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= @@ -350,8 +350,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= -github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= @@ -374,20 +374,20 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= From e1459634b315d1ddea4e364003c7967a48e4bd48 Mon Sep 17 00:00:00 2001 From: Dmytro Steblyna <80773046+dimast-x@users.noreply.github.com> Date: Sat, 7 Mar 2026 15:11:34 +0000 Subject: [PATCH 08/13] YNU-810: Integrate Locking Contracts, SDK fixes, Cerebro Updates, Examples Updates, SigValidators Registration (#606) * **New Features** * Security token escrow: lock, relock, unlock, withdraw, approve, and balance queries across chains (SDK & CLI) * New CLI commands for security token management and WS URL/config handling * Added "Yellow" asset support on Ethereum Sepolia * Per-chain locking contract configuration and client support (Go/TS SDKs) * **Refactors** * Reorganized blockchain client and event handling to separate channel-hub and locking contract flows --------- Co-authored-by: Anton Filonenko --- cerebro/commands.go | 479 ++-- cerebro/main.go | 19 +- cerebro/operator.go | 383 ++- cerebro/operator_test.go | 2 +- cerebro/storage.go | 14 + clearnode/api/node_v1/utils.go | 7 +- clearnode/blockchain_worker.go | 4 +- clearnode/chart/config/v1-rc/assets.yaml | 9 +- clearnode/chart/config/v1-rc/blockchains.yaml | 6 +- clearnode/event_handlers/interface.go | 4 + clearnode/event_handlers/service.go | 16 +- clearnode/event_handlers/service_test.go | 69 + clearnode/event_handlers/testing.go | 7 + clearnode/main.go | 176 +- clearnode/store/database/database.go | 1 + clearnode/store/memory/blockchain_config.go | 44 +- .../store/memory/blockchain_config_test.go | 53 +- clearnode/store/memory/interface.go | 3 + clearnode/store/memory/memory_store.go | 46 +- pkg/blockchain/evm/app_registry_abi.go | 2154 +++++++++++++++++ pkg/blockchain/evm/base_client.go | 7 + .../evm/{client.go => blockchain_client.go} | 144 +- .../{client_test.go => blockchain_test.go} | 16 +- .../{channel_hub.go => channel_hub_abi.go} | 0 .../{reactor.go => channel_hub_reactor.go} | 179 +- pkg/blockchain/evm/client_opts.go | 6 +- pkg/blockchain/evm/{erc20.go => erc20_abi.go} | 0 pkg/blockchain/evm/init.go | 6 + pkg/blockchain/evm/interface.go | 2 +- pkg/blockchain/evm/listener.go | 17 +- pkg/blockchain/evm/listener_test.go | 7 +- pkg/blockchain/evm/locking_client.go | 232 ++ pkg/blockchain/evm/locking_reactor.go | 164 ++ pkg/blockchain/evm/locking_reactor_test.go | 127 + pkg/core/event.go | 8 + pkg/core/interface.go | 24 +- pkg/core/types.go | 9 +- pkg/rpc/types.go | 3 +- sdk/go/README.md | 39 +- sdk/go/app_session.go | 13 +- sdk/go/channel.go | 17 +- sdk/go/client.go | 253 +- sdk/go/examples/app_sessions/lifecycle.go | 333 ++- sdk/go/examples/challenge/main.go | 2 +- sdk/go/utils.go | 9 +- sdk/ts-compat/examples/lifecycle.ts | 4 +- sdk/ts-compat/examples/output.txt | 2 +- sdk/ts/README.md | 104 +- sdk/ts/examples/app_sessions/README.md | 2 +- sdk/ts/examples/app_sessions/lifecycle.ts | 324 +-- sdk/ts/examples/example-app/README.md | 4 +- sdk/ts/examples/example-app/package-lock.json | 2 +- sdk/ts/examples/example-app/src/App.tsx | 44 +- .../src/components/ActionModal.tsx | 4 +- .../example-app/src/components/StatusBar.tsx | 2 +- .../src/components/WalletDashboard.tsx | 472 +++- .../example-app/src/components/ui/button.tsx | 4 +- sdk/ts/examples/example-app/src/index.css | 84 +- .../examples/example-app/src/walletSigners.ts | 14 + sdk/ts/package-lock.json | 4 +- sdk/ts/package.json | 4 +- sdk/ts/src/app/packing.ts | 40 +- sdk/ts/src/blockchain/evm/app_registry_abi.ts | 73 + sdk/ts/src/blockchain/evm/index.ts | 2 + sdk/ts/src/blockchain/evm/locking_client.ts | 223 ++ sdk/ts/src/client.ts | 200 +- sdk/ts/src/core/types.ts | 1 + sdk/ts/src/index.ts | 2 + sdk/ts/src/rpc/api.ts | 2 + sdk/ts/src/rpc/types.ts | 2 + sdk/ts/src/signers.ts | 79 + sdk/ts/src/utils.ts | 1 + 72 files changed, 5770 insertions(+), 1042 deletions(-) create mode 100644 pkg/blockchain/evm/app_registry_abi.go create mode 100644 pkg/blockchain/evm/base_client.go rename pkg/blockchain/evm/{client.go => blockchain_client.go} (80%) rename pkg/blockchain/evm/{client_test.go => blockchain_test.go} (88%) rename pkg/blockchain/evm/{channel_hub.go => channel_hub_abi.go} (100%) rename pkg/blockchain/evm/{reactor.go => channel_hub_reactor.go} (63%) rename pkg/blockchain/evm/{erc20.go => erc20_abi.go} (100%) create mode 100644 pkg/blockchain/evm/init.go create mode 100644 pkg/blockchain/evm/locking_client.go create mode 100644 pkg/blockchain/evm/locking_reactor.go create mode 100644 pkg/blockchain/evm/locking_reactor_test.go create mode 100644 sdk/ts/src/blockchain/evm/app_registry_abi.ts create mode 100644 sdk/ts/src/blockchain/evm/locking_client.ts diff --git a/cerebro/commands.go b/cerebro/commands.go index 695620df8..a3b15fbdf 100644 --- a/cerebro/commands.go +++ b/cerebro/commands.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "crypto/rand" "fmt" + "os" "strconv" "strings" "time" @@ -15,8 +16,19 @@ import ( "github.com/layer-3/nitrolite/pkg/core" "github.com/layer-3/nitrolite/pkg/sign" sdk "github.com/layer-3/nitrolite/sdk/go" + "golang.org/x/term" ) +// readSecure reads a line from stdin without echo. Works under go-prompt's raw mode. +func readSecure() string { + bytes, err := term.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() // newline after hidden input + if err != nil { + return "" + } + return strings.TrimSpace(string(bytes)) +} + // ============================================================================ // Help & Config // ============================================================================ @@ -26,15 +38,27 @@ func (o *Operator) showHelp() { Clearnode CLI - SDK Development Tool ===================================== -SETUP COMMANDS - help Display this help message - config Display current configuration - wallet Display wallet address - import wallet Configure wallet (import or generate) - import rpc Configure blockchain RPC endpoint - -HIGH-LEVEL OPERATIONS (Smart Client) - token-balance Check on-chain token balance for your wallet +CONFIGURATION + config Display current configuration + config wallet Display wallet address + config wallet import Import existing private key + config wallet generate Generate new wallet + config wallet export Export private key to file + config rpc import Configure blockchain RPC endpoint + config node Show node info + config node set-ws-url Set clearnode WebSocket URL + config node set-home-blockchain Set home blockchain for channels + config session-key Show current session key info + config session-key generate Generate new session key + config session-key import Import existing session key + config session-key clear Clear session key, revert to default signer + config session-key register-channel-key Register channel session key + config session-key channel-keys List active channel session keys + config session-key register-app-key [apps] [sessions] Register app session key + config session-key app-keys List active app session keys + +OPERATIONS + token-balance Check on-chain token balance approve Approve token spending for deposits deposit Deposit to channel (auto-create if needed) withdraw Withdraw from channel @@ -43,58 +67,47 @@ HIGH-LEVEL OPERATIONS (Smart Client) close-channel Close home channel on-chain checkpoint Submit latest state on-chain -NODE INFORMATION (Base Client) +QUERIES ping Test node connection - node info Get node configuration chains List supported blockchains assets [chain_id] List supported assets (optionally filter by chain) - -USER QUERIES (Base Client) balances [wallet] Get user balances (defaults to configured wallet) - transactions [wallet] Get transaction history (defaults to configured wallet) - action-allowances [wallet] Get action allowances (defaults to configured wallet) - -LOW-LEVEL STATE MANAGEMENT (Base Client) - state [wallet] Get latest state (wallet defaults to configured) - home-channel [wallet] Get home channel (wallet defaults to configured) + transactions [wallet] Get transaction history + action-allowances [wallet] Get action allowances + state [wallet] Get latest state + home-channel [wallet] Get home channel escrow-channel Get escrow channel by ID APP REGISTRY app-info Show application details my-apps List your registered applications register-app [no-approval] Register a new application + app-sessions List app sessions -LOW-LEVEL APP SESSIONS (Base Client) - app-sessions List app sessions - -SESSION KEY MANAGEMENT - generate-session-key Generate or import session key (stores locally) - session-key Show current session key info - clear-session-key Clear session key, revert to default wallet signer - create-channel-session-key Register channel session key (auto-activates if stored) - channel-session-keys List active channel session keys - create-app-session-key [app_ids] [session_ids] Register app session key (IDs: comma-separated) - app-session-keys List active app session keys +SECURITY TOKEN OPERATIONS + security-token approve Approve security token spending + security-token balance [wallet] Check escrowed security token balance + security-token escrow [target_address] Escrow security tokens + security-token initiate-withdrawal Start unlock period + security-token cancel-withdrawal Cancel unlock and re-lock + security-token withdraw Withdraw unlocked security tokens OTHER + help Display this help message exit Exit the CLI EXAMPLES - import wallet - import rpc 80002 https://polygon-amoy.g.alchemy.com/v2/KEY + config wallet import + config rpc import 80002 https://polygon-amoy.g.alchemy.com/v2/KEY + config session-key generate + config session-key register-channel-key 0xabcd... 24 usdc,weth approve 80002 usdc 1000000 deposit 80002 usdc 100 transfer 0x1234... usdc 50 - balances # Uses configured wallet - balances 0x1234... # Query specific wallet - state usdc # Get state for USDC - chains - generate-session-key # Step 1: generate/import - create-channel-session-key 0xabcd... 24 usdc,weth # Step 2: register + activate - create-app-session-key 0xabcd... 24 app1,app2`) + balances`) } -func (o *Operator) showConfig(ctx context.Context) { +func (o *Operator) showConfig() { fmt.Println("Current Configuration") fmt.Println("=====================") @@ -142,14 +155,15 @@ func (o *Operator) showConfig(ctx context.Context) { } } - // Node info - nodeConfig, err := o.client.GetConfig(ctx) - if err == nil { - fmt.Printf("\nNode Info\n") - fmt.Printf(" Address: %s\n", nodeConfig.NodeAddress) - fmt.Printf(" Version: %s\n", nodeConfig.NodeVersion) - fmt.Printf(" Chains: %d\n", len(nodeConfig.Blockchains)) - } + // Node connection + fmt.Printf("Node: %s\n", o.wsURL) + + fmt.Println() + fmt.Println("Commands:") + fmt.Println(" config wallet Wallet management") + fmt.Println(" config rpc import Configure blockchain RPC") + fmt.Println(" config node Node info and connection") + fmt.Println(" config session-key Session key management") } // ============================================================================ @@ -161,7 +175,7 @@ func (o *Operator) showWallet(_ context.Context) { privateKey, err := o.store.GetPrivateKey() if err != nil { fmt.Println("ERROR: No wallet configured") - fmt.Println("INFO: Use 'import wallet' to configure wallet") + fmt.Println("INFO: Use 'config wallet import' to configure wallet") return } @@ -179,101 +193,81 @@ func (o *Operator) showWallet(_ context.Context) { fmt.Printf("Address: %s\n", address) } -// ============================================================================ -// Import Commands -// ============================================================================ - -func (o *Operator) importWallet(_ context.Context) { - fmt.Println("Wallet Configuration") - fmt.Println("====================") - fmt.Println() - fmt.Println("Choose an option:") - fmt.Println(" 1. Import existing private key") - fmt.Println(" 2. Generate new wallet") - fmt.Println() - fmt.Print("Enter choice (1 or 2): ") +func (o *Operator) exportWallet(exportPath string) { + privateKey, err := o.store.GetPrivateKey() + if err != nil { + fmt.Println("ERROR: No wallet configured") + return + } - var choice string - fmt.Scanln(&choice) - choice = strings.TrimSpace(choice) + if err := os.WriteFile(exportPath, []byte(privateKey+"\n"), 0600); err != nil { + fmt.Printf("ERROR: Failed to export wallet: %v\n", err) + return + } - var privateKey string - var signer sign.Signer - var err error + fmt.Printf("SUCCESS: Private key exported to %s\n", exportPath) + fmt.Println("WARNING: Keep this file secure and do not share it with anyone.") +} - switch choice { - case "1": - // Import existing key - fmt.Println() - fmt.Println("Import Existing Wallet") - fmt.Print("Enter private key (with or without 0x prefix): ") - fmt.Scanln(&privateKey) +// ============================================================================ +// Import Commands +// ============================================================================ - privateKey = strings.TrimSpace(privateKey) - if privateKey == "" { - fmt.Println("ERROR: Private key cannot be empty") - return - } +func (o *Operator) importWallet() { + fmt.Print("Enter private key (with or without 0x prefix): ") + privateKey := readSecure() + if privateKey == "" { + fmt.Println("ERROR: Private key cannot be empty") + return + } - // Validate by creating signer - signer, err = sign.NewEthereumRawSigner(privateKey) - if err != nil { - fmt.Printf("ERROR: Invalid private key: %v\n", err) - return - } + signer, err := sign.NewEthereumRawSigner(privateKey) + if err != nil { + fmt.Printf("ERROR: Invalid private key: %v\n", err) + return + } - case "2": - // Generate new wallet - fmt.Println() - fmt.Println("Generate New Wallet") - privateKey, err = generatePrivateKey() - if err != nil { - fmt.Printf("ERROR: Failed to generate private key: %v\n", err) - return - } + if err := o.store.SetPrivateKey(privateKey); err != nil { + fmt.Printf("ERROR: Failed to save private key: %v\n", err) + return + } - signer, err = sign.NewEthereumRawSigner(privateKey) - if err != nil { - fmt.Printf("ERROR: Failed to create signer: %v\n", err) - return - } + fmt.Printf("SUCCESS: Wallet configured\n") + fmt.Printf("Address: %s\n", signer.PublicKey().Address().String()) - fmt.Println() - fmt.Println("WARNING: Save your private key securely!") - fmt.Println("=========================================") - fmt.Printf("Private Key: %s\n", privateKey) - fmt.Println("=========================================") - fmt.Println() - fmt.Print("Type 'I have saved my private key' to continue: ") + fmt.Println("Reconnecting...") + if err := o.reconnect(); err != nil { + fmt.Printf("WARNING: Failed to reconnect: %v\n", err) + fmt.Println("INFO: Restart the CLI to apply changes.") + } +} - var confirmation string - fmt.Scanln(&confirmation) - // Read the full line - if confirmation == "" { - fmt.Println("ERROR: You must confirm that you saved the private key") - return - } +func (o *Operator) generateWallet() { + privateKey, err := generatePrivateKey() + if err != nil { + fmt.Printf("ERROR: Failed to generate private key: %v\n", err) + return + } - default: - fmt.Println("ERROR: Invalid choice") + signer, err := sign.NewEthereumRawSigner(privateKey) + if err != nil { + fmt.Printf("ERROR: Failed to create signer: %v\n", err) return } - // Save to storage if err := o.store.SetPrivateKey(privateKey); err != nil { fmt.Printf("ERROR: Failed to save private key: %v\n", err) return } - fmt.Printf("SUCCESS: Wallet configured successfully\n") + fmt.Printf("SUCCESS: New wallet generated\n") fmt.Printf("Address: %s\n", signer.PublicKey().Address().String()) + fmt.Println("IMPORTANT: Run 'config wallet export' to save your private key to a file.") - if choice == "2" { - fmt.Println() - fmt.Println("Security Recommendations:") - fmt.Println(" - Store your private key in a secure location") - fmt.Println(" - Never share your private key with anyone") - fmt.Println(" - Consider using a hardware wallet for large amounts") + fmt.Println("Reconnecting...") + if err := o.reconnect(); err != nil { + fmt.Printf("WARNING: Failed to reconnect: %v\n", err) + fmt.Println("INFO: Restart the CLI to apply changes.") } } @@ -288,9 +282,14 @@ func (o *Operator) importRPC(_ context.Context, chainIDStr, rpcURL string) { fmt.Printf("ERROR: Failed to save RPC: %v\n", err) return } - // TODO: add to SDK Client dynamically fmt.Printf("SUCCESS: RPC configured for chain %d\n", chainID) + + fmt.Println("Reconnecting...") + if err := o.reconnect(); err != nil { + fmt.Printf("WARNING: Failed to reconnect: %v\n", err) + fmt.Println("INFO: Restart the CLI to apply changes.") + } } func (o *Operator) setHomeBlockchain(_ context.Context, asset, chainIDStr string) { @@ -345,7 +344,7 @@ func (o *Operator) tokenBalance(ctx context.Context, chainIDStr, asset string) { wallet := o.getImportedWalletAddress() if wallet == "" { - fmt.Println("ERROR: No wallet configured. Use 'import wallet' first.") + fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first.") return } @@ -489,6 +488,7 @@ func (o *Operator) nodeInfo(ctx context.Context) { fmt.Println("Node Information") fmt.Println("================") + fmt.Printf("WS URL: %s\n", o.wsURL) fmt.Printf("Address: %s\n", config.NodeAddress) fmt.Printf("Version: %s\n", config.NodeVersion) fmt.Printf("Chains: %d\n", len(config.Blockchains)) @@ -510,10 +510,30 @@ func (o *Operator) nodeInfo(ctx context.Context) { fmt.Println("\nSupported Blockchains:") for _, bc := range config.Blockchains { fmt.Printf(" - %s (ID: %d)\n", bc.Name, bc.ID) - fmt.Printf(" Contract: %s\n", bc.ChannelHubAddress) + fmt.Printf(" Channel Hub: %s\n", bc.ChannelHubAddress) + if bc.LockingContractAddress != "" { + fmt.Printf(" Locking: %s\n", bc.LockingContractAddress) + } } } +func (o *Operator) setWSURL(wsURL string) { + if err := o.store.SetWSURL(wsURL); err != nil { + fmt.Printf("ERROR: Failed to save WebSocket URL: %v\n", err) + return + } + + o.wsURL = wsURL + fmt.Printf("SUCCESS: WebSocket URL set to %s\n", wsURL) + fmt.Println("INFO: Reconnecting...") + if err := o.reconnect(); err != nil { + fmt.Printf("ERROR: Failed to reconnect: %v\n", err) + fmt.Println("INFO: URL saved. Restart the CLI to connect.") + return + } + fmt.Println("SUCCESS: Connected to new node") +} + func (o *Operator) listChains(ctx context.Context) { chains, err := o.client.GetBlockchains(ctx) if err != nil { @@ -876,50 +896,34 @@ func (o *Operator) listAppSessions(ctx context.Context, wallet string) { // Session Key Management // ============================================================================ -func (o *Operator) generateSessionKey(_ context.Context) { - fmt.Println("Session Key Setup") - fmt.Println("=================") - fmt.Println() - fmt.Println("Choose an option:") - fmt.Println(" 1. Generate new session key") - fmt.Println(" 2. Import existing private key") - fmt.Println() - fmt.Print("Enter choice (1 or 2): ") - - var choice string - fmt.Scanln(&choice) - choice = strings.TrimSpace(choice) +func (o *Operator) generateSessionKey() { + privateKeyHex, err := generatePrivateKey() + if err != nil { + fmt.Printf("ERROR: Failed to generate session key: %v\n", err) + return + } - var privateKeyHex string - var err error + o.storeSessionKey(privateKeyHex) +} - switch choice { - case "1": - privateKeyHex, err = generatePrivateKey() - if err != nil { - fmt.Printf("ERROR: Failed to generate session key: %v\n", err) - return - } - case "2": - fmt.Print("Enter session key private key (hex): ") - fmt.Scanln(&privateKeyHex) - privateKeyHex = strings.TrimSpace(privateKeyHex) - if privateKeyHex == "" { - fmt.Println("ERROR: Private key cannot be empty") - return - } - default: - fmt.Println("ERROR: Invalid choice") +func (o *Operator) importSessionKey() { + fmt.Print("Enter session key private key (hex): ") + privateKeyHex := readSecure() + if privateKeyHex == "" { + fmt.Println("ERROR: Private key cannot be empty") return } + o.storeSessionKey(privateKeyHex) +} + +func (o *Operator) storeSessionKey(privateKeyHex string) { signer, err := sign.NewEthereumRawSigner(privateKeyHex) if err != nil { fmt.Printf("ERROR: Invalid private key: %v\n", err) return } - // Store the session key private key locally (no metadata yet — will be set on registration) if err := o.store.SetSessionKeyPrivateKey(privateKeyHex); err != nil { fmt.Printf("ERROR: Failed to store session key: %v\n", err) return @@ -927,17 +931,11 @@ func (o *Operator) generateSessionKey(_ context.Context) { address := signer.PublicKey().Address().String() - fmt.Println() fmt.Println("SUCCESS: Session key stored locally") fmt.Printf(" Address: %s\n", address) - if choice == "1" { - fmt.Printf(" Private Key: %s\n", privateKeyHex) - fmt.Println() - fmt.Println("WARNING: Save the private key securely!") - } fmt.Println() fmt.Println("Next step: Register it on the clearnode with:") - fmt.Printf(" create-channel-session-key %s \n", address) + fmt.Printf(" config session-key register-channel-key %s \n", address) } func (o *Operator) showSessionKey() { @@ -947,7 +945,7 @@ func (o *Operator) showSessionKey() { pk, pkErr := o.store.GetSessionKeyPrivateKey() if pkErr != nil { fmt.Println("No session key configured") - fmt.Println("INFO: Use 'generate-session-key' to create one.") + fmt.Println("INFO: Use 'config session-key generate' to create one.") return } signer, sigErr := sign.NewEthereumRawSigner(pk) @@ -961,7 +959,7 @@ func (o *Operator) showSessionKey() { fmt.Println("Status: Stored locally (not yet registered on clearnode)") fmt.Println() fmt.Println("Next step: Register it with:") - fmt.Printf(" create-channel-session-key %s \n", signer.PublicKey().Address().String()) + fmt.Printf(" config session-key register-channel-key %s \n", signer.PublicKey().Address().String()) return } @@ -1008,7 +1006,7 @@ func (o *Operator) createChannelSessionKey(ctx context.Context, sessionKeyAddr, wallet := o.getImportedWalletAddress() if wallet == "" { - fmt.Println("ERROR: No wallet configured. Use 'import wallet' first.") + fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first.") return } @@ -1133,7 +1131,7 @@ func (o *Operator) createAppSessionKey(ctx context.Context, sessionKeyAddr, expi wallet := o.getImportedWalletAddress() if wallet == "" { - fmt.Println("ERROR: No wallet configured. Use 'import wallet' first.") + fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first.") return } @@ -1212,6 +1210,145 @@ func (o *Operator) listAppSessionKeys(ctx context.Context, wallet string) { } } +// ============================================================================ +// Security Token Operations +// ============================================================================ + +func (o *Operator) escrowSecurityTokens(ctx context.Context, chainIDStr, targetAddress, amountStr string) { + chainID, err := o.parseChainID(chainIDStr) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + return + } + + amount, err := o.parseAmount(amountStr) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + return + } + + // Default target to own wallet if not specified + if targetAddress == "" { + targetAddress = o.getImportedWalletAddress() + if targetAddress == "" { + fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first.") + return + } + fmt.Printf("INFO: Using configured wallet as target: %s\n", targetAddress) + } + + fmt.Printf("Escrowing %s security tokens for %s on chain %d...\n", amount.String(), targetAddress, chainID) + + txHash, err := o.client.EscrowSecurityTokens(ctx, targetAddress, chainID, amount) + if err != nil { + fmt.Printf("ERROR: Escrow failed: %v\n", err) + return + } + + fmt.Println("SUCCESS: Security tokens escrowed") + fmt.Printf("Transaction Hash: %s\n", txHash) +} + +func (o *Operator) initiateSecurityWithdrawal(ctx context.Context, chainIDStr string) { + chainID, err := o.parseChainID(chainIDStr) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + return + } + + fmt.Printf("Initiating security tokens withdrawal on chain %d...\n", chainID) + + txHash, err := o.client.InitiateSecurityTokensWithdrawal(ctx, chainID) + if err != nil { + fmt.Printf("ERROR: Initiate withdrawal failed: %v\n", err) + return + } + + fmt.Println("SUCCESS: Security tokens withdrawal initiated") + fmt.Printf("Transaction Hash: %s\n", txHash) +} + +func (o *Operator) cancelSecurityWithdrawal(ctx context.Context, chainIDStr string) { + chainID, err := o.parseChainID(chainIDStr) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + return + } + + fmt.Printf("Cancelling security tokens withdrawal on chain %d...\n", chainID) + + txHash, err := o.client.CancelSecurityTokensWithdrawal(ctx, chainID) + if err != nil { + fmt.Printf("ERROR: Cancel withdrawal failed: %v\n", err) + return + } + + fmt.Println("SUCCESS: Security tokens withdrawal cancelled (re-locked)") + fmt.Printf("Transaction Hash: %s\n", txHash) +} + +func (o *Operator) withdrawSecurityTokens(ctx context.Context, chainIDStr, destination string) { + chainID, err := o.parseChainID(chainIDStr) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + return + } + + fmt.Printf("Withdrawing security tokens to %s on chain %d...\n", destination, chainID) + + txHash, err := o.client.WithdrawSecurityTokens(ctx, chainID, destination) + if err != nil { + fmt.Printf("ERROR: Withdraw security tokens failed: %v\n", err) + return + } + + fmt.Println("SUCCESS: Security tokens withdrawn") + fmt.Printf("Transaction Hash: %s\n", txHash) +} + +func (o *Operator) approveSecurityToken(ctx context.Context, chainIDStr, amountStr string) { + chainID, err := o.parseChainID(chainIDStr) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + return + } + + amount, err := o.parseAmount(amountStr) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + return + } + + fmt.Printf("Approving %s security tokens on chain %d...\n", amount.String(), chainID) + + txHash, err := o.client.ApproveSecurityToken(ctx, chainID, amount) + if err != nil { + fmt.Printf("ERROR: Approve security token failed: %v\n", err) + return + } + + fmt.Println("SUCCESS: Security token spending approved") + fmt.Printf("Transaction Hash: %s\n", txHash) +} + +func (o *Operator) securityBalance(ctx context.Context, chainIDStr, wallet string) { + chainID, err := o.parseChainID(chainIDStr) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + return + } + + fmt.Printf("Querying security token balance for %s on chain %d...\n", wallet, chainID) + + balance, err := o.client.GetLockedBalance(ctx, chainID, wallet) + if err != nil { + fmt.Printf("ERROR: Failed to get security token balance: %v\n", err) + return + } + + fmt.Printf("Security token balance: %s\n", balance.String()) +} + // ============================================================================ // Helper Methods // ============================================================================ diff --git a/cerebro/main.go b/cerebro/main.go index 52f907d3a..4fac4f53a 100644 --- a/cerebro/main.go +++ b/cerebro/main.go @@ -12,14 +12,11 @@ import ( ) func main() { + const defaultWSURL = "wss://clearnode-v1-rc.yellow.org/ws" + log.SetFlags(0) log.SetPrefix("clearnode-cli: ") log.SetOutput(os.Stderr) - if len(os.Args) < 2 { - log.Fatalf("Usage: clearnode-cli \nExample: clearnode-cli wss://clearnode.example.com/ws") - } - - wsURL := os.Args[1] // Get config directory configDir := os.Getenv("CLEARNODE_CLI_CONFIG_DIR") @@ -42,8 +39,18 @@ func main() { log.Fatalf("failed to initialize storage: %v", err) } + // Determine WebSocket URL: CLI arg > stored > default + var wsURL string + if len(os.Args) >= 2 { + wsURL = os.Args[1] + } else if stored, err := store.GetWSURL(); err == nil { + wsURL = stored + } else { + wsURL = defaultWSURL + } + // Create operator - operator, err := NewOperator(wsURL, store) + operator, err := NewOperator(wsURL, configDir, store) if err != nil { log.Fatalf("failed to create operator: %v", err) } diff --git a/cerebro/operator.go b/cerebro/operator.go index 68ddb2761..8b2a08835 100644 --- a/cerebro/operator.go +++ b/cerebro/operator.go @@ -15,17 +15,19 @@ import ( ) type Operator struct { - wsURL string - store *Storage - client *sdk.Client - exitCh chan struct{} + wsURL string + configDir string + store *Storage + client *sdk.Client + exitCh chan struct{} } -func NewOperator(wsURL string, store *Storage) (*Operator, error) { +func NewOperator(wsURL, configDir string, store *Storage) (*Operator, error) { op := &Operator{ - wsURL: wsURL, - store: store, - exitCh: make(chan struct{}), + wsURL: wsURL, + configDir: configDir, + store: store, + exitCh: make(chan struct{}), } if err := op.connect(); err != nil { @@ -69,10 +71,29 @@ func (o *Operator) buildStateSigner(walletPrivateKey string) (core.ChannelSigner } // connect creates the SDK client with the appropriate signer. +// If no wallet is configured, a new one is automatically generated. func (o *Operator) connect() error { privateKey, err := o.store.GetPrivateKey() if err != nil { - return fmt.Errorf("no wallet imported (use 'import wallet' first): %w", err) + // Auto-generate a new wallet for first-time users + privateKey, err = generatePrivateKey() + if err != nil { + return fmt.Errorf("failed to generate wallet: %w", err) + } + if err := o.store.SetPrivateKey(privateKey); err != nil { + return fmt.Errorf("failed to save generated wallet: %w", err) + } + signer, err := sign.NewEthereumRawSigner(privateKey) + if err != nil { + return fmt.Errorf("failed to create signer: %w", err) + } + fmt.Println() + fmt.Println("Welcome! No wallet imported. A new wallet has been generated for you.") + fmt.Printf("Address: %s\n", signer.PublicKey().Address().String()) + fmt.Println() + fmt.Println("IMPORTANT: Run 'config wallet export' to save your private key to a file.") + fmt.Println("INFO: You can import a different wallet anytime with 'config wallet import'.") + fmt.Println() } stateSigner, err := o.buildStateSigner(privateKey) @@ -144,10 +165,7 @@ func (o *Operator) complete(d prompt.Document) []prompt.Suggest { return []prompt.Suggest{ // Setup {Text: "help", Description: "Show help information"}, - {Text: "config", Description: "Show current configuration"}, - {Text: "wallet", Description: "Show wallet address"}, - {Text: "import", Description: "Import wallet or blockchain RPC"}, - {Text: "set-home-blockchain", Description: "Set home blockchain for channels"}, + {Text: "config", Description: "Configuration (wallet, rpc, node, session-key)"}, // High-level operations {Text: "token-balance", Description: "Check on-chain token balance"}, @@ -161,7 +179,6 @@ func (o *Operator) complete(d prompt.Document) []prompt.Suggest { // Node information {Text: "ping", Description: "Test node connection"}, - {Text: "node", Description: "Get node information"}, {Text: "chains", Description: "List supported blockchains"}, {Text: "assets", Description: "List supported assets"}, @@ -183,33 +200,36 @@ func (o *Operator) complete(d prompt.Document) []prompt.Suggest { // App sessions (Base Client - Low-level) {Text: "app-sessions", Description: "List app sessions"}, - // Session key management - {Text: "generate-session-key", Description: "Generate or import session key (stores locally)"}, - {Text: "session-key", Description: "Show current session key info"}, - {Text: "clear-session-key", Description: "Clear session key, revert to default signer"}, - {Text: "create-channel-session-key", Description: "Register channel session key"}, - {Text: "channel-session-keys", Description: "List active channel session keys"}, - {Text: "create-app-session-key", Description: "Register app session key"}, - {Text: "app-session-keys", Description: "List active app session keys"}, + // Security token operations + {Text: "security-token", Description: "Security token operations"}, - {Text: "exit", Description: "Exit the CLI"}, +{Text: "exit", Description: "Exit the CLI"}, } } // Second level if len(args) < 3 { switch args[0] { - case "import": + case "config": return []prompt.Suggest{ - {Text: "wallet", Description: "Import private key for signing"}, - {Text: "rpc", Description: "Import blockchain RPC URL"}, + {Text: "wallet", Description: "Wallet management"}, + {Text: "rpc", Description: "RPC management"}, + {Text: "node", Description: "Node info and connection"}, + {Text: "session-key", Description: "Session key management"}, } - case "set-home-blockchain", "close-channel", "acknowledge", "checkpoint": - // set-home-blockchain , others take + case "close-channel", "acknowledge", "checkpoint": return o.getAssetSuggestions() case "token-balance", "approve", "deposit", "withdraw": - // token-balance , approve/deposit/withdraw return o.getChainSuggestions() + case "security-token": + return []prompt.Suggest{ + {Text: "approve", Description: "Approve security token spending"}, + {Text: "balance", Description: "Check escrowed security token balance"}, + {Text: "escrow", Description: "Escrow security tokens"}, + {Text: "initiate-withdrawal", Description: "Start unlock period"}, + {Text: "cancel-withdrawal", Description: "Cancel unlock and re-lock"}, + {Text: "withdraw", Description: "Withdraw unlocked security tokens"}, + } case "state", "home-channel": // state [wallet] , home-channel [wallet] // Suggest asset first (common case), wallet can be typed manually @@ -221,23 +241,40 @@ func (o *Operator) complete(d prompt.Document) []prompt.Suggest { return o.getWalletSuggestion() case "assets": return o.getChainSuggestions() - case "node": - return []prompt.Suggest{ - {Text: "info", Description: "Get node configuration"}, - } } } // Third level if len(args) < 4 { switch args[0] { - case "import": - if args[1] == "rpc" { - return o.getChainSuggestions() + case "config": + switch args[1] { + case "wallet": + return []prompt.Suggest{ + {Text: "import", Description: "Import existing private key"}, + {Text: "generate", Description: "Generate new wallet"}, + {Text: "export", Description: "Export private key to file"}, + } + case "rpc": + return []prompt.Suggest{ + {Text: "import", Description: "Import blockchain RPC URL"}, + } + case "node": + return []prompt.Suggest{ + {Text: "set-ws-url", Description: "Set clearnode WebSocket URL"}, + {Text: "set-home-blockchain", Description: "Set home blockchain for channels"}, + } + case "session-key": + return []prompt.Suggest{ + {Text: "generate", Description: "Generate new session key"}, + {Text: "import", Description: "Import existing session key"}, + {Text: "clear", Description: "Clear session key, revert to default signer"}, + {Text: "register-channel-key", Description: "Register channel session key"}, + {Text: "channel-keys", Description: "List active channel session keys"}, + {Text: "register-app-key", Description: "Register app session key"}, + {Text: "app-keys", Description: "List active app session keys"}, + } } - case "set-home-blockchain": - // set-home-blockchain - return o.getChainSuggestions() case "token-balance": // token-balance return o.getAssetSuggestions() @@ -250,6 +287,9 @@ func (o *Operator) complete(d prompt.Document) []prompt.Suggest { case "state", "home-channel": // state [wallet] — if wallet was explicitly provided, suggest asset return o.getAssetSuggestions() + case "security-token": + // security-token ... + return o.getChainSuggestions() case "escrow-channel": // Escrow channel ID (no suggestion) return nil @@ -259,9 +299,24 @@ func (o *Operator) complete(d prompt.Document) []prompt.Suggest { // Fourth level if len(args) < 5 { switch args[0] { - case "create-channel-session-key": - // Fourth arg is assets (comma-separated) - return o.getAssetSuggestions() + case "config": + switch args[1] { + case "rpc": + if args[2] == "import" { + return o.getChainSuggestions() + } + case "node": + if args[2] == "set-home-blockchain" { + return o.getAssetSuggestions() + } + } + } + } + + // Fifth level + if len(args) < 6 { + if args[0] == "config" && args[1] == "node" && args[2] == "set-home-blockchain" { + return o.getChainSuggestions() } } @@ -281,33 +336,125 @@ func (o *Operator) Execute(s string) { case "help": o.showHelp() case "config": - o.showConfig(ctx) - case "wallet": - o.showWallet(ctx) - case "import": if len(args) < 2 { - fmt.Println("ERROR: Usage: import ...") + o.showConfig() return } switch args[1] { case "wallet": - o.importWallet(ctx) + if len(args) < 3 { + o.showWallet(ctx) + return + } + switch args[2] { + case "import": + o.importWallet() + case "generate": + o.generateWallet() + case "export": + if len(args) < 4 { + fmt.Println("ERROR: Usage: config wallet export ") + return + } + o.exportWallet(args[3]) + default: + fmt.Printf("ERROR: Unknown wallet command: %s\n", args[2]) + fmt.Println("Usage: config wallet [import|generate|export]") + } case "rpc": - if len(args) < 4 { - fmt.Println("ERROR: Usage: import rpc ") + if len(args) < 3 { + fmt.Println("ERROR: Usage: config rpc import ") return } - o.importRPC(ctx, args[2], args[3]) + switch args[2] { + case "import": + if len(args) < 5 { + fmt.Println("ERROR: Usage: config rpc import ") + return + } + o.importRPC(ctx, args[3], args[4]) + default: + fmt.Printf("ERROR: Unknown rpc command: %s\n", args[2]) + fmt.Println("Usage: config rpc [import]") + } + case "node": + if len(args) < 3 { + o.nodeInfo(ctx) + return + } + switch args[2] { + case "set-ws-url": + if len(args) < 4 { + fmt.Println("ERROR: Usage: config node set-ws-url ") + return + } + o.setWSURL(args[3]) + case "set-home-blockchain": + if len(args) < 5 { + fmt.Println("ERROR: Usage: config node set-home-blockchain ") + return + } + o.setHomeBlockchain(ctx, args[3], args[4]) + default: + fmt.Printf("ERROR: Unknown node command: %s\n", args[2]) + fmt.Println("Usage: config node [set-ws-url|set-home-blockchain]") + } + case "session-key": + if len(args) < 3 { + o.showSessionKey() + return + } + switch args[2] { + case "generate": + o.generateSessionKey() + case "import": + o.importSessionKey() + case "clear": + o.clearSessionKey() + case "register-channel-key": + if len(args) < 6 { + fmt.Println("ERROR: Usage: config session-key register-channel-key ") + fmt.Println("INFO: Assets are comma-separated, e.g. usdc,weth") + return + } + o.createChannelSessionKey(ctx, args[3], args[4], args[5]) + case "channel-keys": + wallet := o.getImportedWalletAddress() + if wallet == "" { + fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first.") + return + } + o.listChannelSessionKeys(ctx, wallet) + case "register-app-key": + if len(args) < 5 { + fmt.Println("ERROR: Usage: config session-key register-app-key [app_ids] [session_ids]") + fmt.Println("INFO: IDs are comma-separated. app_ids and session_ids are optional.") + return + } + appIDs := "" + sessionIDs := "" + if len(args) >= 6 { + appIDs = args[5] + } + if len(args) >= 7 { + sessionIDs = args[6] + } + o.createAppSessionKey(ctx, args[3], args[4], appIDs, sessionIDs) + case "app-keys": + wallet := o.getImportedWalletAddress() + if wallet == "" { + fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first.") + return + } + o.listAppSessionKeys(ctx, wallet) + default: + fmt.Printf("ERROR: Unknown session-key command: %s\n", args[2]) + fmt.Println("Usage: config session-key [generate|import|clear|register-channel-key|channel-keys|register-app-key|app-keys]") + } default: - fmt.Printf("ERROR: Unknown import type: %s\n", args[1]) - } - - case "set-home-blockchain": - if len(args) < 3 { - fmt.Println("ERROR: Usage: set-home-blockchain ") - return + fmt.Printf("ERROR: Unknown config command: %s\n", args[1]) + fmt.Println("Usage: config [wallet|rpc|node|session-key]") } - o.setHomeBlockchain(ctx, args[1], args[2]) // High-level operations case "token-balance": if len(args) < 3 { @@ -361,10 +508,6 @@ func (o *Operator) Execute(s string) { // Node information case "ping": o.ping(ctx) - case "node": - if len(args) < 2 || args[1] == "info" { - o.nodeInfo(ctx) - } case "chains": o.listChains(ctx) case "assets": @@ -384,7 +527,7 @@ func (o *Operator) Execute(s string) { wallet = o.getImportedWalletAddress() if wallet == "" { fmt.Println("ERROR: Usage: balances ") - fmt.Println("INFO: No wallet configured. Use 'import wallet' first or specify a wallet address.") + fmt.Println("INFO: No wallet configured. Use 'config wallet import' first or specify a wallet address.") return } fmt.Printf("INFO: Using configured wallet: %s\n", wallet) @@ -399,7 +542,7 @@ func (o *Operator) Execute(s string) { wallet = o.getImportedWalletAddress() if wallet == "" { fmt.Println("ERROR: Usage: transactions ") - fmt.Println("INFO: No wallet configured. Use 'import wallet' first or specify a wallet address.") + fmt.Println("INFO: No wallet configured. Use 'config wallet import' first or specify a wallet address.") return } fmt.Printf("INFO: Using configured wallet: %s\n", wallet) @@ -418,7 +561,7 @@ func (o *Operator) Execute(s string) { wallet = o.getImportedWalletAddress() if wallet == "" { fmt.Println("ERROR: Usage: state ") - fmt.Println("INFO: No wallet configured. Use 'import wallet' first or specify a wallet address.") + fmt.Println("INFO: No wallet configured. Use 'config wallet import' first or specify a wallet address.") return } asset = args[1] @@ -440,7 +583,7 @@ func (o *Operator) Execute(s string) { wallet = o.getImportedWalletAddress() if wallet == "" { fmt.Println("ERROR: Usage: home-channel ") - fmt.Println("INFO: No wallet configured. Use 'import wallet' first or specify a wallet address.") + fmt.Println("INFO: No wallet configured. Use 'config wallet import' first or specify a wallet address.") return } asset = args[1] @@ -469,7 +612,7 @@ func (o *Operator) Execute(s string) { case "my-apps": wallet := o.getImportedWalletAddress() if wallet == "" { - fmt.Println("ERROR: No wallet configured. Use 'import wallet' first.") + fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first.") return } o.getApps(ctx, nil, &wallet) @@ -492,7 +635,7 @@ func (o *Operator) Execute(s string) { wallet = o.getImportedWalletAddress() if wallet == "" { fmt.Println("ERROR: Usage: action-allowances ") - fmt.Println("INFO: No wallet configured. Use 'import wallet' first or specify a wallet address.") + fmt.Println("INFO: No wallet configured. Use 'config wallet import' first or specify a wallet address.") return } fmt.Printf("INFO: Using configured wallet: %s\n", wallet) @@ -504,49 +647,71 @@ func (o *Operator) Execute(s string) { wallet := o.getImportedWalletAddress() o.listAppSessions(ctx, wallet) - // Session key management - case "generate-session-key": - o.generateSessionKey(ctx) - case "session-key": - o.showSessionKey() - case "clear-session-key": - o.clearSessionKey() - case "create-channel-session-key": - if len(args) < 4 { - fmt.Println("ERROR: Usage: create-channel-session-key ") - fmt.Println("INFO: Assets are comma-separated, e.g. usdc,weth") - return - } - o.createChannelSessionKey(ctx, args[1], args[2], args[3]) - case "channel-session-keys": - wallet := o.getImportedWalletAddress() - if wallet == "" { - fmt.Println("ERROR: No wallet configured. Use 'import wallet' first.") - return - } - o.listChannelSessionKeys(ctx, wallet) - case "create-app-session-key": - if len(args) < 3 { - fmt.Println("ERROR: Usage: create-app-session-key [app_ids] [session_ids]") - fmt.Println("INFO: IDs are comma-separated. app_ids and session_ids are optional.") + + // Security token operations + case "security-token": + if len(args) < 2 { + fmt.Println("ERROR: Usage: security-token ...") + fmt.Println("Commands: approve, balance, escrow, initiate-withdrawal, cancel-withdrawal, withdraw") return } - appIDs := "" - sessionIDs := "" - if len(args) >= 4 { - appIDs = args[3] - } - if len(args) >= 5 { - sessionIDs = args[4] - } - o.createAppSessionKey(ctx, args[1], args[2], appIDs, sessionIDs) - case "app-session-keys": - wallet := o.getImportedWalletAddress() - if wallet == "" { - fmt.Println("ERROR: No wallet configured. Use 'import wallet' first.") - return + switch args[1] { + case "approve": + if len(args) < 4 { + fmt.Println("ERROR: Usage: security-token approve ") + return + } + o.approveSecurityToken(ctx, args[2], args[3]) + case "escrow": + if len(args) < 4 { + fmt.Println("ERROR: Usage: security-token escrow [target_address] ") + fmt.Println("INFO: If target_address is omitted, your own wallet is used.") + return + } + if len(args) >= 5 { + o.escrowSecurityTokens(ctx, args[2], args[3], args[4]) + } else { + o.escrowSecurityTokens(ctx, args[2], "", args[3]) + } + case "initiate-withdrawal": + if len(args) < 3 { + fmt.Println("ERROR: Usage: security-token initiate-withdrawal ") + return + } + o.initiateSecurityWithdrawal(ctx, args[2]) + case "cancel-withdrawal": + if len(args) < 3 { + fmt.Println("ERROR: Usage: security-token cancel-withdrawal ") + return + } + o.cancelSecurityWithdrawal(ctx, args[2]) + case "withdraw": + if len(args) < 4 { + fmt.Println("ERROR: Usage: security-token withdraw ") + return + } + o.withdrawSecurityTokens(ctx, args[2], args[3]) + case "balance": + if len(args) < 3 { + fmt.Println("ERROR: Usage: security-token balance [wallet_address]") + return + } + wallet := "" + if len(args) >= 4 { + wallet = args[3] + } else { + wallet = o.getImportedWalletAddress() + if wallet == "" { + fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first or specify a wallet address.") + return + } + fmt.Printf("INFO: Using configured wallet: %s\n", wallet) + } + o.securityBalance(ctx, args[2], wallet) + default: + fmt.Printf("ERROR: Unknown security-token command: %s\n", args[1]) + fmt.Println("Commands: approve, balance, escrow, initiate-withdrawal, cancel-withdrawal, withdraw") } - o.listAppSessionKeys(ctx, wallet) case "exit": fmt.Println("Exiting...") diff --git a/cerebro/operator_test.go b/cerebro/operator_test.go index 69f8d1275..53e338d3e 100644 --- a/cerebro/operator_test.go +++ b/cerebro/operator_test.go @@ -152,7 +152,7 @@ func TestOperator_Connect_Failure(t *testing.T) { require.NoError(t, err) // Attempt to connect to a non-existent server - _, err = NewOperator("ws://localhost:12345/nonexistent", s) + _, err = NewOperator("ws://localhost:12345/nonexistent", t.TempDir(), s) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to connect to clearnode") } diff --git a/cerebro/storage.go b/cerebro/storage.go index 1165d64f0..2939a2cbb 100644 --- a/cerebro/storage.go +++ b/cerebro/storage.go @@ -35,6 +35,20 @@ func NewStorage(path string) (*Storage, error) { return &Storage{db: db}, nil } +func (s *Storage) SetWSURL(wsURL string) error { + _, err := s.db.Exec("INSERT OR REPLACE INTO config (key, value) VALUES ('ws_url', ?)", wsURL) + return err +} + +func (s *Storage) GetWSURL() (string, error) { + var wsURL string + err := s.db.QueryRow("SELECT value FROM config WHERE key = 'ws_url'").Scan(&wsURL) + if err == sql.ErrNoRows { + return "", fmt.Errorf("no WebSocket URL configured") + } + return wsURL, err +} + func (s *Storage) SetPrivateKey(privateKey string) error { _, err := s.db.Exec("INSERT OR REPLACE INTO config (key, value) VALUES ('private_key', ?)", privateKey) return err diff --git a/clearnode/api/node_v1/utils.go b/clearnode/api/node_v1/utils.go index fb6ed7041..378dd5545 100644 --- a/clearnode/api/node_v1/utils.go +++ b/clearnode/api/node_v1/utils.go @@ -10,9 +10,10 @@ import ( func mapBlockchainV1(blockchain core.Blockchain) rpc.BlockchainInfoV1 { return rpc.BlockchainInfoV1{ - Name: blockchain.Name, - BlockchainID: strconv.FormatUint(blockchain.ID, 10), - ChannelHubAddress: blockchain.ChannelHubAddress, + Name: blockchain.Name, + BlockchainID: strconv.FormatUint(blockchain.ID, 10), + ChannelHubAddress: blockchain.ChannelHubAddress, + LockingContractAddress: blockchain.LockingContractAddress, } } diff --git a/clearnode/blockchain_worker.go b/clearnode/blockchain_worker.go index a30d34b54..e63b4d9aa 100644 --- a/clearnode/blockchain_worker.go +++ b/clearnode/blockchain_worker.go @@ -38,13 +38,13 @@ const ( type BlockchainWorker struct { blockchainID uint64 - client core.Client + client core.BlockchainClient store BlockchainWorkerStore logger log.Logger metrics MetricsExporter } -func NewBlockchainWorker(blockchainID uint64, client core.Client, store BlockchainWorkerStore, logger log.Logger, m MetricsExporter) *BlockchainWorker { +func NewBlockchainWorker(blockchainID uint64, client core.BlockchainClient, store BlockchainWorkerStore, logger log.Logger, m MetricsExporter) *BlockchainWorker { return &BlockchainWorker{ blockchainID: blockchainID, client: client, diff --git a/clearnode/chart/config/v1-rc/assets.yaml b/clearnode/chart/config/v1-rc/assets.yaml index 2290bd4c1..80c7f299e 100644 --- a/clearnode/chart/config/v1-rc/assets.yaml +++ b/clearnode/chart/config/v1-rc/assets.yaml @@ -23,4 +23,11 @@ assets: - blockchain_id: 11155111 address: "0x0000000000000000000000000000000000000000" decimals: 18 - + - name: "Yellow" + symbol: "yellow" + decimals: 18 + suggested_blockchain_id: 11155111 + tokens: + - blockchain_id: 11155111 + address: "0xB1aA0ac73B5E648a57db2d9342f11c471FcC85F1" + decimals: 18 diff --git a/clearnode/chart/config/v1-rc/blockchains.yaml b/clearnode/chart/config/v1-rc/blockchains.yaml index f338ed26d..2c63d8745 100644 --- a/clearnode/chart/config/v1-rc/blockchains.yaml +++ b/clearnode/chart/config/v1-rc/blockchains.yaml @@ -1,7 +1,7 @@ -default_contract_addresses: - channel_hub: "0x09ffB9eA86F42e3e3B5E34650311d7E595dFB769" - blockchains: - name: "ethereum_sepolia" id: 11155111 channel_hub_address: "0x09ffB9eA86F42e3e3B5E34650311d7E595dFB769" + channel_hub_sig_validators: + 1: "0xae5f5f520d0edb5c2ead31381f6d1d1fc2a7c36b" + locking_contract_address: "0x89A969AE9D7e695DF948Ba744e72A97769C5C1ef" diff --git a/clearnode/event_handlers/interface.go b/clearnode/event_handlers/interface.go index c053fd9cf..7b4e011ac 100644 --- a/clearnode/event_handlers/interface.go +++ b/clearnode/event_handlers/interface.go @@ -2,6 +2,7 @@ package event_handlers import ( "github.com/layer-3/nitrolite/pkg/core" + "github.com/shopspring/decimal" ) // StoreTxHandler is a function that executes Store operations within a transaction. @@ -49,4 +50,7 @@ type Store interface { // ScheduleFinalizeEscrowWithdrawal schedules a checkpoint for an escrow withdrawal operation. // This queues the state to be submitted on-chain to finalize an escrow withdrawal. ScheduleFinalizeEscrowWithdrawal(stateID string, chainID uint64) error + + // UpdateUserStaked updates the total staked amount for a user on a specific blockchain. + UpdateUserStaked(wallet string, blockchainID uint64, amount decimal.Decimal) error } diff --git a/clearnode/event_handlers/service.go b/clearnode/event_handlers/service.go index 159d1c201..ff3dbb7b1 100644 --- a/clearnode/event_handlers/service.go +++ b/clearnode/event_handlers/service.go @@ -8,7 +8,8 @@ import ( "github.com/layer-3/nitrolite/pkg/log" ) -var _ core.BlockchainEventHandler = &EventHandlerService{} +var _ core.ChannelHubEventHandler = &EventHandlerService{} +var _ core.LockingContractEventHandler = &EventHandlerService{} // EventHandlerService processes blockchain events and updates the local database state accordingly. // It handles events from both home channels (user state channels) and escrow channels (temporary lock channels). @@ -445,3 +446,16 @@ func (s *EventHandlerService) HandleEscrowWithdrawalFinalized(ctx context.Contex return nil }) } + +func (s *EventHandlerService) HandleUserLockedBalanceUpdated(ctx context.Context, event *core.UserLockedBalanceUpdatedEvent) error { + logger := log.FromContext(ctx) + return s.useStoreInTx(func(tx Store) error { + err := tx.UpdateUserStaked(event.UserAddress, event.BlockchainID, event.Balance) + if err != nil { + return err + } + + logger.Info("handled UserLockedBalanceUpdatedEvent event", "userWallet", event.UserAddress, "blockchainID", event.BlockchainID, "balance", event.Balance) + return nil + }) +} diff --git a/clearnode/event_handlers/service_test.go b/clearnode/event_handlers/service_test.go index 63fc1b934..e5e9afb1c 100644 --- a/clearnode/event_handlers/service_test.go +++ b/clearnode/event_handlers/service_test.go @@ -2,9 +2,11 @@ package event_handlers import ( "context" + "errors" "testing" "time" + "github.com/shopspring/decimal" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -515,3 +517,70 @@ func TestHandleEscrowWithdrawalFinalized_Success(t *testing.T) { require.NoError(t, err) mockStore.AssertExpectations(t) } + +func TestHandleUserLockedBalanceUpdated_Success(t *testing.T) { + // Setup + mockStore := new(MockStore) + ctx := log.SetContextLogger(context.Background(), log.NewNoopLogger()) + + service := &EventHandlerService{ + useStoreInTx: func(handler StoreTxHandler) error { + return handler(mockStore) + }, + } + + // Test data + userWallet := "0x1234567890123456789012345678901234567890" + blockchainID := uint64(1) + balance := decimal.NewFromInt(1000) + + event := &core.UserLockedBalanceUpdatedEvent{ + UserAddress: userWallet, + BlockchainID: blockchainID, + Balance: balance, + } + + // Mock expectations + mockStore.On("UpdateUserStaked", userWallet, blockchainID, balance).Return(nil) + + // Execute + err := service.HandleUserLockedBalanceUpdated(ctx, event) + + // Assert + require.NoError(t, err) + mockStore.AssertExpectations(t) +} + +func TestHandleUserLockedBalanceUpdated_StoreError(t *testing.T) { + // Setup + mockStore := new(MockStore) + ctx := log.SetContextLogger(context.Background(), log.NewNoopLogger()) + + service := &EventHandlerService{ + useStoreInTx: func(handler StoreTxHandler) error { + return handler(mockStore) + }, + } + + // Test data + userWallet := "0x1234567890123456789012345678901234567890" + blockchainID := uint64(1) + balance := decimal.NewFromInt(500) + + event := &core.UserLockedBalanceUpdatedEvent{ + UserAddress: userWallet, + BlockchainID: blockchainID, + Balance: balance, + } + + // Mock expectations + mockStore.On("UpdateUserStaked", userWallet, blockchainID, balance).Return(errors.New("db error")) + + // Execute + err := service.HandleUserLockedBalanceUpdated(ctx, event) + + // Assert + require.Error(t, err) + require.Contains(t, err.Error(), "db error") + mockStore.AssertExpectations(t) +} diff --git a/clearnode/event_handlers/testing.go b/clearnode/event_handlers/testing.go index 5d18d64b8..aab414c32 100644 --- a/clearnode/event_handlers/testing.go +++ b/clearnode/event_handlers/testing.go @@ -1,6 +1,7 @@ package event_handlers import ( + "github.com/shopspring/decimal" "github.com/stretchr/testify/mock" "github.com/layer-3/nitrolite/pkg/core" @@ -67,3 +68,9 @@ func (m *MockStore) ScheduleFinalizeEscrowWithdrawal(stateID string, chainID uin args := m.Called(stateID, chainID) return args.Error(0) } + +// UpdateUserStaked mocks updating the total staked amount for a user +func (m *MockStore) UpdateUserStaked(wallet string, blockchainID uint64, amount decimal.Decimal) error { + args := m.Called(wallet, blockchainID, amount) + return args.Error(0) +} diff --git a/clearnode/main.go b/clearnode/main.go index 29dd4167a..3980fb3b1 100644 --- a/clearnode/main.go +++ b/clearnode/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "net/http" "os" "os/signal" @@ -18,6 +19,7 @@ import ( "github.com/layer-3/nitrolite/clearnode/store/database" "github.com/layer-3/nitrolite/clearnode/stress" "github.com/layer-3/nitrolite/pkg/blockchain/evm" + "github.com/layer-3/nitrolite/pkg/core" "github.com/layer-3/nitrolite/pkg/log" ) @@ -26,9 +28,16 @@ func main() { os.Exit(stress.Run(os.Args[2:])) } + if len(os.Args) > 1 && os.Args[1] == "operator" { + runOperatorCommand(os.Args[2:]) + return + } + bb := InitBackbone() logger := bb.Logger - ctx := context.Background() + rootCtx := context.Background() + blockchainCtx, cancelBlockchain := context.WithCancel(rootCtx) + ctx := rootCtx vl := bb.ValidationLimits rpcRouterCfg := api.RPCRouterConfig{ @@ -78,34 +87,68 @@ func main() { if err != nil { logger.Fatal("failed to connect to EVM Node") } - reactor := evm.NewReactor(b.ID, eventHandlerService, bb.DbStore.StoreContractEvent) - reactor.SetOnEventProcessed(bb.RuntimeMetrics.IncBlockchainEvent) - l := evm.NewListener(common.HexToAddress(b.ChannelHubAddress), client, b.ID, b.BlockStep, logger, reactor.HandleEvent, bb.DbStore.GetLatestEvent) - l.Listen(ctx, func(err error) { + + if b.ChannelHubAddress != "" { + // For the node itself, the node address is the signer's address + nodeAddress := bb.StateSigner.PublicKey().Address().String() + + clientOpts := []evm.ClientOption{ + evm.ClientBalanceCheck{RequireBalanceCheck: false}, + evm.ClientAllowanceCheck{RequireAllowanceCheck: false}, + } + + blockchainClient, err := evm.NewBlockchainClient(common.HexToAddress(b.ChannelHubAddress), client, bb.TxSigner, b.ID, nodeAddress, bb.MemoryStore, clientOpts...) if err != nil { - logger.Fatal("blockchain listener stopped", "error", err, "blockchainID", b.ID) + logger.Fatal("failed to create EVM client") } - }) - // For the node itself, the node address is the signer's address - nodeAddress := bb.StateSigner.PublicKey().Address().String() + sigValidators, err := bb.MemoryStore.GetChannelSigValidators(b.ID) + if err != nil { + logger.Fatal("failed to get channel signature validators from memory store", "error", err, "blockchainID", b.ID) + } - clientOpts := []evm.ClientOption{ - evm.ClientBalanceCheck{RequireBalanceCheck: false}, - evm.ClientAllowanceCheck{RequireAllowanceCheck: false}, - } + if err := ensureSigValidatorsRegistered(blockchainClient, sigValidators, true); err != nil { + logger.Fatal("failed to ensure signature validators are registered", "error", err, "blockchainID", b.ID) + } - blockchainClient, err := evm.NewClient(common.HexToAddress(b.ChannelHubAddress), client, bb.TxSigner, b.ID, nodeAddress, bb.MemoryStore, clientOpts...) - if err != nil { - logger.Fatal("failed to create EVM client") + reactor := evm.NewChannelHubReactor(b.ID, eventHandlerService, bb.DbStore.StoreContractEvent) + reactor.SetOnEventProcessed(bb.RuntimeMetrics.IncBlockchainEvent) + l := evm.NewListener(common.HexToAddress(b.ChannelHubAddress), client, b.ID, b.BlockStep, logger, reactor.HandleEvent, bb.DbStore.GetLatestEvent) + l.Listen(blockchainCtx, func(err error) { + if err != nil { + logger.Fatal("blockchain listener stopped", "error", err, "blockchainID", b.ID) + } + }) + + worker := NewBlockchainWorker(b.ID, blockchainClient, bb.DbStore, logger, bb.RuntimeMetrics) + worker.Start(blockchainCtx, func(err error) { + if err != nil { + logger.Fatal("blockchain worker stopped", "error", err, "blockchainID", b.ID) + } + }) + } else { + logger.Info("channel hub address is not configured for blockchain", "blockchainID", b.ID) } - worker := NewBlockchainWorker(b.ID, blockchainClient, bb.DbStore, logger, bb.RuntimeMetrics) - worker.Start(ctx, func(err error) { + if b.LockingContractAddress != "" { + appRegistryClient, err := evm.NewLockingClient(common.HexToAddress(b.LockingContractAddress), client, b.ID) if err != nil { - logger.Fatal("blockchain worker stopped", "error", err, "blockchainID", b.ID) + logger.Fatal("failed to create locking client", "error", err, "blockchainID", b.ID) } - }) + + reactor, err := evm.NewLockingContractReactor(b.ID, eventHandlerService, appRegistryClient.GetTokenDecimals, bb.DbStore.StoreContractEvent) + if err != nil { + logger.Fatal("failed to create app registry reactor", "error", err, "blockchainID", b.ID) + } + + reactor.SetOnEventProcessed(bb.RuntimeMetrics.IncBlockchainEvent) + l := evm.NewListener(common.HexToAddress(b.LockingContractAddress), client, b.ID, b.BlockStep, logger, reactor.HandleEvent, bb.DbStore.GetLatestEvent) + l.Listen(blockchainCtx, func(err error) { + if err != nil { + logger.Fatal("blockchain listener stopped", "error", err, "blockchainID", b.ID) + } + }) + } } go runStoreMetricsExporter(ctx, 30*time.Second, bb.DbStore, bb.StoreMetrics, logger) @@ -158,15 +201,106 @@ func main() { logger.Error("failed to shut down RPC server", "error", err) } + logger.Info("stopping blockchain listeners and workers") + cancelBlockchain() + // Close backbone resources if err := bb.Close(); err != nil { logger.Error("failed to close backbone resources", "error", err) } - // TODO: gracefully stop blockchain listeners and workers logger.Info("shutdown complete") } +func runOperatorCommand(args []string) { + if len(args) == 0 { + fmt.Println("Usage: clearnode operator ") + fmt.Println("Commands:") + fmt.Println(" address Print the operator address") + fmt.Println(" register-validators Register signature validators on-chain") + os.Exit(1) + } + + switch args[0] { + case "address": + runOperatorAddress() + case "register-validators": + runRegisterValidators() + default: + fmt.Printf("Unknown operator command: %s\n", args[0]) + os.Exit(1) + } +} + +func runOperatorAddress() { + bb := InitBackbone() + defer bb.Close() + + fmt.Println(bb.StateSigner.PublicKey().Address().String()) + time.Sleep(5 * time.Second) +} + +func runRegisterValidators() { + bb := InitBackbone() + defer bb.Close() + logger := bb.Logger + + blockchains, err := bb.MemoryStore.GetBlockchains() + if err != nil { + logger.Fatal("failed to get blockchains from memory store", "error", err) + } + + for _, b := range blockchains { + if b.ChannelHubAddress == "" { + continue + } + + rpcURL, ok := bb.BlockchainRPCs[b.ID] + if !ok { + logger.Fatal("no RPC URL configured for blockchain", "blockchainID", b.ID) + } + + client, err := ethclient.Dial(rpcURL) + if err != nil { + logger.Fatal("failed to connect to EVM Node", "blockchainID", b.ID) + } + + nodeAddress := bb.StateSigner.PublicKey().Address().String() + clientOpts := []evm.ClientOption{ + evm.ClientBalanceCheck{RequireBalanceCheck: false}, + evm.ClientAllowanceCheck{RequireAllowanceCheck: false}, + } + + blockchainClient, err := evm.NewBlockchainClient(common.HexToAddress(b.ChannelHubAddress), client, bb.TxSigner, b.ID, nodeAddress, bb.MemoryStore, clientOpts...) + if err != nil { + logger.Fatal("failed to create EVM client", "blockchainID", b.ID) + } + + sigValidators, err := bb.MemoryStore.GetChannelSigValidators(b.ID) + if err != nil { + logger.Fatal("failed to get channel signature validators from memory store", "error", err, "blockchainID", b.ID) + } + + if err := ensureSigValidatorsRegistered(blockchainClient, sigValidators, false); err != nil { + logger.Fatal("failed to register signature validators", "error", err, "blockchainID", b.ID) + } + + logger.Info("signature validators registered successfully", "blockchainID", b.ID) + } + + logger.Info("all signature validators registered") +} + +func ensureSigValidatorsRegistered(client core.BlockchainClient, validators map[uint8]string, checkOnly bool) error { + for id, addr := range validators { + if err := client.EnsureSigValidatorRegistered(id, addr, checkOnly); err != nil { + return err + } + } + + return nil +} + func runStoreMetricsExporter( ctx context.Context, fetchInterval time.Duration, diff --git a/clearnode/store/database/database.go b/clearnode/store/database/database.go index 82fab6270..bb01c3189 100644 --- a/clearnode/store/database/database.go +++ b/clearnode/store/database/database.go @@ -67,6 +67,7 @@ func connectToPostgresql(cnf DatabaseConfig, embedMigrations embed.FS) (*gorm.DB } dial := postgres.Open(dsn) + // TODO: don't print SQL in logs db, err := gorm.Open(dial, &gorm.Config{ NamingStrategy: schema.NamingStrategy{ TablePrefix: cnf.Schema + ".", // schema name diff --git a/clearnode/store/memory/blockchain_config.go b/clearnode/store/memory/blockchain_config.go index 361907249..27e89f688 100644 --- a/clearnode/store/memory/blockchain_config.go +++ b/clearnode/store/memory/blockchain_config.go @@ -6,6 +6,7 @@ import ( "path/filepath" "regexp" + "github.com/layer-3/nitrolite/pkg/core" "gopkg.in/yaml.v3" ) @@ -23,12 +24,7 @@ var ( // It contains default contract addresses that apply to all blockchains unless overridden, // and a list of individual blockchain configurations. type BlockchainsConfig struct { - DefaultContractAddresses DefaultContractAddresses `yaml:"default_contract_addresses"` - Blockchains []BlockchainConfig `yaml:"blockchains"` -} - -type DefaultContractAddresses struct { - ChannelHub string `yaml:"channel_hub,omitempty"` + Blockchains []BlockchainConfig `yaml:"blockchains"` } // BlockchainConfig represents configuration for a single blockchain. @@ -45,6 +41,10 @@ type BlockchainConfig struct { BlockStep uint64 `yaml:"block_step"` // ChannelHubAddress is the address of the ChannelHub contract on this blockchain ChannelHubAddress string `yaml:"channel_hub_address"` + // ChannelHubSigValidators maps validator IDs to the addresses of signature validators for the ChannelHub contract on this blockchain + ChannelHubSigValidators map[uint8]string `yaml:"channel_hub_sig_validators"` + // LockingContractAddress is the address of the locking contract on this blockchain + LockingContractAddress string `yaml:"locking_contract_address"` } // LoadEnabledBlockchains loads and validates blockchain configurations from a YAML file. @@ -77,10 +77,6 @@ func LoadEnabledBlockchains(configDirPath string) (map[uint64]BlockchainConfig, } func verifyBlockchainsConfig(cfg *BlockchainsConfig) error { - if addr := cfg.DefaultContractAddresses.ChannelHub; !contractAddressRegex.MatchString(addr) && addr != "" { - return fmt.Errorf("invalid default channel hub address '%s'", addr) - } - for i, bc := range cfg.Blockchains { if bc.Disabled { continue @@ -90,19 +86,33 @@ func verifyBlockchainsConfig(cfg *BlockchainsConfig) error { return fmt.Errorf("invalid blockchain name '%s', should match snake_case format", bc.Name) } - if bc.ChannelHubAddress == "" { - if cfg.DefaultContractAddresses.ChannelHub == "" { - return fmt.Errorf("missing default and blockchain-specific channel hub address for blockchain '%s'", bc.Name) - } else { - cfg.Blockchains[i].ChannelHubAddress = cfg.DefaultContractAddresses.ChannelHub - } - } else if !contractAddressRegex.MatchString(bc.ChannelHubAddress) { + if bc.ChannelHubAddress == "" && bc.LockingContractAddress == "" { + return fmt.Errorf("blockchain '%s' must specify at least one of channel_hub_address or locking_contract_address", bc.Name) + } + + if bc.ChannelHubAddress != "" && !contractAddressRegex.MatchString(bc.ChannelHubAddress) { return fmt.Errorf("invalid channel hub address '%s' for blockchain '%s'", bc.ChannelHubAddress, bc.Name) } + if bc.LockingContractAddress != "" && !contractAddressRegex.MatchString(bc.LockingContractAddress) { + return fmt.Errorf("invalid locking contract address '%s' for blockchain '%s'", bc.LockingContractAddress, bc.Name) + } + if bc.BlockStep == 0 { cfg.Blockchains[i].BlockStep = defaultBlockStep } + + if bc.ChannelHubAddress != "" && len(core.ChannelSignerTypes) > 1 { + for _, channelSignerType := range core.ChannelSignerTypes[1:] { + validatorAddress, ok := bc.ChannelHubSigValidators[uint8(channelSignerType)] + if !ok { + return fmt.Errorf("blockchain '%s' must specify a signature validator address for channel signer type %d", bc.Name, channelSignerType) + } + if !contractAddressRegex.MatchString(validatorAddress) { + return fmt.Errorf("invalid signature validator address '%s' for channel signer type %d on blockchain '%s'", validatorAddress, channelSignerType, bc.Name) + } + } + } } return nil diff --git a/clearnode/store/memory/blockchain_config_test.go b/clearnode/store/memory/blockchain_config_test.go index 4a67ea2ba..60dcb579b 100644 --- a/clearnode/store/memory/blockchain_config_test.go +++ b/clearnode/store/memory/blockchain_config_test.go @@ -17,20 +17,18 @@ func TestBlockchainConfig_verifyVariables(t *testing.T) { { name: "valid config", cfg: BlockchainsConfig{ - DefaultContractAddresses: DefaultContractAddresses{ - ChannelHub: "0x0000000000000000000000000000000000000001", - }, - Blockchains: []BlockchainConfig{ { - ID: 1, - Name: "ethereum", - ChannelHubAddress: "0x1111111111111111111111111111111111111111", - BlockStep: 10, + ID: 1, + Name: "ethereum", + ChannelHubAddress: "0x1111111111111111111111111111111111111111", + BlockStep: 10, + ChannelHubSigValidators: map[uint8]string{1: "0x3333333333333333333333333333333333333333"}, }, { - ID: 11155111, - Name: "ethereum_sepolia", + ID: 11155111, + Name: "ethereum_sepolia", + LockingContractAddress: "0x2222222222222222222222222222222222222222", }, }, }, @@ -48,7 +46,7 @@ func TestBlockchainConfig_verifyVariables(t *testing.T) { sepoliaCfg := blockchains[1] assert.Equal(t, "ethereum_sepolia", sepoliaCfg.Name) assert.Equal(t, uint64(11155111), sepoliaCfg.ID) - assert.Equal(t, "0x0000000000000000000000000000000000000001", sepoliaCfg.ChannelHubAddress) + assert.Equal(t, "0x2222222222222222222222222222222222222222", sepoliaCfg.LockingContractAddress) assert.False(t, sepoliaCfg.Disabled) assert.Equal(t, defaultBlockStep, sepoliaCfg.BlockStep) }, @@ -80,14 +78,13 @@ func TestBlockchainConfig_verifyVariables(t *testing.T) { { name: "disabled blockchain", cfg: BlockchainsConfig{ - DefaultContractAddresses: DefaultContractAddresses{ - ChannelHub: "0x0000000000000000000000000000000000000001", - }, Blockchains: []BlockchainConfig{ { - ID: 1, - Name: "ethereum", - Disabled: false, + ID: 1, + Name: "ethereum", + Disabled: false, + ChannelHubAddress: "0x1111111111111111111111111111111111111111", + ChannelHubSigValidators: map[uint8]string{1: "0x3333333333333333333333333333333333333333"}, }, { ID: 11155111, @@ -109,28 +106,6 @@ func TestBlockchainConfig_verifyVariables(t *testing.T) { assert.Equal(t, uint64(11155111), sepoliaCfg.ID) }, }, - { - name: "invalid default channel hub address", - cfg: BlockchainsConfig{ - DefaultContractAddresses: DefaultContractAddresses{ - ChannelHub: "0x0000s00000000000000000000000000000000001", - }, - }, - expectedErrorStr: "invalid default channel hub address '0x0000s00000000000000000000000000000000001'", - }, - { - name: "missing channel hub address", - cfg: BlockchainsConfig{ - Blockchains: []BlockchainConfig{ - { - ID: 1, - Name: "ethereum", - ChannelHubAddress: "", - }, - }, - }, - expectedErrorStr: "missing default and blockchain-specific channel hub address for blockchain 'ethereum'", - }, { name: "invalid channel hub address", cfg: BlockchainsConfig{ diff --git a/clearnode/store/memory/interface.go b/clearnode/store/memory/interface.go index eb80cbe44..5f0aef35d 100644 --- a/clearnode/store/memory/interface.go +++ b/clearnode/store/memory/interface.go @@ -14,6 +14,9 @@ type MemoryStore interface { // If blockchainID is provided, filters assets to only include tokens on that blockchain. GetAssets(blockchainID *uint64) ([]core.Asset, error) + // GetChannelSigValidators retrieves the channel signature validators for a specific blockchain. + GetChannelSigValidators(blockchainID uint64) (map[uint8]string, error) + // GetTokenAddress retrieves the token address for a given asset on a specific blockchain. GetTokenAddress(asset string, blockchainID uint64) (string, error) diff --git a/clearnode/store/memory/memory_store.go b/clearnode/store/memory/memory_store.go index 707207edf..18b572e86 100644 --- a/clearnode/store/memory/memory_store.go +++ b/clearnode/store/memory/memory_store.go @@ -9,27 +9,34 @@ import ( ) type MemoryStoreV1 struct { - blockchains []core.Blockchain - assets []core.Asset - supportedAssets map[string]map[uint64]string // map[asset]map[blockchain_id]string - tokenDecimals map[uint64]map[string]uint8 // map[blockchain_id]map[token_address]decimals - assetDecimals map[string]uint8 // map[asset]decimals + blockchains []core.Blockchain + assets []core.Asset + channelSigValidators map[uint64]map[uint8]string // map[blockchain_id]map[validator_id]validator_address + supportedAssets map[string]map[uint64]string // map[asset]map[blockchain_id]string + tokenDecimals map[uint64]map[string]uint8 // map[blockchain_id]map[token_address]decimals + assetDecimals map[string]uint8 // map[asset]decimals } func NewMemoryStoreV1(assetsConfig AssetsConfig, blockchainsConfig map[uint64]BlockchainConfig) (MemoryStore, error) { supportedBlockchainIDs := make(map[uint64]struct{}) blockchains := make([]core.Blockchain, 0, len(blockchainsConfig)) + channelSigValidators := make(map[uint64]map[uint8]string) for _, bc := range blockchainsConfig { if bc.Disabled { continue } - supportedBlockchainIDs[bc.ID] = struct{}{} + + if bc.ChannelHubAddress != "" { + supportedBlockchainIDs[bc.ID] = struct{}{} + channelSigValidators[bc.ID] = bc.ChannelHubSigValidators + } blockchains = append(blockchains, core.Blockchain{ - ID: bc.ID, - Name: bc.Name, - ChannelHubAddress: bc.ChannelHubAddress, - BlockStep: bc.BlockStep, + ID: bc.ID, + Name: bc.Name, + ChannelHubAddress: bc.ChannelHubAddress, + LockingContractAddress: bc.LockingContractAddress, + BlockStep: bc.BlockStep, }) } slices.SortFunc(blockchains, func(a, b core.Blockchain) int { @@ -120,11 +127,12 @@ func NewMemoryStoreV1(assetsConfig AssetsConfig, blockchainsConfig map[uint64]Bl }) return &MemoryStoreV1{ - blockchains: blockchains, - assets: assets, - supportedAssets: supportedAssets, - tokenDecimals: tokenDecimals, - assetDecimals: assetDecimals, + blockchains: blockchains, + assets: assets, + channelSigValidators: channelSigValidators, + supportedAssets: supportedAssets, + tokenDecimals: tokenDecimals, + assetDecimals: assetDecimals, }, nil } @@ -171,6 +179,14 @@ func (ms *MemoryStoreV1) GetAssets(blockchainID *uint64) ([]core.Asset, error) { return filteredAssets, nil } +func (ms *MemoryStoreV1) GetChannelSigValidators(blockchainID uint64) (map[uint8]string, error) { + channelSigValidators, ok := ms.channelSigValidators[blockchainID] + if !ok { + return nil, fmt.Errorf("blockchain with ID '%d' is not supported", blockchainID) + } + return channelSigValidators, nil +} + func (ms *MemoryStoreV1) GetTokenAddress(asset string, blockchainID uint64) (string, error) { tokensOnchain, ok := ms.supportedAssets[asset] if !ok { diff --git a/pkg/blockchain/evm/app_registry_abi.go b/pkg/blockchain/evm/app_registry_abi.go new file mode 100644 index 000000000..686285cf7 --- /dev/null +++ b/pkg/blockchain/evm/app_registry_abi.go @@ -0,0 +1,2154 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package evm + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription +) + +// AppRegistryMetaData contains all meta data concerning the AppRegistry contract. +var AppRegistryMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"asset_\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"unlockPeriod_\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"admin_\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"ADJUDICATOR_ROLE\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"ASSET\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"DEFAULT_ADMIN_ROLE\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"UNLOCK_PERIOD\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"asset\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"balanceOf\",\"inputs\":[{\"name\":\"user\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getRoleAdmin\",\"inputs\":[{\"name\":\"role\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"grantRole\",\"inputs\":[{\"name\":\"role\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"hasRole\",\"inputs\":[{\"name\":\"role\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"lastSlashTimestamp\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"lock\",\"inputs\":[{\"name\":\"target\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"lockStateOf\",\"inputs\":[{\"name\":\"user\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint8\",\"internalType\":\"enumILock.LockState\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"relock\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"renounceRole\",\"inputs\":[{\"name\":\"role\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"callerConfirmation\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"revokeRole\",\"inputs\":[{\"name\":\"role\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setSlashCooldown\",\"inputs\":[{\"name\":\"newCooldown\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"slash\",\"inputs\":[{\"name\":\"user\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"recipient\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"decision\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"slashCooldown\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"supportsInterface\",\"inputs\":[{\"name\":\"interfaceId\",\"type\":\"bytes4\",\"internalType\":\"bytes4\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"unlock\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"unlockTimestampOf\",\"inputs\":[{\"name\":\"user\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"withdraw\",\"inputs\":[{\"name\":\"destination\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"Locked\",\"inputs\":[{\"name\":\"user\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"deposited\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"newBalance\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Relocked\",\"inputs\":[{\"name\":\"user\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"balance\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"RoleAdminChanged\",\"inputs\":[{\"name\":\"role\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"previousAdminRole\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"newAdminRole\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"RoleGranted\",\"inputs\":[{\"name\":\"role\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"account\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"sender\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"RoleRevoked\",\"inputs\":[{\"name\":\"role\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"account\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"sender\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"SlashCooldownUpdated\",\"inputs\":[{\"name\":\"oldCooldown\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"newCooldown\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Slashed\",\"inputs\":[{\"name\":\"user\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"recipient\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"decision\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"UnlockInitiated\",\"inputs\":[{\"name\":\"user\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"balance\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"availableAt\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Withdrawn\",\"inputs\":[{\"name\":\"user\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"balance\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"AccessControlBadConfirmation\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"AccessControlUnauthorizedAccount\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"neededRole\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}]},{\"type\":\"error\",\"name\":\"AlreadyUnlocking\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"InsufficientBalance\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"InvalidAddress\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"InvalidAmount\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"InvalidPeriod\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"NotLocked\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"NotUnlocking\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"RecipientIsAdjudicator\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ReentrancyGuardReentrantCall\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"SafeERC20FailedOperation\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"SlashCooldownActive\",\"inputs\":[{\"name\":\"availableAt\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"UnlockPeriodNotElapsed\",\"inputs\":[{\"name\":\"availableAt\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]}]", + Bin: "0x60c03461010d57601f61176038819003918201601f19168301916001600160401b038311848410176101115780849260609460405283398101031261010d5761004781610125565b90610059604060208301519201610125565b60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055916001600160a01b038116156100ef5781156100fe5760805260a0526001600160a01b038116156100ef576100b190610139565b5060405161157d90816101c3823960805181818161060d0152818161084c01528181610edd01526110e1015260a05181818161030b0152610aca0152f35b63e6c4247b60e01b5f5260045ffd5b6302e8f35960e31b5f5260045ffd5b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b51906001600160a01b038216820361010d57565b6001600160a01b0381165f9081525f5160206117405f395f51905f52602052604090205460ff166101bd576001600160a01b03165f8181525f5160206117405f395f51905f5260205260408120805460ff191660011790553391907f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d8180a4600190565b505f9056fe60806040526004361015610011575f80fd5b5f3560e01c8063010379b214610d3257806301ffc9a714610c7357806302f70fe614610c10578063248a9ca314610bbf578063256f8f0914610aed578063259a28cf14610a95578063282d3fdf146107ad5780632f2ff15d1461075157806336568abe146106c957806338d52e0f146106c45780634800d97f146106c457806351cff8d91461058a5780635a6ca4ae1461054f57806370a08231146104ed57806391d1485414610478578063a217fddf14610440578063a43e9cf4146103c1578063a69df4b5146102b5578063c53b573d14610205578063c77f5062146101ca578063d547741f146101675763f538f7dd1461010b575f80fd5b34610163575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101635760206040517f5ad6c1ce52091d5f5a49dff6df7d4bf577735e77d8c13251e1ea9c82ce8c380d8152f35b5f80fd5b346101635760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610163576101c86004356101a4611074565b906101c36101be825f526002602052600160405f20015490565b611201565b61147d565b005b34610163575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610163576020600454604051908152f35b34610163575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261016357335f52600160205260405f20541561028d57335f525f60205260405f2054335f5260016020525f60408120556040519081527fe2d155d9d74decc198a9a9b4f5bddb24ee0842ee745c9ce57e3573b971e50d9d60203392a2005b7ff61050dc000000000000000000000000000000000000000000000000000000005f5260045ffd5b34610163575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261016357335f525f60205260405f2054801561039957335f52600160205260405f2054610371576103307f000000000000000000000000000000000000000000000000000000000000000042611105565b335f5260016020528060405f205560405191825260208201527fd73fc4aafa5101b91e88b8c2fa75d8d6db73466773addb11c079d063c1e63c0c60403392a2005b7fe47b668b000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f1834e265000000000000000000000000000000000000000000000000000000005f5260045ffd5b346101635760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610163576104006103fb611051565b6111ba565b6040516003821015610413576020918152f35b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b34610163575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101635760206040515f8152f35b346101635760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610163576104af611074565b6004355f52600260205273ffffffffffffffffffffffffffffffffffffffff60405f2091165f52602052602060ff60405f2054166040519015158152f35b346101635760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101635773ffffffffffffffffffffffffffffffffffffffff610539611051565b165f525f602052602060405f2054604051908152f35b34610163575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610163576020600354604051908152f35b346101635760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610163576105c1611051565b6105c9611268565b335f52600160205260405f2054801561028d578042106106995750335f908152602081815260408083208054908490556001909252822091909155906106479082907f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff166112df565b6040519081527f7084f5476618d8e60b11ef0d7d3f06914655adb8793e28ff7f018d4c76d505d560203392a260017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055005b7fc33c45d1000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b611097565b346101635760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261016357610700611074565b3373ffffffffffffffffffffffffffffffffffffffff821603610729576101c89060043561147d565b7f6697b232000000000000000000000000000000000000000000000000000000005f5260045ffd5b346101635760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610163576101c860043561078e611074565b906107a86101be825f526002602052600160405f20015490565b6113a9565b346101635760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610163576107e4611051565b602435906107f0611268565b8115610a6d5773ffffffffffffffffffffffffffffffffffffffff1690815f52600160205260405f2054610371576040517f70a082310000000000000000000000000000000000000000000000000000000081523060048201527f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff16602082602481845afa9182156109e4575f92610a39575b50604051927f23b872dd000000000000000000000000000000000000000000000000000000005f52336004523060245260445260205f60648180855af160015f5114811615610a1a575b836040525f606052156109ef57826024816020937f70a082310000000000000000000000000000000000000000000000000000000082523060048301525afa9182156109e4575f926109ae575b5061095d6040917fd4665e3049283582ba6f9eba07a5b3e12dab49e02da99e8927a47af5d134bea59361113f565b835f525f60205261097181835f2054611105565b845f525f60205280835f205582519182526020820152a260017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055005b91506020823d6020116109dc575b816109c96020938361114c565b810103126101635790519061095d61092f565b3d91506109bc565b6040513d5f823e3d90fd5b7f5274afe7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b6001811516610a3057813b15153d1516166108e2565b833d5f823e3d90fd5b9091506020813d602011610a65575b81610a556020938361114c565b8101031261016357519084610898565b3d9150610a48565b7f2c5211c6000000000000000000000000000000000000000000000000000000005f5260045ffd5b34610163575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101635760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b346101635760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261016357335f9081527fac33ff75c19e70fe83507db0d683fd3465c996598dc972688b7ace676c89077b60205260409020546004359060ff1615610b8f5760407f21dd401bad58f48f88b208aad5157305ac7e8ec5db030042aaec08ebd7f50e4c91600354908060035582519182526020820152a1005b7fe2517d3f000000000000000000000000000000000000000000000000000000005f52336004525f60245260445ffd5b346101635760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610163576020610c086004355f526002602052600160405f20015490565b604051908152f35b346101635760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101635773ffffffffffffffffffffffffffffffffffffffff610c5c611051565b165f526001602052602060405f2054604051908152f35b346101635760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610163576004357fffffffff00000000000000000000000000000000000000000000000000000000811680910361016357807f7965db0b0000000000000000000000000000000000000000000000000000000060209214908115610d08575b506040519015158152f35b7f01ffc9a70000000000000000000000000000000000000000000000000000000091501482610cfd565b346101635760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261016357610d69611051565b60243560443573ffffffffffffffffffffffffffffffffffffffff811692838203610163576064359067ffffffffffffffff821161016357366023830112156101635781600401359067ffffffffffffffff821161016357366024838501011161016357335f9081527f7f9272e0d81bdb9b1e0d5b8b11f4dfdbe7d5ebf3410318d28f3e6024ce33d903602052604090205460ff161561100157610e0b611268565b60045460035480151580610ff8575b610fb7575b5050338614610f8f5773ffffffffffffffffffffffffffffffffffffffff1693845f525f60205260405f2054938415610f6757848211610f6757601f606093610f02847fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe094610eaf827ff204f6a8b6d1439051d5fbd742389d0f778d18d21016e81c8ad3d4558266454c9b61113f565b8b5f525f6020528060405f205515610f54575b4260045573ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000166112df565b80602460405197889687526040602088015282604088015201868601375f85828601015201168101030190a360017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055005b8a5f5260016020525f6040812055610ec2565b7ff4d678b8000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f8d3fe42e000000000000000000000000000000000000000000000000000000005f5260045ffd5b610fc091611105565b804210610fcd5780610e1f565b7f773faedc000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b50811515610e1a565b7fe2517d3f000000000000000000000000000000000000000000000000000000005f52336004527f5ad6c1ce52091d5f5a49dff6df7d4bf577735e77d8c13251e1ea9c82ce8c380d60245260445ffd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361016357565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361016357565b34610163575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261016357602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b9190820180921161111257565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b9190820391821161111257565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761118d57604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b73ffffffffffffffffffffffffffffffffffffffff16805f525f60205260405f2054156111fc575f52600160205260405f2054156111f757600290565b600190565b505f90565b805f52600260205260405f2073ffffffffffffffffffffffffffffffffffffffff33165f5260205260ff60405f205416156112395750565b7fe2517d3f000000000000000000000000000000000000000000000000000000005f523360045260245260445ffd5b60027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0054146112b75760027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b7f3ee5aeb5000000000000000000000000000000000000000000000000000000005f5260045ffd5b9173ffffffffffffffffffffffffffffffffffffffff604051927fa9059cbb000000000000000000000000000000000000000000000000000000005f521660045260245260205f60448180865af19060015f5114821615611388575b604052156113465750565b73ffffffffffffffffffffffffffffffffffffffff907f5274afe7000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b9060018115166113a057823b15153d1516169061133b565b503d5f823e3d90fd5b805f52600260205260405f2073ffffffffffffffffffffffffffffffffffffffff83165f5260205260ff60405f205416155f1461147757805f52600260205260405f2073ffffffffffffffffffffffffffffffffffffffff83165f5260205260405f2060017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0082541617905573ffffffffffffffffffffffffffffffffffffffff339216907f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d5f80a4600190565b50505f90565b805f52600260205260405f2073ffffffffffffffffffffffffffffffffffffffff83165f5260205260ff60405f2054165f1461147757805f52600260205260405f2073ffffffffffffffffffffffffffffffffffffffff83165f5260205260405f207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00815416905573ffffffffffffffffffffffffffffffffffffffff339216907ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b5f80a460019056fea2646970667358221220d76073a01636f84db6fa5e41c0f05a2695c4df18ed5b02a8e189ed7ed6d4da3a64736f6c63430008220033ac33ff75c19e70fe83507db0d683fd3465c996598dc972688b7ace676c89077b", +} + +// AppRegistryABI is the input ABI used to generate the binding from. +// Deprecated: Use AppRegistryMetaData.ABI instead. +var AppRegistryABI = AppRegistryMetaData.ABI + +// AppRegistryBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use AppRegistryMetaData.Bin instead. +var AppRegistryBin = AppRegistryMetaData.Bin + +// DeployAppRegistry deploys a new Ethereum contract, binding an instance of AppRegistry to it. +func DeployAppRegistry(auth *bind.TransactOpts, backend bind.ContractBackend, asset_ common.Address, unlockPeriod_ *big.Int, admin_ common.Address) (common.Address, *types.Transaction, *AppRegistry, error) { + parsed, err := AppRegistryMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(AppRegistryBin), backend, asset_, unlockPeriod_, admin_) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &AppRegistry{AppRegistryCaller: AppRegistryCaller{contract: contract}, AppRegistryTransactor: AppRegistryTransactor{contract: contract}, AppRegistryFilterer: AppRegistryFilterer{contract: contract}}, nil +} + +// AppRegistry is an auto generated Go binding around an Ethereum contract. +type AppRegistry struct { + AppRegistryCaller // Read-only binding to the contract + AppRegistryTransactor // Write-only binding to the contract + AppRegistryFilterer // Log filterer for contract events +} + +// AppRegistryCaller is an auto generated read-only Go binding around an Ethereum contract. +type AppRegistryCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// AppRegistryTransactor is an auto generated write-only Go binding around an Ethereum contract. +type AppRegistryTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// AppRegistryFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type AppRegistryFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// AppRegistrySession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type AppRegistrySession struct { + Contract *AppRegistry // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// AppRegistryCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type AppRegistryCallerSession struct { + Contract *AppRegistryCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// AppRegistryTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type AppRegistryTransactorSession struct { + Contract *AppRegistryTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// AppRegistryRaw is an auto generated low-level Go binding around an Ethereum contract. +type AppRegistryRaw struct { + Contract *AppRegistry // Generic contract binding to access the raw methods on +} + +// AppRegistryCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type AppRegistryCallerRaw struct { + Contract *AppRegistryCaller // Generic read-only contract binding to access the raw methods on +} + +// AppRegistryTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type AppRegistryTransactorRaw struct { + Contract *AppRegistryTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewAppRegistry creates a new instance of AppRegistry, bound to a specific deployed contract. +func NewAppRegistry(address common.Address, backend bind.ContractBackend) (*AppRegistry, error) { + contract, err := bindAppRegistry(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &AppRegistry{AppRegistryCaller: AppRegistryCaller{contract: contract}, AppRegistryTransactor: AppRegistryTransactor{contract: contract}, AppRegistryFilterer: AppRegistryFilterer{contract: contract}}, nil +} + +// NewAppRegistryCaller creates a new read-only instance of AppRegistry, bound to a specific deployed contract. +func NewAppRegistryCaller(address common.Address, caller bind.ContractCaller) (*AppRegistryCaller, error) { + contract, err := bindAppRegistry(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &AppRegistryCaller{contract: contract}, nil +} + +// NewAppRegistryTransactor creates a new write-only instance of AppRegistry, bound to a specific deployed contract. +func NewAppRegistryTransactor(address common.Address, transactor bind.ContractTransactor) (*AppRegistryTransactor, error) { + contract, err := bindAppRegistry(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &AppRegistryTransactor{contract: contract}, nil +} + +// NewAppRegistryFilterer creates a new log filterer instance of AppRegistry, bound to a specific deployed contract. +func NewAppRegistryFilterer(address common.Address, filterer bind.ContractFilterer) (*AppRegistryFilterer, error) { + contract, err := bindAppRegistry(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &AppRegistryFilterer{contract: contract}, nil +} + +// bindAppRegistry binds a generic wrapper to an already deployed contract. +func bindAppRegistry(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := abi.JSON(strings.NewReader(AppRegistryABI)) + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_AppRegistry *AppRegistryRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _AppRegistry.Contract.AppRegistryCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_AppRegistry *AppRegistryRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _AppRegistry.Contract.AppRegistryTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_AppRegistry *AppRegistryRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _AppRegistry.Contract.AppRegistryTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_AppRegistry *AppRegistryCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _AppRegistry.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_AppRegistry *AppRegistryTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _AppRegistry.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_AppRegistry *AppRegistryTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _AppRegistry.Contract.contract.Transact(opts, method, params...) +} + +// ADJUDICATORROLE is a free data retrieval call binding the contract method 0xf538f7dd. +// +// Solidity: function ADJUDICATOR_ROLE() view returns(bytes32) +func (_AppRegistry *AppRegistryCaller) ADJUDICATORROLE(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _AppRegistry.contract.Call(opts, &out, "ADJUDICATOR_ROLE") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// ADJUDICATORROLE is a free data retrieval call binding the contract method 0xf538f7dd. +// +// Solidity: function ADJUDICATOR_ROLE() view returns(bytes32) +func (_AppRegistry *AppRegistrySession) ADJUDICATORROLE() ([32]byte, error) { + return _AppRegistry.Contract.ADJUDICATORROLE(&_AppRegistry.CallOpts) +} + +// ADJUDICATORROLE is a free data retrieval call binding the contract method 0xf538f7dd. +// +// Solidity: function ADJUDICATOR_ROLE() view returns(bytes32) +func (_AppRegistry *AppRegistryCallerSession) ADJUDICATORROLE() ([32]byte, error) { + return _AppRegistry.Contract.ADJUDICATORROLE(&_AppRegistry.CallOpts) +} + +// ASSET is a free data retrieval call binding the contract method 0x4800d97f. +// +// Solidity: function ASSET() view returns(address) +func (_AppRegistry *AppRegistryCaller) ASSET(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _AppRegistry.contract.Call(opts, &out, "ASSET") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// ASSET is a free data retrieval call binding the contract method 0x4800d97f. +// +// Solidity: function ASSET() view returns(address) +func (_AppRegistry *AppRegistrySession) ASSET() (common.Address, error) { + return _AppRegistry.Contract.ASSET(&_AppRegistry.CallOpts) +} + +// ASSET is a free data retrieval call binding the contract method 0x4800d97f. +// +// Solidity: function ASSET() view returns(address) +func (_AppRegistry *AppRegistryCallerSession) ASSET() (common.Address, error) { + return _AppRegistry.Contract.ASSET(&_AppRegistry.CallOpts) +} + +// DEFAULTADMINROLE is a free data retrieval call binding the contract method 0xa217fddf. +// +// Solidity: function DEFAULT_ADMIN_ROLE() view returns(bytes32) +func (_AppRegistry *AppRegistryCaller) DEFAULTADMINROLE(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _AppRegistry.contract.Call(opts, &out, "DEFAULT_ADMIN_ROLE") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// DEFAULTADMINROLE is a free data retrieval call binding the contract method 0xa217fddf. +// +// Solidity: function DEFAULT_ADMIN_ROLE() view returns(bytes32) +func (_AppRegistry *AppRegistrySession) DEFAULTADMINROLE() ([32]byte, error) { + return _AppRegistry.Contract.DEFAULTADMINROLE(&_AppRegistry.CallOpts) +} + +// DEFAULTADMINROLE is a free data retrieval call binding the contract method 0xa217fddf. +// +// Solidity: function DEFAULT_ADMIN_ROLE() view returns(bytes32) +func (_AppRegistry *AppRegistryCallerSession) DEFAULTADMINROLE() ([32]byte, error) { + return _AppRegistry.Contract.DEFAULTADMINROLE(&_AppRegistry.CallOpts) +} + +// UNLOCKPERIOD is a free data retrieval call binding the contract method 0x259a28cf. +// +// Solidity: function UNLOCK_PERIOD() view returns(uint256) +func (_AppRegistry *AppRegistryCaller) UNLOCKPERIOD(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _AppRegistry.contract.Call(opts, &out, "UNLOCK_PERIOD") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// UNLOCKPERIOD is a free data retrieval call binding the contract method 0x259a28cf. +// +// Solidity: function UNLOCK_PERIOD() view returns(uint256) +func (_AppRegistry *AppRegistrySession) UNLOCKPERIOD() (*big.Int, error) { + return _AppRegistry.Contract.UNLOCKPERIOD(&_AppRegistry.CallOpts) +} + +// UNLOCKPERIOD is a free data retrieval call binding the contract method 0x259a28cf. +// +// Solidity: function UNLOCK_PERIOD() view returns(uint256) +func (_AppRegistry *AppRegistryCallerSession) UNLOCKPERIOD() (*big.Int, error) { + return _AppRegistry.Contract.UNLOCKPERIOD(&_AppRegistry.CallOpts) +} + +// Asset is a free data retrieval call binding the contract method 0x38d52e0f. +// +// Solidity: function asset() view returns(address) +func (_AppRegistry *AppRegistryCaller) Asset(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _AppRegistry.contract.Call(opts, &out, "asset") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Asset is a free data retrieval call binding the contract method 0x38d52e0f. +// +// Solidity: function asset() view returns(address) +func (_AppRegistry *AppRegistrySession) Asset() (common.Address, error) { + return _AppRegistry.Contract.Asset(&_AppRegistry.CallOpts) +} + +// Asset is a free data retrieval call binding the contract method 0x38d52e0f. +// +// Solidity: function asset() view returns(address) +func (_AppRegistry *AppRegistryCallerSession) Asset() (common.Address, error) { + return _AppRegistry.Contract.Asset(&_AppRegistry.CallOpts) +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(address user) view returns(uint256) +func (_AppRegistry *AppRegistryCaller) BalanceOf(opts *bind.CallOpts, user common.Address) (*big.Int, error) { + var out []interface{} + err := _AppRegistry.contract.Call(opts, &out, "balanceOf", user) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(address user) view returns(uint256) +func (_AppRegistry *AppRegistrySession) BalanceOf(user common.Address) (*big.Int, error) { + return _AppRegistry.Contract.BalanceOf(&_AppRegistry.CallOpts, user) +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(address user) view returns(uint256) +func (_AppRegistry *AppRegistryCallerSession) BalanceOf(user common.Address) (*big.Int, error) { + return _AppRegistry.Contract.BalanceOf(&_AppRegistry.CallOpts, user) +} + +// GetRoleAdmin is a free data retrieval call binding the contract method 0x248a9ca3. +// +// Solidity: function getRoleAdmin(bytes32 role) view returns(bytes32) +func (_AppRegistry *AppRegistryCaller) GetRoleAdmin(opts *bind.CallOpts, role [32]byte) ([32]byte, error) { + var out []interface{} + err := _AppRegistry.contract.Call(opts, &out, "getRoleAdmin", role) + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// GetRoleAdmin is a free data retrieval call binding the contract method 0x248a9ca3. +// +// Solidity: function getRoleAdmin(bytes32 role) view returns(bytes32) +func (_AppRegistry *AppRegistrySession) GetRoleAdmin(role [32]byte) ([32]byte, error) { + return _AppRegistry.Contract.GetRoleAdmin(&_AppRegistry.CallOpts, role) +} + +// GetRoleAdmin is a free data retrieval call binding the contract method 0x248a9ca3. +// +// Solidity: function getRoleAdmin(bytes32 role) view returns(bytes32) +func (_AppRegistry *AppRegistryCallerSession) GetRoleAdmin(role [32]byte) ([32]byte, error) { + return _AppRegistry.Contract.GetRoleAdmin(&_AppRegistry.CallOpts, role) +} + +// HasRole is a free data retrieval call binding the contract method 0x91d14854. +// +// Solidity: function hasRole(bytes32 role, address account) view returns(bool) +func (_AppRegistry *AppRegistryCaller) HasRole(opts *bind.CallOpts, role [32]byte, account common.Address) (bool, error) { + var out []interface{} + err := _AppRegistry.contract.Call(opts, &out, "hasRole", role, account) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// HasRole is a free data retrieval call binding the contract method 0x91d14854. +// +// Solidity: function hasRole(bytes32 role, address account) view returns(bool) +func (_AppRegistry *AppRegistrySession) HasRole(role [32]byte, account common.Address) (bool, error) { + return _AppRegistry.Contract.HasRole(&_AppRegistry.CallOpts, role, account) +} + +// HasRole is a free data retrieval call binding the contract method 0x91d14854. +// +// Solidity: function hasRole(bytes32 role, address account) view returns(bool) +func (_AppRegistry *AppRegistryCallerSession) HasRole(role [32]byte, account common.Address) (bool, error) { + return _AppRegistry.Contract.HasRole(&_AppRegistry.CallOpts, role, account) +} + +// LastSlashTimestamp is a free data retrieval call binding the contract method 0xc77f5062. +// +// Solidity: function lastSlashTimestamp() view returns(uint256) +func (_AppRegistry *AppRegistryCaller) LastSlashTimestamp(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _AppRegistry.contract.Call(opts, &out, "lastSlashTimestamp") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// LastSlashTimestamp is a free data retrieval call binding the contract method 0xc77f5062. +// +// Solidity: function lastSlashTimestamp() view returns(uint256) +func (_AppRegistry *AppRegistrySession) LastSlashTimestamp() (*big.Int, error) { + return _AppRegistry.Contract.LastSlashTimestamp(&_AppRegistry.CallOpts) +} + +// LastSlashTimestamp is a free data retrieval call binding the contract method 0xc77f5062. +// +// Solidity: function lastSlashTimestamp() view returns(uint256) +func (_AppRegistry *AppRegistryCallerSession) LastSlashTimestamp() (*big.Int, error) { + return _AppRegistry.Contract.LastSlashTimestamp(&_AppRegistry.CallOpts) +} + +// LockStateOf is a free data retrieval call binding the contract method 0xa43e9cf4. +// +// Solidity: function lockStateOf(address user) view returns(uint8) +func (_AppRegistry *AppRegistryCaller) LockStateOf(opts *bind.CallOpts, user common.Address) (uint8, error) { + var out []interface{} + err := _AppRegistry.contract.Call(opts, &out, "lockStateOf", user) + + if err != nil { + return *new(uint8), err + } + + out0 := *abi.ConvertType(out[0], new(uint8)).(*uint8) + + return out0, err + +} + +// LockStateOf is a free data retrieval call binding the contract method 0xa43e9cf4. +// +// Solidity: function lockStateOf(address user) view returns(uint8) +func (_AppRegistry *AppRegistrySession) LockStateOf(user common.Address) (uint8, error) { + return _AppRegistry.Contract.LockStateOf(&_AppRegistry.CallOpts, user) +} + +// LockStateOf is a free data retrieval call binding the contract method 0xa43e9cf4. +// +// Solidity: function lockStateOf(address user) view returns(uint8) +func (_AppRegistry *AppRegistryCallerSession) LockStateOf(user common.Address) (uint8, error) { + return _AppRegistry.Contract.LockStateOf(&_AppRegistry.CallOpts, user) +} + +// SlashCooldown is a free data retrieval call binding the contract method 0x5a6ca4ae. +// +// Solidity: function slashCooldown() view returns(uint256) +func (_AppRegistry *AppRegistryCaller) SlashCooldown(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _AppRegistry.contract.Call(opts, &out, "slashCooldown") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// SlashCooldown is a free data retrieval call binding the contract method 0x5a6ca4ae. +// +// Solidity: function slashCooldown() view returns(uint256) +func (_AppRegistry *AppRegistrySession) SlashCooldown() (*big.Int, error) { + return _AppRegistry.Contract.SlashCooldown(&_AppRegistry.CallOpts) +} + +// SlashCooldown is a free data retrieval call binding the contract method 0x5a6ca4ae. +// +// Solidity: function slashCooldown() view returns(uint256) +func (_AppRegistry *AppRegistryCallerSession) SlashCooldown() (*big.Int, error) { + return _AppRegistry.Contract.SlashCooldown(&_AppRegistry.CallOpts) +} + +// SupportsInterface is a free data retrieval call binding the contract method 0x01ffc9a7. +// +// Solidity: function supportsInterface(bytes4 interfaceId) view returns(bool) +func (_AppRegistry *AppRegistryCaller) SupportsInterface(opts *bind.CallOpts, interfaceId [4]byte) (bool, error) { + var out []interface{} + err := _AppRegistry.contract.Call(opts, &out, "supportsInterface", interfaceId) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// SupportsInterface is a free data retrieval call binding the contract method 0x01ffc9a7. +// +// Solidity: function supportsInterface(bytes4 interfaceId) view returns(bool) +func (_AppRegistry *AppRegistrySession) SupportsInterface(interfaceId [4]byte) (bool, error) { + return _AppRegistry.Contract.SupportsInterface(&_AppRegistry.CallOpts, interfaceId) +} + +// SupportsInterface is a free data retrieval call binding the contract method 0x01ffc9a7. +// +// Solidity: function supportsInterface(bytes4 interfaceId) view returns(bool) +func (_AppRegistry *AppRegistryCallerSession) SupportsInterface(interfaceId [4]byte) (bool, error) { + return _AppRegistry.Contract.SupportsInterface(&_AppRegistry.CallOpts, interfaceId) +} + +// UnlockTimestampOf is a free data retrieval call binding the contract method 0x02f70fe6. +// +// Solidity: function unlockTimestampOf(address user) view returns(uint256) +func (_AppRegistry *AppRegistryCaller) UnlockTimestampOf(opts *bind.CallOpts, user common.Address) (*big.Int, error) { + var out []interface{} + err := _AppRegistry.contract.Call(opts, &out, "unlockTimestampOf", user) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// UnlockTimestampOf is a free data retrieval call binding the contract method 0x02f70fe6. +// +// Solidity: function unlockTimestampOf(address user) view returns(uint256) +func (_AppRegistry *AppRegistrySession) UnlockTimestampOf(user common.Address) (*big.Int, error) { + return _AppRegistry.Contract.UnlockTimestampOf(&_AppRegistry.CallOpts, user) +} + +// UnlockTimestampOf is a free data retrieval call binding the contract method 0x02f70fe6. +// +// Solidity: function unlockTimestampOf(address user) view returns(uint256) +func (_AppRegistry *AppRegistryCallerSession) UnlockTimestampOf(user common.Address) (*big.Int, error) { + return _AppRegistry.Contract.UnlockTimestampOf(&_AppRegistry.CallOpts, user) +} + +// GrantRole is a paid mutator transaction binding the contract method 0x2f2ff15d. +// +// Solidity: function grantRole(bytes32 role, address account) returns() +func (_AppRegistry *AppRegistryTransactor) GrantRole(opts *bind.TransactOpts, role [32]byte, account common.Address) (*types.Transaction, error) { + return _AppRegistry.contract.Transact(opts, "grantRole", role, account) +} + +// GrantRole is a paid mutator transaction binding the contract method 0x2f2ff15d. +// +// Solidity: function grantRole(bytes32 role, address account) returns() +func (_AppRegistry *AppRegistrySession) GrantRole(role [32]byte, account common.Address) (*types.Transaction, error) { + return _AppRegistry.Contract.GrantRole(&_AppRegistry.TransactOpts, role, account) +} + +// GrantRole is a paid mutator transaction binding the contract method 0x2f2ff15d. +// +// Solidity: function grantRole(bytes32 role, address account) returns() +func (_AppRegistry *AppRegistryTransactorSession) GrantRole(role [32]byte, account common.Address) (*types.Transaction, error) { + return _AppRegistry.Contract.GrantRole(&_AppRegistry.TransactOpts, role, account) +} + +// Lock is a paid mutator transaction binding the contract method 0x282d3fdf. +// +// Solidity: function lock(address target, uint256 amount) returns() +func (_AppRegistry *AppRegistryTransactor) Lock(opts *bind.TransactOpts, target common.Address, amount *big.Int) (*types.Transaction, error) { + return _AppRegistry.contract.Transact(opts, "lock", target, amount) +} + +// Lock is a paid mutator transaction binding the contract method 0x282d3fdf. +// +// Solidity: function lock(address target, uint256 amount) returns() +func (_AppRegistry *AppRegistrySession) Lock(target common.Address, amount *big.Int) (*types.Transaction, error) { + return _AppRegistry.Contract.Lock(&_AppRegistry.TransactOpts, target, amount) +} + +// Lock is a paid mutator transaction binding the contract method 0x282d3fdf. +// +// Solidity: function lock(address target, uint256 amount) returns() +func (_AppRegistry *AppRegistryTransactorSession) Lock(target common.Address, amount *big.Int) (*types.Transaction, error) { + return _AppRegistry.Contract.Lock(&_AppRegistry.TransactOpts, target, amount) +} + +// Relock is a paid mutator transaction binding the contract method 0xc53b573d. +// +// Solidity: function relock() returns() +func (_AppRegistry *AppRegistryTransactor) Relock(opts *bind.TransactOpts) (*types.Transaction, error) { + return _AppRegistry.contract.Transact(opts, "relock") +} + +// Relock is a paid mutator transaction binding the contract method 0xc53b573d. +// +// Solidity: function relock() returns() +func (_AppRegistry *AppRegistrySession) Relock() (*types.Transaction, error) { + return _AppRegistry.Contract.Relock(&_AppRegistry.TransactOpts) +} + +// Relock is a paid mutator transaction binding the contract method 0xc53b573d. +// +// Solidity: function relock() returns() +func (_AppRegistry *AppRegistryTransactorSession) Relock() (*types.Transaction, error) { + return _AppRegistry.Contract.Relock(&_AppRegistry.TransactOpts) +} + +// RenounceRole is a paid mutator transaction binding the contract method 0x36568abe. +// +// Solidity: function renounceRole(bytes32 role, address callerConfirmation) returns() +func (_AppRegistry *AppRegistryTransactor) RenounceRole(opts *bind.TransactOpts, role [32]byte, callerConfirmation common.Address) (*types.Transaction, error) { + return _AppRegistry.contract.Transact(opts, "renounceRole", role, callerConfirmation) +} + +// RenounceRole is a paid mutator transaction binding the contract method 0x36568abe. +// +// Solidity: function renounceRole(bytes32 role, address callerConfirmation) returns() +func (_AppRegistry *AppRegistrySession) RenounceRole(role [32]byte, callerConfirmation common.Address) (*types.Transaction, error) { + return _AppRegistry.Contract.RenounceRole(&_AppRegistry.TransactOpts, role, callerConfirmation) +} + +// RenounceRole is a paid mutator transaction binding the contract method 0x36568abe. +// +// Solidity: function renounceRole(bytes32 role, address callerConfirmation) returns() +func (_AppRegistry *AppRegistryTransactorSession) RenounceRole(role [32]byte, callerConfirmation common.Address) (*types.Transaction, error) { + return _AppRegistry.Contract.RenounceRole(&_AppRegistry.TransactOpts, role, callerConfirmation) +} + +// RevokeRole is a paid mutator transaction binding the contract method 0xd547741f. +// +// Solidity: function revokeRole(bytes32 role, address account) returns() +func (_AppRegistry *AppRegistryTransactor) RevokeRole(opts *bind.TransactOpts, role [32]byte, account common.Address) (*types.Transaction, error) { + return _AppRegistry.contract.Transact(opts, "revokeRole", role, account) +} + +// RevokeRole is a paid mutator transaction binding the contract method 0xd547741f. +// +// Solidity: function revokeRole(bytes32 role, address account) returns() +func (_AppRegistry *AppRegistrySession) RevokeRole(role [32]byte, account common.Address) (*types.Transaction, error) { + return _AppRegistry.Contract.RevokeRole(&_AppRegistry.TransactOpts, role, account) +} + +// RevokeRole is a paid mutator transaction binding the contract method 0xd547741f. +// +// Solidity: function revokeRole(bytes32 role, address account) returns() +func (_AppRegistry *AppRegistryTransactorSession) RevokeRole(role [32]byte, account common.Address) (*types.Transaction, error) { + return _AppRegistry.Contract.RevokeRole(&_AppRegistry.TransactOpts, role, account) +} + +// SetSlashCooldown is a paid mutator transaction binding the contract method 0x256f8f09. +// +// Solidity: function setSlashCooldown(uint256 newCooldown) returns() +func (_AppRegistry *AppRegistryTransactor) SetSlashCooldown(opts *bind.TransactOpts, newCooldown *big.Int) (*types.Transaction, error) { + return _AppRegistry.contract.Transact(opts, "setSlashCooldown", newCooldown) +} + +// SetSlashCooldown is a paid mutator transaction binding the contract method 0x256f8f09. +// +// Solidity: function setSlashCooldown(uint256 newCooldown) returns() +func (_AppRegistry *AppRegistrySession) SetSlashCooldown(newCooldown *big.Int) (*types.Transaction, error) { + return _AppRegistry.Contract.SetSlashCooldown(&_AppRegistry.TransactOpts, newCooldown) +} + +// SetSlashCooldown is a paid mutator transaction binding the contract method 0x256f8f09. +// +// Solidity: function setSlashCooldown(uint256 newCooldown) returns() +func (_AppRegistry *AppRegistryTransactorSession) SetSlashCooldown(newCooldown *big.Int) (*types.Transaction, error) { + return _AppRegistry.Contract.SetSlashCooldown(&_AppRegistry.TransactOpts, newCooldown) +} + +// Slash is a paid mutator transaction binding the contract method 0x010379b2. +// +// Solidity: function slash(address user, uint256 amount, address recipient, bytes decision) returns() +func (_AppRegistry *AppRegistryTransactor) Slash(opts *bind.TransactOpts, user common.Address, amount *big.Int, recipient common.Address, decision []byte) (*types.Transaction, error) { + return _AppRegistry.contract.Transact(opts, "slash", user, amount, recipient, decision) +} + +// Slash is a paid mutator transaction binding the contract method 0x010379b2. +// +// Solidity: function slash(address user, uint256 amount, address recipient, bytes decision) returns() +func (_AppRegistry *AppRegistrySession) Slash(user common.Address, amount *big.Int, recipient common.Address, decision []byte) (*types.Transaction, error) { + return _AppRegistry.Contract.Slash(&_AppRegistry.TransactOpts, user, amount, recipient, decision) +} + +// Slash is a paid mutator transaction binding the contract method 0x010379b2. +// +// Solidity: function slash(address user, uint256 amount, address recipient, bytes decision) returns() +func (_AppRegistry *AppRegistryTransactorSession) Slash(user common.Address, amount *big.Int, recipient common.Address, decision []byte) (*types.Transaction, error) { + return _AppRegistry.Contract.Slash(&_AppRegistry.TransactOpts, user, amount, recipient, decision) +} + +// Unlock is a paid mutator transaction binding the contract method 0xa69df4b5. +// +// Solidity: function unlock() returns() +func (_AppRegistry *AppRegistryTransactor) Unlock(opts *bind.TransactOpts) (*types.Transaction, error) { + return _AppRegistry.contract.Transact(opts, "unlock") +} + +// Unlock is a paid mutator transaction binding the contract method 0xa69df4b5. +// +// Solidity: function unlock() returns() +func (_AppRegistry *AppRegistrySession) Unlock() (*types.Transaction, error) { + return _AppRegistry.Contract.Unlock(&_AppRegistry.TransactOpts) +} + +// Unlock is a paid mutator transaction binding the contract method 0xa69df4b5. +// +// Solidity: function unlock() returns() +func (_AppRegistry *AppRegistryTransactorSession) Unlock() (*types.Transaction, error) { + return _AppRegistry.Contract.Unlock(&_AppRegistry.TransactOpts) +} + +// Withdraw is a paid mutator transaction binding the contract method 0x51cff8d9. +// +// Solidity: function withdraw(address destination) returns() +func (_AppRegistry *AppRegistryTransactor) Withdraw(opts *bind.TransactOpts, destination common.Address) (*types.Transaction, error) { + return _AppRegistry.contract.Transact(opts, "withdraw", destination) +} + +// Withdraw is a paid mutator transaction binding the contract method 0x51cff8d9. +// +// Solidity: function withdraw(address destination) returns() +func (_AppRegistry *AppRegistrySession) Withdraw(destination common.Address) (*types.Transaction, error) { + return _AppRegistry.Contract.Withdraw(&_AppRegistry.TransactOpts, destination) +} + +// Withdraw is a paid mutator transaction binding the contract method 0x51cff8d9. +// +// Solidity: function withdraw(address destination) returns() +func (_AppRegistry *AppRegistryTransactorSession) Withdraw(destination common.Address) (*types.Transaction, error) { + return _AppRegistry.Contract.Withdraw(&_AppRegistry.TransactOpts, destination) +} + +// AppRegistryLockedIterator is returned from FilterLocked and is used to iterate over the raw logs and unpacked data for Locked events raised by the AppRegistry contract. +type AppRegistryLockedIterator struct { + Event *AppRegistryLocked // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *AppRegistryLockedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(AppRegistryLocked) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(AppRegistryLocked) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *AppRegistryLockedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *AppRegistryLockedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// AppRegistryLocked represents a Locked event raised by the AppRegistry contract. +type AppRegistryLocked struct { + User common.Address + Deposited *big.Int + NewBalance *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterLocked is a free log retrieval operation binding the contract event 0xd4665e3049283582ba6f9eba07a5b3e12dab49e02da99e8927a47af5d134bea5. +// +// Solidity: event Locked(address indexed user, uint256 deposited, uint256 newBalance) +func (_AppRegistry *AppRegistryFilterer) FilterLocked(opts *bind.FilterOpts, user []common.Address) (*AppRegistryLockedIterator, error) { + + var userRule []interface{} + for _, userItem := range user { + userRule = append(userRule, userItem) + } + + logs, sub, err := _AppRegistry.contract.FilterLogs(opts, "Locked", userRule) + if err != nil { + return nil, err + } + return &AppRegistryLockedIterator{contract: _AppRegistry.contract, event: "Locked", logs: logs, sub: sub}, nil +} + +// WatchLocked is a free log subscription operation binding the contract event 0xd4665e3049283582ba6f9eba07a5b3e12dab49e02da99e8927a47af5d134bea5. +// +// Solidity: event Locked(address indexed user, uint256 deposited, uint256 newBalance) +func (_AppRegistry *AppRegistryFilterer) WatchLocked(opts *bind.WatchOpts, sink chan<- *AppRegistryLocked, user []common.Address) (event.Subscription, error) { + + var userRule []interface{} + for _, userItem := range user { + userRule = append(userRule, userItem) + } + + logs, sub, err := _AppRegistry.contract.WatchLogs(opts, "Locked", userRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(AppRegistryLocked) + if err := _AppRegistry.contract.UnpackLog(event, "Locked", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseLocked is a log parse operation binding the contract event 0xd4665e3049283582ba6f9eba07a5b3e12dab49e02da99e8927a47af5d134bea5. +// +// Solidity: event Locked(address indexed user, uint256 deposited, uint256 newBalance) +func (_AppRegistry *AppRegistryFilterer) ParseLocked(log types.Log) (*AppRegistryLocked, error) { + event := new(AppRegistryLocked) + if err := _AppRegistry.contract.UnpackLog(event, "Locked", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// AppRegistryRelockedIterator is returned from FilterRelocked and is used to iterate over the raw logs and unpacked data for Relocked events raised by the AppRegistry contract. +type AppRegistryRelockedIterator struct { + Event *AppRegistryRelocked // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *AppRegistryRelockedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(AppRegistryRelocked) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(AppRegistryRelocked) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *AppRegistryRelockedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *AppRegistryRelockedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// AppRegistryRelocked represents a Relocked event raised by the AppRegistry contract. +type AppRegistryRelocked struct { + User common.Address + Balance *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRelocked is a free log retrieval operation binding the contract event 0xe2d155d9d74decc198a9a9b4f5bddb24ee0842ee745c9ce57e3573b971e50d9d. +// +// Solidity: event Relocked(address indexed user, uint256 balance) +func (_AppRegistry *AppRegistryFilterer) FilterRelocked(opts *bind.FilterOpts, user []common.Address) (*AppRegistryRelockedIterator, error) { + + var userRule []interface{} + for _, userItem := range user { + userRule = append(userRule, userItem) + } + + logs, sub, err := _AppRegistry.contract.FilterLogs(opts, "Relocked", userRule) + if err != nil { + return nil, err + } + return &AppRegistryRelockedIterator{contract: _AppRegistry.contract, event: "Relocked", logs: logs, sub: sub}, nil +} + +// WatchRelocked is a free log subscription operation binding the contract event 0xe2d155d9d74decc198a9a9b4f5bddb24ee0842ee745c9ce57e3573b971e50d9d. +// +// Solidity: event Relocked(address indexed user, uint256 balance) +func (_AppRegistry *AppRegistryFilterer) WatchRelocked(opts *bind.WatchOpts, sink chan<- *AppRegistryRelocked, user []common.Address) (event.Subscription, error) { + + var userRule []interface{} + for _, userItem := range user { + userRule = append(userRule, userItem) + } + + logs, sub, err := _AppRegistry.contract.WatchLogs(opts, "Relocked", userRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(AppRegistryRelocked) + if err := _AppRegistry.contract.UnpackLog(event, "Relocked", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseRelocked is a log parse operation binding the contract event 0xe2d155d9d74decc198a9a9b4f5bddb24ee0842ee745c9ce57e3573b971e50d9d. +// +// Solidity: event Relocked(address indexed user, uint256 balance) +func (_AppRegistry *AppRegistryFilterer) ParseRelocked(log types.Log) (*AppRegistryRelocked, error) { + event := new(AppRegistryRelocked) + if err := _AppRegistry.contract.UnpackLog(event, "Relocked", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// AppRegistryRoleAdminChangedIterator is returned from FilterRoleAdminChanged and is used to iterate over the raw logs and unpacked data for RoleAdminChanged events raised by the AppRegistry contract. +type AppRegistryRoleAdminChangedIterator struct { + Event *AppRegistryRoleAdminChanged // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *AppRegistryRoleAdminChangedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(AppRegistryRoleAdminChanged) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(AppRegistryRoleAdminChanged) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *AppRegistryRoleAdminChangedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *AppRegistryRoleAdminChangedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// AppRegistryRoleAdminChanged represents a RoleAdminChanged event raised by the AppRegistry contract. +type AppRegistryRoleAdminChanged struct { + Role [32]byte + PreviousAdminRole [32]byte + NewAdminRole [32]byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRoleAdminChanged is a free log retrieval operation binding the contract event 0xbd79b86ffe0ab8e8776151514217cd7cacd52c909f66475c3af44e129f0b00ff. +// +// Solidity: event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) +func (_AppRegistry *AppRegistryFilterer) FilterRoleAdminChanged(opts *bind.FilterOpts, role [][32]byte, previousAdminRole [][32]byte, newAdminRole [][32]byte) (*AppRegistryRoleAdminChangedIterator, error) { + + var roleRule []interface{} + for _, roleItem := range role { + roleRule = append(roleRule, roleItem) + } + var previousAdminRoleRule []interface{} + for _, previousAdminRoleItem := range previousAdminRole { + previousAdminRoleRule = append(previousAdminRoleRule, previousAdminRoleItem) + } + var newAdminRoleRule []interface{} + for _, newAdminRoleItem := range newAdminRole { + newAdminRoleRule = append(newAdminRoleRule, newAdminRoleItem) + } + + logs, sub, err := _AppRegistry.contract.FilterLogs(opts, "RoleAdminChanged", roleRule, previousAdminRoleRule, newAdminRoleRule) + if err != nil { + return nil, err + } + return &AppRegistryRoleAdminChangedIterator{contract: _AppRegistry.contract, event: "RoleAdminChanged", logs: logs, sub: sub}, nil +} + +// WatchRoleAdminChanged is a free log subscription operation binding the contract event 0xbd79b86ffe0ab8e8776151514217cd7cacd52c909f66475c3af44e129f0b00ff. +// +// Solidity: event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) +func (_AppRegistry *AppRegistryFilterer) WatchRoleAdminChanged(opts *bind.WatchOpts, sink chan<- *AppRegistryRoleAdminChanged, role [][32]byte, previousAdminRole [][32]byte, newAdminRole [][32]byte) (event.Subscription, error) { + + var roleRule []interface{} + for _, roleItem := range role { + roleRule = append(roleRule, roleItem) + } + var previousAdminRoleRule []interface{} + for _, previousAdminRoleItem := range previousAdminRole { + previousAdminRoleRule = append(previousAdminRoleRule, previousAdminRoleItem) + } + var newAdminRoleRule []interface{} + for _, newAdminRoleItem := range newAdminRole { + newAdminRoleRule = append(newAdminRoleRule, newAdminRoleItem) + } + + logs, sub, err := _AppRegistry.contract.WatchLogs(opts, "RoleAdminChanged", roleRule, previousAdminRoleRule, newAdminRoleRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(AppRegistryRoleAdminChanged) + if err := _AppRegistry.contract.UnpackLog(event, "RoleAdminChanged", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseRoleAdminChanged is a log parse operation binding the contract event 0xbd79b86ffe0ab8e8776151514217cd7cacd52c909f66475c3af44e129f0b00ff. +// +// Solidity: event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) +func (_AppRegistry *AppRegistryFilterer) ParseRoleAdminChanged(log types.Log) (*AppRegistryRoleAdminChanged, error) { + event := new(AppRegistryRoleAdminChanged) + if err := _AppRegistry.contract.UnpackLog(event, "RoleAdminChanged", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// AppRegistryRoleGrantedIterator is returned from FilterRoleGranted and is used to iterate over the raw logs and unpacked data for RoleGranted events raised by the AppRegistry contract. +type AppRegistryRoleGrantedIterator struct { + Event *AppRegistryRoleGranted // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *AppRegistryRoleGrantedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(AppRegistryRoleGranted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(AppRegistryRoleGranted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *AppRegistryRoleGrantedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *AppRegistryRoleGrantedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// AppRegistryRoleGranted represents a RoleGranted event raised by the AppRegistry contract. +type AppRegistryRoleGranted struct { + Role [32]byte + Account common.Address + Sender common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRoleGranted is a free log retrieval operation binding the contract event 0x2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d. +// +// Solidity: event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) +func (_AppRegistry *AppRegistryFilterer) FilterRoleGranted(opts *bind.FilterOpts, role [][32]byte, account []common.Address, sender []common.Address) (*AppRegistryRoleGrantedIterator, error) { + + var roleRule []interface{} + for _, roleItem := range role { + roleRule = append(roleRule, roleItem) + } + var accountRule []interface{} + for _, accountItem := range account { + accountRule = append(accountRule, accountItem) + } + var senderRule []interface{} + for _, senderItem := range sender { + senderRule = append(senderRule, senderItem) + } + + logs, sub, err := _AppRegistry.contract.FilterLogs(opts, "RoleGranted", roleRule, accountRule, senderRule) + if err != nil { + return nil, err + } + return &AppRegistryRoleGrantedIterator{contract: _AppRegistry.contract, event: "RoleGranted", logs: logs, sub: sub}, nil +} + +// WatchRoleGranted is a free log subscription operation binding the contract event 0x2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d. +// +// Solidity: event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) +func (_AppRegistry *AppRegistryFilterer) WatchRoleGranted(opts *bind.WatchOpts, sink chan<- *AppRegistryRoleGranted, role [][32]byte, account []common.Address, sender []common.Address) (event.Subscription, error) { + + var roleRule []interface{} + for _, roleItem := range role { + roleRule = append(roleRule, roleItem) + } + var accountRule []interface{} + for _, accountItem := range account { + accountRule = append(accountRule, accountItem) + } + var senderRule []interface{} + for _, senderItem := range sender { + senderRule = append(senderRule, senderItem) + } + + logs, sub, err := _AppRegistry.contract.WatchLogs(opts, "RoleGranted", roleRule, accountRule, senderRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(AppRegistryRoleGranted) + if err := _AppRegistry.contract.UnpackLog(event, "RoleGranted", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseRoleGranted is a log parse operation binding the contract event 0x2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d. +// +// Solidity: event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) +func (_AppRegistry *AppRegistryFilterer) ParseRoleGranted(log types.Log) (*AppRegistryRoleGranted, error) { + event := new(AppRegistryRoleGranted) + if err := _AppRegistry.contract.UnpackLog(event, "RoleGranted", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// AppRegistryRoleRevokedIterator is returned from FilterRoleRevoked and is used to iterate over the raw logs and unpacked data for RoleRevoked events raised by the AppRegistry contract. +type AppRegistryRoleRevokedIterator struct { + Event *AppRegistryRoleRevoked // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *AppRegistryRoleRevokedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(AppRegistryRoleRevoked) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(AppRegistryRoleRevoked) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *AppRegistryRoleRevokedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *AppRegistryRoleRevokedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// AppRegistryRoleRevoked represents a RoleRevoked event raised by the AppRegistry contract. +type AppRegistryRoleRevoked struct { + Role [32]byte + Account common.Address + Sender common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRoleRevoked is a free log retrieval operation binding the contract event 0xf6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b. +// +// Solidity: event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) +func (_AppRegistry *AppRegistryFilterer) FilterRoleRevoked(opts *bind.FilterOpts, role [][32]byte, account []common.Address, sender []common.Address) (*AppRegistryRoleRevokedIterator, error) { + + var roleRule []interface{} + for _, roleItem := range role { + roleRule = append(roleRule, roleItem) + } + var accountRule []interface{} + for _, accountItem := range account { + accountRule = append(accountRule, accountItem) + } + var senderRule []interface{} + for _, senderItem := range sender { + senderRule = append(senderRule, senderItem) + } + + logs, sub, err := _AppRegistry.contract.FilterLogs(opts, "RoleRevoked", roleRule, accountRule, senderRule) + if err != nil { + return nil, err + } + return &AppRegistryRoleRevokedIterator{contract: _AppRegistry.contract, event: "RoleRevoked", logs: logs, sub: sub}, nil +} + +// WatchRoleRevoked is a free log subscription operation binding the contract event 0xf6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b. +// +// Solidity: event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) +func (_AppRegistry *AppRegistryFilterer) WatchRoleRevoked(opts *bind.WatchOpts, sink chan<- *AppRegistryRoleRevoked, role [][32]byte, account []common.Address, sender []common.Address) (event.Subscription, error) { + + var roleRule []interface{} + for _, roleItem := range role { + roleRule = append(roleRule, roleItem) + } + var accountRule []interface{} + for _, accountItem := range account { + accountRule = append(accountRule, accountItem) + } + var senderRule []interface{} + for _, senderItem := range sender { + senderRule = append(senderRule, senderItem) + } + + logs, sub, err := _AppRegistry.contract.WatchLogs(opts, "RoleRevoked", roleRule, accountRule, senderRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(AppRegistryRoleRevoked) + if err := _AppRegistry.contract.UnpackLog(event, "RoleRevoked", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseRoleRevoked is a log parse operation binding the contract event 0xf6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b. +// +// Solidity: event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) +func (_AppRegistry *AppRegistryFilterer) ParseRoleRevoked(log types.Log) (*AppRegistryRoleRevoked, error) { + event := new(AppRegistryRoleRevoked) + if err := _AppRegistry.contract.UnpackLog(event, "RoleRevoked", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// AppRegistrySlashCooldownUpdatedIterator is returned from FilterSlashCooldownUpdated and is used to iterate over the raw logs and unpacked data for SlashCooldownUpdated events raised by the AppRegistry contract. +type AppRegistrySlashCooldownUpdatedIterator struct { + Event *AppRegistrySlashCooldownUpdated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *AppRegistrySlashCooldownUpdatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(AppRegistrySlashCooldownUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(AppRegistrySlashCooldownUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *AppRegistrySlashCooldownUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *AppRegistrySlashCooldownUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// AppRegistrySlashCooldownUpdated represents a SlashCooldownUpdated event raised by the AppRegistry contract. +type AppRegistrySlashCooldownUpdated struct { + OldCooldown *big.Int + NewCooldown *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSlashCooldownUpdated is a free log retrieval operation binding the contract event 0x21dd401bad58f48f88b208aad5157305ac7e8ec5db030042aaec08ebd7f50e4c. +// +// Solidity: event SlashCooldownUpdated(uint256 oldCooldown, uint256 newCooldown) +func (_AppRegistry *AppRegistryFilterer) FilterSlashCooldownUpdated(opts *bind.FilterOpts) (*AppRegistrySlashCooldownUpdatedIterator, error) { + + logs, sub, err := _AppRegistry.contract.FilterLogs(opts, "SlashCooldownUpdated") + if err != nil { + return nil, err + } + return &AppRegistrySlashCooldownUpdatedIterator{contract: _AppRegistry.contract, event: "SlashCooldownUpdated", logs: logs, sub: sub}, nil +} + +// WatchSlashCooldownUpdated is a free log subscription operation binding the contract event 0x21dd401bad58f48f88b208aad5157305ac7e8ec5db030042aaec08ebd7f50e4c. +// +// Solidity: event SlashCooldownUpdated(uint256 oldCooldown, uint256 newCooldown) +func (_AppRegistry *AppRegistryFilterer) WatchSlashCooldownUpdated(opts *bind.WatchOpts, sink chan<- *AppRegistrySlashCooldownUpdated) (event.Subscription, error) { + + logs, sub, err := _AppRegistry.contract.WatchLogs(opts, "SlashCooldownUpdated") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(AppRegistrySlashCooldownUpdated) + if err := _AppRegistry.contract.UnpackLog(event, "SlashCooldownUpdated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseSlashCooldownUpdated is a log parse operation binding the contract event 0x21dd401bad58f48f88b208aad5157305ac7e8ec5db030042aaec08ebd7f50e4c. +// +// Solidity: event SlashCooldownUpdated(uint256 oldCooldown, uint256 newCooldown) +func (_AppRegistry *AppRegistryFilterer) ParseSlashCooldownUpdated(log types.Log) (*AppRegistrySlashCooldownUpdated, error) { + event := new(AppRegistrySlashCooldownUpdated) + if err := _AppRegistry.contract.UnpackLog(event, "SlashCooldownUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// AppRegistrySlashedIterator is returned from FilterSlashed and is used to iterate over the raw logs and unpacked data for Slashed events raised by the AppRegistry contract. +type AppRegistrySlashedIterator struct { + Event *AppRegistrySlashed // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *AppRegistrySlashedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(AppRegistrySlashed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(AppRegistrySlashed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *AppRegistrySlashedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *AppRegistrySlashedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// AppRegistrySlashed represents a Slashed event raised by the AppRegistry contract. +type AppRegistrySlashed struct { + User common.Address + Amount *big.Int + Recipient common.Address + Decision []byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSlashed is a free log retrieval operation binding the contract event 0xf204f6a8b6d1439051d5fbd742389d0f778d18d21016e81c8ad3d4558266454c. +// +// Solidity: event Slashed(address indexed user, uint256 amount, address indexed recipient, bytes decision) +func (_AppRegistry *AppRegistryFilterer) FilterSlashed(opts *bind.FilterOpts, user []common.Address, recipient []common.Address) (*AppRegistrySlashedIterator, error) { + + var userRule []interface{} + for _, userItem := range user { + userRule = append(userRule, userItem) + } + + var recipientRule []interface{} + for _, recipientItem := range recipient { + recipientRule = append(recipientRule, recipientItem) + } + + logs, sub, err := _AppRegistry.contract.FilterLogs(opts, "Slashed", userRule, recipientRule) + if err != nil { + return nil, err + } + return &AppRegistrySlashedIterator{contract: _AppRegistry.contract, event: "Slashed", logs: logs, sub: sub}, nil +} + +// WatchSlashed is a free log subscription operation binding the contract event 0xf204f6a8b6d1439051d5fbd742389d0f778d18d21016e81c8ad3d4558266454c. +// +// Solidity: event Slashed(address indexed user, uint256 amount, address indexed recipient, bytes decision) +func (_AppRegistry *AppRegistryFilterer) WatchSlashed(opts *bind.WatchOpts, sink chan<- *AppRegistrySlashed, user []common.Address, recipient []common.Address) (event.Subscription, error) { + + var userRule []interface{} + for _, userItem := range user { + userRule = append(userRule, userItem) + } + + var recipientRule []interface{} + for _, recipientItem := range recipient { + recipientRule = append(recipientRule, recipientItem) + } + + logs, sub, err := _AppRegistry.contract.WatchLogs(opts, "Slashed", userRule, recipientRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(AppRegistrySlashed) + if err := _AppRegistry.contract.UnpackLog(event, "Slashed", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseSlashed is a log parse operation binding the contract event 0xf204f6a8b6d1439051d5fbd742389d0f778d18d21016e81c8ad3d4558266454c. +// +// Solidity: event Slashed(address indexed user, uint256 amount, address indexed recipient, bytes decision) +func (_AppRegistry *AppRegistryFilterer) ParseSlashed(log types.Log) (*AppRegistrySlashed, error) { + event := new(AppRegistrySlashed) + if err := _AppRegistry.contract.UnpackLog(event, "Slashed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// AppRegistryUnlockInitiatedIterator is returned from FilterUnlockInitiated and is used to iterate over the raw logs and unpacked data for UnlockInitiated events raised by the AppRegistry contract. +type AppRegistryUnlockInitiatedIterator struct { + Event *AppRegistryUnlockInitiated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *AppRegistryUnlockInitiatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(AppRegistryUnlockInitiated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(AppRegistryUnlockInitiated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *AppRegistryUnlockInitiatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *AppRegistryUnlockInitiatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// AppRegistryUnlockInitiated represents a UnlockInitiated event raised by the AppRegistry contract. +type AppRegistryUnlockInitiated struct { + User common.Address + Balance *big.Int + AvailableAt *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterUnlockInitiated is a free log retrieval operation binding the contract event 0xd73fc4aafa5101b91e88b8c2fa75d8d6db73466773addb11c079d063c1e63c0c. +// +// Solidity: event UnlockInitiated(address indexed user, uint256 balance, uint256 availableAt) +func (_AppRegistry *AppRegistryFilterer) FilterUnlockInitiated(opts *bind.FilterOpts, user []common.Address) (*AppRegistryUnlockInitiatedIterator, error) { + + var userRule []interface{} + for _, userItem := range user { + userRule = append(userRule, userItem) + } + + logs, sub, err := _AppRegistry.contract.FilterLogs(opts, "UnlockInitiated", userRule) + if err != nil { + return nil, err + } + return &AppRegistryUnlockInitiatedIterator{contract: _AppRegistry.contract, event: "UnlockInitiated", logs: logs, sub: sub}, nil +} + +// WatchUnlockInitiated is a free log subscription operation binding the contract event 0xd73fc4aafa5101b91e88b8c2fa75d8d6db73466773addb11c079d063c1e63c0c. +// +// Solidity: event UnlockInitiated(address indexed user, uint256 balance, uint256 availableAt) +func (_AppRegistry *AppRegistryFilterer) WatchUnlockInitiated(opts *bind.WatchOpts, sink chan<- *AppRegistryUnlockInitiated, user []common.Address) (event.Subscription, error) { + + var userRule []interface{} + for _, userItem := range user { + userRule = append(userRule, userItem) + } + + logs, sub, err := _AppRegistry.contract.WatchLogs(opts, "UnlockInitiated", userRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(AppRegistryUnlockInitiated) + if err := _AppRegistry.contract.UnpackLog(event, "UnlockInitiated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseUnlockInitiated is a log parse operation binding the contract event 0xd73fc4aafa5101b91e88b8c2fa75d8d6db73466773addb11c079d063c1e63c0c. +// +// Solidity: event UnlockInitiated(address indexed user, uint256 balance, uint256 availableAt) +func (_AppRegistry *AppRegistryFilterer) ParseUnlockInitiated(log types.Log) (*AppRegistryUnlockInitiated, error) { + event := new(AppRegistryUnlockInitiated) + if err := _AppRegistry.contract.UnpackLog(event, "UnlockInitiated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// AppRegistryWithdrawnIterator is returned from FilterWithdrawn and is used to iterate over the raw logs and unpacked data for Withdrawn events raised by the AppRegistry contract. +type AppRegistryWithdrawnIterator struct { + Event *AppRegistryWithdrawn // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *AppRegistryWithdrawnIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(AppRegistryWithdrawn) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(AppRegistryWithdrawn) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *AppRegistryWithdrawnIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *AppRegistryWithdrawnIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// AppRegistryWithdrawn represents a Withdrawn event raised by the AppRegistry contract. +type AppRegistryWithdrawn struct { + User common.Address + Balance *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterWithdrawn is a free log retrieval operation binding the contract event 0x7084f5476618d8e60b11ef0d7d3f06914655adb8793e28ff7f018d4c76d505d5. +// +// Solidity: event Withdrawn(address indexed user, uint256 balance) +func (_AppRegistry *AppRegistryFilterer) FilterWithdrawn(opts *bind.FilterOpts, user []common.Address) (*AppRegistryWithdrawnIterator, error) { + + var userRule []interface{} + for _, userItem := range user { + userRule = append(userRule, userItem) + } + + logs, sub, err := _AppRegistry.contract.FilterLogs(opts, "Withdrawn", userRule) + if err != nil { + return nil, err + } + return &AppRegistryWithdrawnIterator{contract: _AppRegistry.contract, event: "Withdrawn", logs: logs, sub: sub}, nil +} + +// WatchWithdrawn is a free log subscription operation binding the contract event 0x7084f5476618d8e60b11ef0d7d3f06914655adb8793e28ff7f018d4c76d505d5. +// +// Solidity: event Withdrawn(address indexed user, uint256 balance) +func (_AppRegistry *AppRegistryFilterer) WatchWithdrawn(opts *bind.WatchOpts, sink chan<- *AppRegistryWithdrawn, user []common.Address) (event.Subscription, error) { + + var userRule []interface{} + for _, userItem := range user { + userRule = append(userRule, userItem) + } + + logs, sub, err := _AppRegistry.contract.WatchLogs(opts, "Withdrawn", userRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(AppRegistryWithdrawn) + if err := _AppRegistry.contract.UnpackLog(event, "Withdrawn", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseWithdrawn is a log parse operation binding the contract event 0x7084f5476618d8e60b11ef0d7d3f06914655adb8793e28ff7f018d4c76d505d5. +// +// Solidity: event Withdrawn(address indexed user, uint256 balance) +func (_AppRegistry *AppRegistryFilterer) ParseWithdrawn(log types.Log) (*AppRegistryWithdrawn, error) { + event := new(AppRegistryWithdrawn) + if err := _AppRegistry.contract.UnpackLog(event, "Withdrawn", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/pkg/blockchain/evm/base_client.go b/pkg/blockchain/evm/base_client.go new file mode 100644 index 000000000..83bd50442 --- /dev/null +++ b/pkg/blockchain/evm/base_client.go @@ -0,0 +1,7 @@ +package evm + +// BaseClient holds the shared fields for all EVM-based clients. +type BaseClient struct { + evmClient EVMClient + blockchainID uint64 +} diff --git a/pkg/blockchain/evm/client.go b/pkg/blockchain/evm/blockchain_client.go similarity index 80% rename from pkg/blockchain/evm/client.go rename to pkg/blockchain/evm/blockchain_client.go index 1620b2c97..c922c354a 100644 --- a/pkg/blockchain/evm/client.go +++ b/pkg/blockchain/evm/blockchain_client.go @@ -2,7 +2,9 @@ package evm import ( "context" + "math/big" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -14,16 +16,16 @@ import ( "github.com/layer-3/nitrolite/pkg/sign" ) -var _ core.Client = &Client{} +var _ core.BlockchainClient = &BlockchainClient{} -type Client struct { - contract *ChannelHub - transactOpts *bind.TransactOpts - nodeAddress common.Address - contractAddress common.Address - blockchainID uint64 - assetStore AssetStore - evmClient EVMClient +type BlockchainClient struct { + BaseClient + contract *ChannelHub + transactOpts *bind.TransactOpts + txSigner sign.Signer + nodeAddress common.Address + channelHubContractAddress common.Address + assetStore AssetStore requireCheckAllowance bool requireCheckBalance bool @@ -31,10 +33,10 @@ type Client struct { } type ClientOption interface { - apply(c *Client) + apply(c *BlockchainClient) } -func NewClient( +func NewBlockchainClient( contractAddress common.Address, evmClient EVMClient, txSigner sign.Signer, @@ -42,19 +44,22 @@ func NewClient( nodeAddress string, assetStore AssetStore, opts ...ClientOption, -) (*Client, error) { +) (*BlockchainClient, error) { contract, err := NewChannelHub(contractAddress, evmClient) if err != nil { return nil, errors.Wrap(err, "failed to create ChannelHub contract instance") } - client := &Client{ - contract: contract, - transactOpts: signerTxOpts(txSigner, blockchainID), - nodeAddress: common.HexToAddress(nodeAddress), - contractAddress: contractAddress, - blockchainID: blockchainID, - assetStore: assetStore, - evmClient: evmClient, + client := &BlockchainClient{ + BaseClient: BaseClient{ + evmClient: evmClient, + blockchainID: blockchainID, + }, + contract: contract, + transactOpts: signerTxOpts(txSigner, blockchainID), + txSigner: txSigner, + nodeAddress: common.HexToAddress(nodeAddress), + channelHubContractAddress: contractAddress, + assetStore: assetStore, requireCheckAllowance: true, requireCheckBalance: true, @@ -69,7 +74,7 @@ func NewClient( // ========= Getters - IVault ========= -func (c *Client) GetAccountsBalances(accounts []string, tokens []string) ([][]decimal.Decimal, error) { +func (c *BlockchainClient) GetAccountsBalances(accounts []string, tokens []string) ([][]decimal.Decimal, error) { if len(accounts) == 0 || len(tokens) == 0 { return [][]decimal.Decimal{}, nil } @@ -92,7 +97,7 @@ func (c *Client) GetAccountsBalances(accounts []string, tokens []string) ([][]de return result, nil } -func (c *Client) getAllowance(asset string, owner string) (decimal.Decimal, error) { +func (c *BlockchainClient) getAllowance(asset string, owner string) (decimal.Decimal, error) { tokenAddrHex, err := c.assetStore.GetTokenAddress(asset, c.blockchainID) if err != nil { return decimal.Zero, errors.Wrap(err, "failed to get token address") @@ -109,7 +114,7 @@ func (c *Client) getAllowance(asset string, owner string) (decimal.Decimal, erro if err != nil { return decimal.Zero, errors.Wrap(err, "failed to instantiate token contract") } - allowance, err := erc20Contract.Allowance(&bind.CallOpts{}, ownerAddr, c.contractAddress) + allowance, err := erc20Contract.Allowance(&bind.CallOpts{}, ownerAddr, c.channelHubContractAddress) if err != nil { return decimal.Zero, errors.Wrapf(err, "failed to get allowance for token %s", asset) } @@ -122,7 +127,7 @@ func (c *Client) getAllowance(asset string, owner string) (decimal.Decimal, erro return decimal.NewFromBigInt(allowance, -int32(decimals)), nil } -func (c *Client) GetTokenBalance(asset string, walletAddress string) (decimal.Decimal, error) { +func (c *BlockchainClient) GetTokenBalance(asset string, walletAddress string) (decimal.Decimal, error) { tokenAddrHex, err := c.assetStore.GetTokenAddress(asset, c.blockchainID) if err != nil { return decimal.Zero, errors.Wrap(err, "failed to get token address") @@ -160,7 +165,7 @@ func (c *Client) GetTokenBalance(asset string, walletAddress string) (decimal.De // ========= Getters - ChannelsHub ========= -func (c *Client) GetNodeBalance(token string) (decimal.Decimal, error) { +func (c *BlockchainClient) GetNodeBalance(token string) (decimal.Decimal, error) { tokenAddr := common.HexToAddress(token) balance, err := c.contract.GetAccountBalance(nil, c.nodeAddress, tokenAddr) if err != nil { @@ -173,7 +178,7 @@ func (c *Client) GetNodeBalance(token string) (decimal.Decimal, error) { return decimal.NewFromBigInt(balance, -int32(decimals)), nil } -func (c *Client) GetOpenChannels(user string) ([]string, error) { +func (c *BlockchainClient) GetOpenChannels(user string) ([]string, error) { userAddr := common.HexToAddress(user) channelIDs, err := c.contract.GetOpenChannels(nil, userAddr) if err != nil { @@ -187,7 +192,7 @@ func (c *Client) GetOpenChannels(user string) ([]string, error) { return result, nil } -func (c *Client) GetHomeChannelData(homeChannelID string) (core.HomeChannelDataResponse, error) { +func (c *BlockchainClient) GetHomeChannelData(homeChannelID string) (core.HomeChannelDataResponse, error) { channelIDBytes, err := hexToBytes32(homeChannelID) if err != nil { return core.HomeChannelDataResponse{}, errors.Wrap(err, "invalid channel ID") @@ -214,7 +219,7 @@ func (c *Client) GetHomeChannelData(homeChannelID string) (core.HomeChannelDataR }, nil } -func (c *Client) GetEscrowDepositData(escrowChannelID string) (core.EscrowDepositDataResponse, error) { +func (c *BlockchainClient) GetEscrowDepositData(escrowChannelID string) (core.EscrowDepositDataResponse, error) { escrowIDBytes, err := hexToBytes32(escrowChannelID) if err != nil { return core.EscrowDepositDataResponse{}, errors.Wrap(err, "invalid escrow ID") @@ -232,14 +237,14 @@ func (c *Client) GetEscrowDepositData(escrowChannelID string) (core.EscrowDeposi return core.EscrowDepositDataResponse{ EscrowChannelID: escrowChannelID, - Node: c.contractAddress.Hex(), + Node: c.channelHubContractAddress.Hex(), LastState: *lastState, UnlockExpiry: data.UnlockAt, ChallengeExpiry: data.ChallengeExpiry, }, nil } -func (c *Client) GetEscrowWithdrawalData(escrowChannelID string) (core.EscrowWithdrawalDataResponse, error) { +func (c *BlockchainClient) GetEscrowWithdrawalData(escrowChannelID string) (core.EscrowWithdrawalDataResponse, error) { escrowIDBytes, err := hexToBytes32(escrowChannelID) if err != nil { return core.EscrowWithdrawalDataResponse{}, errors.Wrap(err, "invalid escrow ID") @@ -257,14 +262,14 @@ func (c *Client) GetEscrowWithdrawalData(escrowChannelID string) (core.EscrowWit return core.EscrowWithdrawalDataResponse{ EscrowChannelID: escrowChannelID, - Node: c.contractAddress.Hex(), + Node: c.channelHubContractAddress.Hex(), LastState: *lastState, }, nil } // ========= IVault Functions ========= -func (c *Client) Deposit(node, token string, amount decimal.Decimal) (string, error) { +func (c *BlockchainClient) Deposit(node, token string, amount decimal.Decimal) (string, error) { nodeAddr := common.HexToAddress(node) tokenAddr := common.HexToAddress(token) @@ -294,7 +299,7 @@ func (c *Client) Deposit(node, token string, amount decimal.Decimal) (string, er return tx.Hash().Hex(), nil } -func (c *Client) Withdraw(node, token string, amount decimal.Decimal) (string, error) { +func (c *BlockchainClient) Withdraw(node, token string, amount decimal.Decimal) (string, error) { nodeAddr := common.HexToAddress(node) tokenAddr := common.HexToAddress(token) @@ -321,7 +326,7 @@ func (c *Client) Withdraw(node, token string, amount decimal.Decimal) (string, e // ========= Getters - ERC20 ========= -func (c *Client) Approve(asset string, amount decimal.Decimal) (string, error) { +func (c *BlockchainClient) Approve(asset string, amount decimal.Decimal) (string, error) { tokenAddrHex, err := c.assetStore.GetTokenAddress(asset, c.blockchainID) if err != nil { return "", errors.Wrap(err, "failed to get token address") @@ -351,7 +356,7 @@ func (c *Client) Approve(asset string, amount decimal.Decimal) (string, error) { return "", err } - tx, err := erc20Contract.Approve(c.transactOpts, c.contractAddress, amountBig) + tx, err := erc20Contract.Approve(c.transactOpts, c.channelHubContractAddress, amountBig) if err != nil { return "", errors.Wrap(err, "failed to approve token spending") } @@ -361,7 +366,7 @@ func (c *Client) Approve(asset string, amount decimal.Decimal) (string, error) { // ========= Channel Lifecycle ========= -func (c *Client) Create(def core.ChannelDefinition, initCCS core.State) (string, error) { +func (c *BlockchainClient) Create(def core.ChannelDefinition, initCCS core.State) (string, error) { contractDef, err := coreDefToContractDef(def, initCCS.Asset, initCCS.UserWallet, c.nodeAddress) if err != nil { return "", errors.Wrap(err, "failed to convert channel definition") @@ -421,7 +426,7 @@ func (c *Client) Create(def core.ChannelDefinition, initCCS core.State) (string, return tx.Hash().Hex(), nil } -func (c *Client) MigrateChannelHere(def core.ChannelDefinition, candidate core.State) (string, error) { +func (c *BlockchainClient) MigrateChannelHere(def core.ChannelDefinition, candidate core.State) (string, error) { contractDef, err := coreDefToContractDef(def, candidate.Asset, candidate.UserWallet, c.nodeAddress) if err != nil { return "", errors.Wrap(err, "failed to convert channel definition") @@ -444,7 +449,7 @@ func (c *Client) MigrateChannelHere(def core.ChannelDefinition, candidate core.S return tx.Hash().Hex(), nil } -func (c *Client) Checkpoint(candidate core.State) (string, error) { +func (c *BlockchainClient) Checkpoint(candidate core.State) (string, error) { if candidate.HomeChannelID == nil { return "", errors.New("candidate state must have a home channel ID") } @@ -511,7 +516,7 @@ func (c *Client) Checkpoint(candidate core.State) (string, error) { return tx.Hash().Hex(), nil } -func (c *Client) Challenge(candidate core.State, challengerSig []byte, challengerIdx core.ChannelParticipant) (string, error) { +func (c *BlockchainClient) Challenge(candidate core.State, challengerSig []byte, challengerIdx core.ChannelParticipant) (string, error) { if candidate.HomeChannelID == nil { return "", errors.New("candidate state must have a home channel ID") } @@ -539,7 +544,7 @@ func (c *Client) Challenge(candidate core.State, challengerSig []byte, challenge return tx.Hash().Hex(), nil } -func (c *Client) Close(candidate core.State) (string, error) { +func (c *BlockchainClient) Close(candidate core.State) (string, error) { if candidate.HomeChannelID == nil { return "", errors.New("candidate state must have a home channel ID") } @@ -573,7 +578,7 @@ func (c *Client) Close(candidate core.State) (string, error) { // ========= Escrow Deposit ========= -func (c *Client) InitiateEscrowDeposit(def core.ChannelDefinition, initCCS core.State) (string, error) { +func (c *BlockchainClient) InitiateEscrowDeposit(def core.ChannelDefinition, initCCS core.State) (string, error) { contractDef, err := coreDefToContractDef(def, initCCS.Asset, initCCS.UserWallet, c.nodeAddress) if err != nil { return "", errors.Wrap(err, "failed to convert channel definition") @@ -600,7 +605,7 @@ func (c *Client) InitiateEscrowDeposit(def core.ChannelDefinition, initCCS core. return tx.Hash().Hex(), nil } -func (c *Client) ChallengeEscrowDeposit(candidate core.State, challengerSig []byte, challengerIdx core.ChannelParticipant) (string, error) { +func (c *BlockchainClient) ChallengeEscrowDeposit(candidate core.State, challengerSig []byte, challengerIdx core.ChannelParticipant) (string, error) { if candidate.EscrowChannelID == nil { return "", errors.New("candidate state must have an escrow channel ID") } @@ -622,7 +627,7 @@ func (c *Client) ChallengeEscrowDeposit(candidate core.State, challengerSig []by return tx.Hash().Hex(), nil } -func (c *Client) FinalizeEscrowDeposit(candidate core.State) (string, error) { +func (c *BlockchainClient) FinalizeEscrowDeposit(candidate core.State) (string, error) { if candidate.EscrowChannelID == nil { return "", errors.New("candidate state must have an escrow channel ID") } @@ -655,7 +660,7 @@ func (c *Client) FinalizeEscrowDeposit(candidate core.State) (string, error) { // ========= Escrow Withdrawal ========= -func (c *Client) InitiateEscrowWithdrawal(def core.ChannelDefinition, initCCS core.State) (string, error) { +func (c *BlockchainClient) InitiateEscrowWithdrawal(def core.ChannelDefinition, initCCS core.State) (string, error) { contractDef, err := coreDefToContractDef(def, initCCS.Asset, initCCS.UserWallet, c.nodeAddress) if err != nil { return "", errors.Wrap(err, "failed to convert channel definition") @@ -682,7 +687,7 @@ func (c *Client) InitiateEscrowWithdrawal(def core.ChannelDefinition, initCCS co return tx.Hash().Hex(), nil } -func (c *Client) ChallengeEscrowWithdrawal(candidate core.State, challengerSig []byte, challengerIdx core.ChannelParticipant) (string, error) { +func (c *BlockchainClient) ChallengeEscrowWithdrawal(candidate core.State, challengerSig []byte, challengerIdx core.ChannelParticipant) (string, error) { if candidate.EscrowChannelID == nil { return "", errors.New("candidate state must have an escrow channel ID") } @@ -704,7 +709,7 @@ func (c *Client) ChallengeEscrowWithdrawal(candidate core.State, challengerSig [ return tx.Hash().Hex(), nil } -func (c *Client) FinalizeEscrowWithdrawal(candidate core.State) (string, error) { +func (c *BlockchainClient) FinalizeEscrowWithdrawal(candidate core.State) (string, error) { if candidate.EscrowChannelID == nil { return "", errors.New("candidate state must have an escrow channel ID") } @@ -737,3 +742,50 @@ func (c *Client) FinalizeEscrowWithdrawal(candidate core.State) (string, error) return tx.Hash().Hex(), nil } + +func (c *BlockchainClient) EnsureSigValidatorRegistered(validatorID uint8, validatorAddress string, checkOnly bool) error { + validatorAddr := common.HexToAddress(validatorAddress) + + _validatorAddr, err := c.contract.GetNodeValidator(nil, c.nodeAddress, validatorID) + if err != nil { + return errors.Wrapf(err, "failed to check if validator %d is registered", validatorID) + } + if _validatorAddr.Hex() == validatorAddr.Hex() { + return nil + } else if _validatorAddr != (common.Address{}) { + return errors.Errorf("validator ID %d is already registered with a different address %s", validatorID, _validatorAddr.Hex()) + } + + if checkOnly { + return errors.Errorf("validator ID %d with address %s is not registered; run 'clearnode operator register-validator' to register", validatorID, validatorAddress) + } + + if err := c.checkFeeFn(context.Background(), c.transactOpts.From); err != nil { + return err + } + + uint8Type, _ := abi.NewType("uint8", "", nil) + addressType, _ := abi.NewType("address", "", nil) + uint256Type, _ := abi.NewType("uint256", "", nil) + args := abi.Arguments{ + {Type: uint8Type}, + {Type: addressType}, + {Type: uint256Type}, + } + message, err := args.Pack(validatorID, validatorAddr, new(big.Int).SetUint64(c.blockchainID)) + if err != nil { + return errors.Wrap(err, "failed to encode validator registration message") + } + + sig, err := c.txSigner.Sign(sign.ComputeEthereumSignedMessageHash(message)) + if err != nil { + return errors.Wrap(err, "failed to sign validator registration message") + } + + _, err = c.contract.RegisterNodeValidator(c.transactOpts, c.nodeAddress, validatorID, validatorAddr, sig) + if err != nil { + return errors.Wrapf(err, "failed to register validator %d with address %s", validatorID, validatorAddress) + } + + return nil +} diff --git a/pkg/blockchain/evm/client_test.go b/pkg/blockchain/evm/blockchain_test.go similarity index 88% rename from pkg/blockchain/evm/client_test.go rename to pkg/blockchain/evm/blockchain_test.go index 98a7d479b..8d3318015 100644 --- a/pkg/blockchain/evm/client_test.go +++ b/pkg/blockchain/evm/blockchain_test.go @@ -47,7 +47,7 @@ func (m *MockPublicKey) Bytes() []byte { return m.pubBytes } -func TestNewClient(t *testing.T) { +func TestNewBlockchainClient(t *testing.T) { t.Parallel() mockEVMClient := new(MockEVMClient) mockAssetStore := new(MockAssetStore) @@ -60,7 +60,7 @@ func TestNewClient(t *testing.T) { blockchainID := uint64(1337) // Mock models simulate an EVM client where NewClient/NewChannelHub return a local struct without external calls - client, err := NewClient( + client, err := NewBlockchainClient( contractAddress, mockEVMClient, mockSigner, @@ -75,7 +75,7 @@ func TestNewClient(t *testing.T) { mock.AssertExpectationsForObjects(t, mockEVMClient, mockAssetStore, mockSigner) } -func TestClient_GetAccountsBalances(t *testing.T) { +func TestBlockchainClient_GetAccountsBalances(t *testing.T) { t.Parallel() mockEVMClient := new(MockEVMClient) mockAssetStore := new(MockAssetStore) @@ -84,7 +84,7 @@ func TestClient_GetAccountsBalances(t *testing.T) { setupMockSigner(t, mockSigner) contractAddress := common.HexToAddress("0xContract") - client, err := NewClient(contractAddress, mockEVMClient, mockSigner, 1, "0xNode", mockAssetStore) + client, err := NewBlockchainClient(contractAddress, mockEVMClient, mockSigner, 1, "0xNode", mockAssetStore) require.NoError(t, err) accounts := []string{"0xUser1", "0xUser2"} @@ -104,7 +104,7 @@ func TestClient_GetAccountsBalances(t *testing.T) { mock.AssertExpectationsForObjects(t, mockEVMClient, mockAssetStore, mockSigner) } -func TestClient_GetNodeBalance(t *testing.T) { +func TestBlockchainClient_GetNodeBalance(t *testing.T) { t.Parallel() mockEVMClient := new(MockEVMClient) mockAssetStore := new(MockAssetStore) @@ -112,7 +112,7 @@ func TestClient_GetNodeBalance(t *testing.T) { setupMockSigner(t, mockSigner) - client, err := NewClient(common.Address{}, mockEVMClient, mockSigner, 1, "0xNode", mockAssetStore) + client, err := NewBlockchainClient(common.Address{}, mockEVMClient, mockSigner, 1, "0xNode", mockAssetStore) require.NoError(t, err) token := "0xToken" @@ -129,7 +129,7 @@ func TestClient_GetNodeBalance(t *testing.T) { mock.AssertExpectationsForObjects(t, mockEVMClient, mockAssetStore, mockSigner) } -func TestClient_GetOpenChannels(t *testing.T) { +func TestBlockchainClient_GetOpenChannels(t *testing.T) { t.Parallel() mockEVMClient := new(MockEVMClient) mockAssetStore := new(MockAssetStore) @@ -137,7 +137,7 @@ func TestClient_GetOpenChannels(t *testing.T) { setupMockSigner(t, mockSigner) - client, err := NewClient(common.Address{}, mockEVMClient, mockSigner, 1, "0xNode", mockAssetStore) + client, err := NewBlockchainClient(common.Address{}, mockEVMClient, mockSigner, 1, "0xNode", mockAssetStore) require.NoError(t, err) // Mock GetOpenChannels return: bytes32[] diff --git a/pkg/blockchain/evm/channel_hub.go b/pkg/blockchain/evm/channel_hub_abi.go similarity index 100% rename from pkg/blockchain/evm/channel_hub.go rename to pkg/blockchain/evm/channel_hub_abi.go diff --git a/pkg/blockchain/evm/reactor.go b/pkg/blockchain/evm/channel_hub_reactor.go similarity index 63% rename from pkg/blockchain/evm/reactor.go rename to pkg/blockchain/evm/channel_hub_reactor.go index f0036183a..5bd6db8c4 100644 --- a/pkg/blockchain/evm/reactor.go +++ b/pkg/blockchain/evm/channel_hub_reactor.go @@ -14,36 +14,36 @@ import ( "github.com/layer-3/nitrolite/pkg/log" ) -var contractAbi *abi.ABI -var contractFilterer *ChannelHubFilterer -var eventMapping map[common.Hash]string +var channelHubAbi *abi.ABI +var channelHubFilterer *ChannelHubFilterer +var channelHubEventMapping map[common.Hash]string -func init() { +func initChannelHub() { var err error - contractAbi, err = ChannelHubMetaData.GetAbi() + channelHubAbi, err = ChannelHubMetaData.GetAbi() if err != nil { panic(err) } // Create a filterer for parsing events (address not needed for parsing) - contract := bind.NewBoundContract(common.Address{}, *contractAbi, nil, nil, nil) - contractFilterer = &ChannelHubFilterer{contract: contract} + contract := bind.NewBoundContract(common.Address{}, *channelHubAbi, nil, nil, nil) + channelHubFilterer = &ChannelHubFilterer{contract: contract} - eventMapping = make(map[common.Hash]string) - for name, event := range contractAbi.Events { - eventMapping[event.ID] = name + channelHubEventMapping = make(map[common.Hash]string) + for name, event := range channelHubAbi.Events { + channelHubEventMapping[event.ID] = name } } -type Reactor struct { +type ChannelHubReactor struct { blockchainID uint64 - eventHandler core.BlockchainEventHandler + eventHandler core.ChannelHubEventHandler storeContractEvent StoreContractEvent onEventProcessed func(blockchainID uint64, success bool) } -func NewReactor(blockchainID uint64, eventHandler core.BlockchainEventHandler, storeContractEvent StoreContractEvent) *Reactor { - return &Reactor{ +func NewChannelHubReactor(blockchainID uint64, eventHandler core.ChannelHubEventHandler, storeContractEvent StoreContractEvent) *ChannelHubReactor { + return &ChannelHubReactor{ blockchainID: blockchainID, eventHandler: eventHandler, storeContractEvent: storeContractEvent, @@ -51,78 +51,79 @@ func NewReactor(blockchainID uint64, eventHandler core.BlockchainEventHandler, s } // SetOnEventProcessed sets an optional callback invoked after each event is processed. -func (r *Reactor) SetOnEventProcessed(fn func(blockchainID uint64, success bool)) { +func (r *ChannelHubReactor) SetOnEventProcessed(fn func(blockchainID uint64, success bool)) { r.onEventProcessed = fn } -func (r *Reactor) HandleEvent(ctx context.Context, l types.Log) { +func (r *ChannelHubReactor) HandleEvent(ctx context.Context, l types.Log) error { logger := log.FromContext(ctx) eventID := l.Topics[0] - eventName, ok := eventMapping[eventID] + eventName, ok := channelHubEventMapping[eventID] if !ok { logger.Warn("unknown event ID", "eventID", eventID.Hex(), "blockNumber", l.BlockNumber, "txHash", l.TxHash.String(), "logIndex", l.Index) - return + return nil } logger.Debug("received event", "name", eventName, "blockNumber", l.BlockNumber, "txHash", l.TxHash.String(), "logIndex", l.Index) var err error switch eventID { - case contractAbi.Events["ChannelCreated"].ID: + case channelHubAbi.Events["ChannelCreated"].ID: err = r.handleHomeChannelCreated(ctx, l) - case contractAbi.Events["ChannelCheckpointed"].ID: + case channelHubAbi.Events["ChannelCheckpointed"].ID: err = r.handleHomeChannelCheckpointed(ctx, l) - case contractAbi.Events["ChannelDeposited"].ID: + case channelHubAbi.Events["ChannelDeposited"].ID: err = r.handleChannelDeposited(ctx, l) - case contractAbi.Events["ChannelWithdrawn"].ID: + case channelHubAbi.Events["ChannelWithdrawn"].ID: err = r.handleChannelWithdrawn(ctx, l) - case contractAbi.Events["ChannelChallenged"].ID: + case channelHubAbi.Events["ChannelChallenged"].ID: err = r.handleHomeChannelChallenged(ctx, l) - case contractAbi.Events["ChannelClosed"].ID: + case channelHubAbi.Events["ChannelClosed"].ID: err = r.handleHomeChannelClosed(ctx, l) - case contractAbi.Events["EscrowDepositInitiated"].ID: + case channelHubAbi.Events["EscrowDepositInitiated"].ID: err = r.handleEscrowDepositInitiated(ctx, l) - case contractAbi.Events["EscrowDepositChallenged"].ID: + case channelHubAbi.Events["EscrowDepositChallenged"].ID: err = r.handleEscrowDepositChallenged(ctx, l) - case contractAbi.Events["EscrowDepositFinalized"].ID: + case channelHubAbi.Events["EscrowDepositFinalized"].ID: err = r.handleEscrowDepositFinalized(ctx, l) - case contractAbi.Events["EscrowWithdrawalInitiated"].ID: + case channelHubAbi.Events["EscrowWithdrawalInitiated"].ID: err = r.handleEscrowWithdrawalInitiated(ctx, l) - case contractAbi.Events["EscrowWithdrawalChallenged"].ID: + case channelHubAbi.Events["EscrowWithdrawalChallenged"].ID: err = r.handleEscrowWithdrawalChallenged(ctx, l) - case contractAbi.Events["EscrowWithdrawalFinalized"].ID: + case channelHubAbi.Events["EscrowWithdrawalFinalized"].ID: err = r.handleEscrowWithdrawalFinalized(ctx, l) - case contractAbi.Events["EscrowDepositInitiatedOnHome"].ID: + case channelHubAbi.Events["EscrowDepositInitiatedOnHome"].ID: err = r.handleEscrowDepositInitiatedOnHome(ctx, l) - case contractAbi.Events["EscrowDepositFinalizedOnHome"].ID: + case channelHubAbi.Events["EscrowDepositFinalizedOnHome"].ID: err = r.handleEscrowDepositFinalizedOnHome(ctx, l) - case contractAbi.Events["EscrowWithdrawalInitiatedOnHome"].ID: + case channelHubAbi.Events["EscrowWithdrawalInitiatedOnHome"].ID: err = r.handleEscrowWithdrawalInitiatedOnHome(ctx, l) - case contractAbi.Events["EscrowWithdrawalFinalizedOnHome"].ID: + case channelHubAbi.Events["EscrowWithdrawalFinalizedOnHome"].ID: err = r.handleEscrowWithdrawalFinalizedOnHome(ctx, l) // NOTE: Unimplemented handlers: - case contractAbi.Events["MigrationInInitiated"].ID: + case channelHubAbi.Events["MigrationInInitiated"].ID: err = r.handleHomeChannelMigrated(ctx, l) - case contractAbi.Events["MigrationInFinalized"].ID: + case channelHubAbi.Events["MigrationInFinalized"].ID: err = r.handleMigrationInFinalized(ctx, l) - case contractAbi.Events["MigrationOutInitiated"].ID: + case channelHubAbi.Events["MigrationOutInitiated"].ID: err = r.handleMigrationOutInitiated(ctx, l) - case contractAbi.Events["MigrationOutFinalized"].ID: + case channelHubAbi.Events["MigrationOutFinalized"].ID: err = r.handleMigrationOutFinalized(ctx, l) - case contractAbi.Events["Deposited"].ID: + case channelHubAbi.Events["Deposited"].ID: err = r.handleDeposited(ctx, l) - case contractAbi.Events["Withdrawn"].ID: + case channelHubAbi.Events["Withdrawn"].ID: err = r.handleWithdrawn(ctx, l) - case contractAbi.Events["EscrowDepositsPurged"].ID: + case channelHubAbi.Events["EscrowDepositsPurged"].ID: err = r.handleEscrowDepositsPurged(ctx, l) default: - err = errors.New("unknown event: " + eventID.Hex()) + logger.Warn("unknown event: " + eventID.Hex()) } if r.onEventProcessed != nil { r.onEventProcessed(r.blockchainID, err == nil) } if err != nil { logger.Warn("error processing event", "error", err) + return errors.Wrap(err, "error processing event") } if err := r.storeContractEvent(core.BlockchainEvent{ @@ -134,13 +135,15 @@ func (r *Reactor) HandleEvent(ctx context.Context, l types.Log) { LogIndex: uint32(l.Index), }); err != nil { logger.Warn("error storing contract event", "error", err, "event", eventName, "blockNumber", l.BlockNumber, "txHash", l.TxHash.String(), "logIndex", l.Index) + return errors.Wrap(err, "error storing contract event") } logger.Info("processed event", "event", eventName, "blockNumber", l.BlockNumber, "txHash", l.TxHash.String(), "logIndex", l.Index) + return nil } -func (r *Reactor) handleHomeChannelCreated(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseChannelCreated(l) +func (r *ChannelHubReactor) handleHomeChannelCreated(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseChannelCreated(l) if err != nil { return errors.Wrap(err, "failed to parse ChannelCreated event") } @@ -152,8 +155,8 @@ func (r *Reactor) handleHomeChannelCreated(ctx context.Context, l types.Log) err return r.eventHandler.HandleHomeChannelCreated(ctx, &ev) } -func (r *Reactor) handleHomeChannelMigrated(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseMigrationInInitiated(l) +func (r *ChannelHubReactor) handleHomeChannelMigrated(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseMigrationInInitiated(l) if err != nil { return errors.Wrap(err, "failed to parse MigrationInInitiated event") } @@ -165,8 +168,8 @@ func (r *Reactor) handleHomeChannelMigrated(ctx context.Context, l types.Log) er return r.eventHandler.HandleHomeChannelMigrated(ctx, &ev) } -func (r *Reactor) handleHomeChannelCheckpointed(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseChannelCheckpointed(l) +func (r *ChannelHubReactor) handleHomeChannelCheckpointed(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseChannelCheckpointed(l) if err != nil { return errors.Wrap(err, "failed to parse ChannelCheckpointed event") } @@ -178,8 +181,8 @@ func (r *Reactor) handleHomeChannelCheckpointed(ctx context.Context, l types.Log return r.eventHandler.HandleHomeChannelCheckpointed(ctx, &ev) } -func (r *Reactor) handleChannelDeposited(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseChannelDeposited(l) +func (r *ChannelHubReactor) handleChannelDeposited(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseChannelDeposited(l) if err != nil { return errors.Wrap(err, "failed to parse ChannelDeposited event") } @@ -191,8 +194,8 @@ func (r *Reactor) handleChannelDeposited(ctx context.Context, l types.Log) error return r.eventHandler.HandleHomeChannelCheckpointed(ctx, &ev) } -func (r *Reactor) handleChannelWithdrawn(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseChannelWithdrawn(l) +func (r *ChannelHubReactor) handleChannelWithdrawn(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseChannelWithdrawn(l) if err != nil { return errors.Wrap(err, "failed to parse ChannelWithdrawn event") } @@ -204,8 +207,8 @@ func (r *Reactor) handleChannelWithdrawn(ctx context.Context, l types.Log) error return r.eventHandler.HandleHomeChannelCheckpointed(ctx, &ev) } -func (r *Reactor) handleHomeChannelChallenged(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseChannelChallenged(l) +func (r *ChannelHubReactor) handleHomeChannelChallenged(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseChannelChallenged(l) if err != nil { return errors.Wrap(err, "failed to parse ChannelChallenged event") } @@ -218,8 +221,8 @@ func (r *Reactor) handleHomeChannelChallenged(ctx context.Context, l types.Log) return r.eventHandler.HandleHomeChannelChallenged(ctx, &ev) } -func (r *Reactor) handleHomeChannelClosed(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseChannelClosed(l) +func (r *ChannelHubReactor) handleHomeChannelClosed(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseChannelClosed(l) if err != nil { return errors.Wrap(err, "failed to parse ChannelClosed event") } @@ -231,8 +234,8 @@ func (r *Reactor) handleHomeChannelClosed(ctx context.Context, l types.Log) erro return r.eventHandler.HandleHomeChannelClosed(ctx, &ev) } -func (r *Reactor) handleEscrowDepositInitiated(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseEscrowDepositInitiated(l) +func (r *ChannelHubReactor) handleEscrowDepositInitiated(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseEscrowDepositInitiated(l) if err != nil { return errors.Wrap(err, "failed to parse EscrowDepositInitiated event") } @@ -244,8 +247,8 @@ func (r *Reactor) handleEscrowDepositInitiated(ctx context.Context, l types.Log) return r.eventHandler.HandleEscrowDepositInitiated(ctx, &ev) } -func (r *Reactor) handleEscrowDepositChallenged(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseEscrowDepositChallenged(l) +func (r *ChannelHubReactor) handleEscrowDepositChallenged(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseEscrowDepositChallenged(l) if err != nil { return errors.Wrap(err, "failed to parse EscrowDepositChallenged event") } @@ -258,8 +261,8 @@ func (r *Reactor) handleEscrowDepositChallenged(ctx context.Context, l types.Log return r.eventHandler.HandleEscrowDepositChallenged(ctx, &ev) } -func (r *Reactor) handleEscrowDepositFinalized(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseEscrowDepositFinalized(l) +func (r *ChannelHubReactor) handleEscrowDepositFinalized(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseEscrowDepositFinalized(l) if err != nil { return errors.Wrap(err, "failed to parse EscrowDepositFinalized event") } @@ -271,8 +274,8 @@ func (r *Reactor) handleEscrowDepositFinalized(ctx context.Context, l types.Log) return r.eventHandler.HandleEscrowDepositFinalized(ctx, &ev) } -func (r *Reactor) handleEscrowWithdrawalInitiated(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseEscrowWithdrawalInitiated(l) +func (r *ChannelHubReactor) handleEscrowWithdrawalInitiated(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseEscrowWithdrawalInitiated(l) if err != nil { return errors.Wrap(err, "failed to parse EscrowWithdrawalInitiated event") } @@ -284,8 +287,8 @@ func (r *Reactor) handleEscrowWithdrawalInitiated(ctx context.Context, l types.L return r.eventHandler.HandleEscrowWithdrawalInitiated(ctx, &ev) } -func (r *Reactor) handleEscrowWithdrawalChallenged(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseEscrowWithdrawalChallenged(l) +func (r *ChannelHubReactor) handleEscrowWithdrawalChallenged(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseEscrowWithdrawalChallenged(l) if err != nil { return errors.Wrap(err, "failed to parse EscrowWithdrawalChallenged event") } @@ -298,8 +301,8 @@ func (r *Reactor) handleEscrowWithdrawalChallenged(ctx context.Context, l types. return r.eventHandler.HandleEscrowWithdrawalChallenged(ctx, &ev) } -func (r *Reactor) handleEscrowWithdrawalFinalized(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseEscrowWithdrawalFinalized(l) +func (r *ChannelHubReactor) handleEscrowWithdrawalFinalized(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseEscrowWithdrawalFinalized(l) if err != nil { return errors.Wrap(err, "failed to parse EscrowWithdrawalFinalized event") } @@ -311,8 +314,8 @@ func (r *Reactor) handleEscrowWithdrawalFinalized(ctx context.Context, l types.L return r.eventHandler.HandleEscrowWithdrawalFinalized(ctx, &ev) } -func (r *Reactor) handleEscrowDepositInitiatedOnHome(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseEscrowDepositInitiatedOnHome(l) +func (r *ChannelHubReactor) handleEscrowDepositInitiatedOnHome(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseEscrowDepositInitiatedOnHome(l) if err != nil { return errors.Wrap(err, "failed to parse EscrowDepositInitiatedOnHome event") } @@ -324,8 +327,8 @@ func (r *Reactor) handleEscrowDepositInitiatedOnHome(ctx context.Context, l type return r.eventHandler.HandleHomeChannelCheckpointed(ctx, &ev) } -func (r *Reactor) handleEscrowDepositFinalizedOnHome(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseEscrowDepositFinalizedOnHome(l) +func (r *ChannelHubReactor) handleEscrowDepositFinalizedOnHome(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseEscrowDepositFinalizedOnHome(l) if err != nil { return errors.Wrap(err, "failed to parse EscrowDepositFinalizedOnHome event") } @@ -337,8 +340,8 @@ func (r *Reactor) handleEscrowDepositFinalizedOnHome(ctx context.Context, l type return r.eventHandler.HandleHomeChannelCheckpointed(ctx, &ev) } -func (r *Reactor) handleEscrowWithdrawalInitiatedOnHome(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseEscrowWithdrawalInitiatedOnHome(l) +func (r *ChannelHubReactor) handleEscrowWithdrawalInitiatedOnHome(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseEscrowWithdrawalInitiatedOnHome(l) if err != nil { return errors.Wrap(err, "failed to parse EscrowWithdrawalInitiatedOnHome event") } @@ -350,8 +353,8 @@ func (r *Reactor) handleEscrowWithdrawalInitiatedOnHome(ctx context.Context, l t return r.eventHandler.HandleHomeChannelCheckpointed(ctx, &ev) } -func (r *Reactor) handleEscrowWithdrawalFinalizedOnHome(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseEscrowWithdrawalFinalizedOnHome(l) +func (r *ChannelHubReactor) handleEscrowWithdrawalFinalizedOnHome(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseEscrowWithdrawalFinalizedOnHome(l) if err != nil { return errors.Wrap(err, "failed to parse EscrowWithdrawalFinalizedOnHome event") } @@ -365,8 +368,8 @@ func (r *Reactor) handleEscrowWithdrawalFinalizedOnHome(ctx context.Context, l t // Additional event handlers for events not yet defined in core.BlockchainEventHandler -func (r *Reactor) handleMigrationInFinalized(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseMigrationInFinalized(l) +func (r *ChannelHubReactor) handleMigrationInFinalized(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseMigrationInFinalized(l) if err != nil { return errors.Wrap(err, "failed to parse MigrationInFinalized event") } @@ -378,8 +381,8 @@ func (r *Reactor) handleMigrationInFinalized(ctx context.Context, l types.Log) e return nil } -func (r *Reactor) handleMigrationOutInitiated(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseMigrationOutInitiated(l) +func (r *ChannelHubReactor) handleMigrationOutInitiated(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseMigrationOutInitiated(l) if err != nil { return errors.Wrap(err, "failed to parse MigrationOutInitiated event") } @@ -391,8 +394,8 @@ func (r *Reactor) handleMigrationOutInitiated(ctx context.Context, l types.Log) return nil } -func (r *Reactor) handleMigrationOutFinalized(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseMigrationOutFinalized(l) +func (r *ChannelHubReactor) handleMigrationOutFinalized(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseMigrationOutFinalized(l) if err != nil { return errors.Wrap(err, "failed to parse MigrationOutFinalized event") } @@ -404,8 +407,8 @@ func (r *Reactor) handleMigrationOutFinalized(ctx context.Context, l types.Log) return nil } -func (r *Reactor) handleDeposited(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseDeposited(l) +func (r *ChannelHubReactor) handleDeposited(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseDeposited(l) if err != nil { return errors.Wrap(err, "failed to parse Deposited event") } @@ -418,8 +421,8 @@ func (r *Reactor) handleDeposited(ctx context.Context, l types.Log) error { return nil } -func (r *Reactor) handleWithdrawn(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseWithdrawn(l) +func (r *ChannelHubReactor) handleWithdrawn(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseWithdrawn(l) if err != nil { return errors.Wrap(err, "failed to parse Withdrawn event") } @@ -432,8 +435,8 @@ func (r *Reactor) handleWithdrawn(ctx context.Context, l types.Log) error { return nil } -func (r *Reactor) handleEscrowDepositsPurged(ctx context.Context, l types.Log) error { - event, err := contractFilterer.ParseEscrowDepositsPurged(l) +func (r *ChannelHubReactor) handleEscrowDepositsPurged(ctx context.Context, l types.Log) error { + event, err := channelHubFilterer.ParseEscrowDepositsPurged(l) if err != nil { return errors.Wrap(err, "failed to parse EscrowDepositsPurged event") } diff --git a/pkg/blockchain/evm/client_opts.go b/pkg/blockchain/evm/client_opts.go index 3aa4bc832..fe9368721 100644 --- a/pkg/blockchain/evm/client_opts.go +++ b/pkg/blockchain/evm/client_opts.go @@ -11,7 +11,7 @@ type ClientAllowanceCheck struct { RequireAllowanceCheck bool } -func (ch ClientAllowanceCheck) apply(c *Client) { +func (ch ClientAllowanceCheck) apply(c *BlockchainClient) { c.requireCheckAllowance = ch.RequireAllowanceCheck } @@ -19,7 +19,7 @@ type ClientBalanceCheck struct { RequireBalanceCheck bool } -func (ch ClientBalanceCheck) apply(c *Client) { +func (ch ClientBalanceCheck) apply(c *BlockchainClient) { c.requireCheckBalance = ch.RequireBalanceCheck } @@ -27,7 +27,7 @@ type ClientFeeCheck struct { RequirePositiveNativeBalance bool } -func (ch ClientFeeCheck) apply(c *Client) { +func (ch ClientFeeCheck) apply(c *BlockchainClient) { if !ch.RequirePositiveNativeBalance { c.checkFeeFn = func(ctx context.Context, account common.Address) error { return nil diff --git a/pkg/blockchain/evm/erc20.go b/pkg/blockchain/evm/erc20_abi.go similarity index 100% rename from pkg/blockchain/evm/erc20.go rename to pkg/blockchain/evm/erc20_abi.go diff --git a/pkg/blockchain/evm/init.go b/pkg/blockchain/evm/init.go new file mode 100644 index 000000000..a042d718b --- /dev/null +++ b/pkg/blockchain/evm/init.go @@ -0,0 +1,6 @@ +package evm + +func init() { + initChannelHub() + initLockingContract() +} diff --git a/pkg/blockchain/evm/interface.go b/pkg/blockchain/evm/interface.go index 662e00ba2..9a28ccd97 100644 --- a/pkg/blockchain/evm/interface.go +++ b/pkg/blockchain/evm/interface.go @@ -9,7 +9,7 @@ import ( "github.com/layer-3/nitrolite/pkg/core" ) -type HandleEvent func(ctx context.Context, eventLog types.Log) +type HandleEvent func(ctx context.Context, eventLog types.Log) error type StoreContractEvent func(ev core.BlockchainEvent) error type LatestEventGetter func(contractAddress string, blockchainID uint64) (ev core.BlockchainEvent, err error) diff --git a/pkg/blockchain/evm/listener.go b/pkg/blockchain/evm/listener.go index b3a4853bf..1fcf9efc2 100644 --- a/pkg/blockchain/evm/listener.go +++ b/pkg/blockchain/evm/listener.go @@ -72,8 +72,7 @@ func (l *Listener) Listen(ctx context.Context, handleClosure func(err error)) { } go func() { - defer childHandleClosure(nil) - l.listenEvents(childCtx) + childHandleClosure(l.listenEvents(childCtx)) }() go func() { @@ -87,11 +86,10 @@ func (l *Listener) Listen(ctx context.Context, handleClosure func(err error)) { } // listenEvents listens for blockchain events and processes them with the provided handler -func (l *Listener) listenEvents(ctx context.Context) { +func (l *Listener) listenEvents(ctx context.Context) error { ev, err := l.getLatestEvent(l.contractAddress.String(), l.blockchainID) if err != nil { l.logger.Error("failed to get latest processed event", "error", err, "blockchainID", l.blockchainID, "contractAddress", l.contractAddress.String()) - return } lastBlock := ev.BlockNumber lastIndex := ev.LogIndex @@ -151,18 +149,23 @@ func (l *Listener) listenEvents(ctx context.Context) { select { case <-ctx.Done(): l.logger.Info("stopping event listener", "blockchainID", l.blockchainID, "contractAddress", l.contractAddress.String()) - return + eventSubscription.Unsubscribe() + return nil case eventLog := <-historicalCh: l.logger.Debug("received new event", "blockchainID", l.blockchainID, "contractAddress", l.contractAddress.String(), "blockNumber", lastBlock, "logIndex", eventLog.Index) ctx := log.SetContextLogger(context.Background(), l.logger) - l.handleEvent(ctx, eventLog) + if err := l.handleEvent(ctx, eventLog); err != nil { + return err + } case eventLog := <-currentCh: lastBlock = eventLog.BlockNumber l.logger.Debug("received new event", "blockchainID", l.blockchainID, "contractAddress", l.contractAddress.String(), "blockNumber", lastBlock, "logIndex", eventLog.Index) ctx := log.SetContextLogger(context.Background(), l.logger) - l.handleEvent(ctx, eventLog) + if err := l.handleEvent(ctx, eventLog); err != nil { + return err + } case err := <-eventSubscription.Err(): if err != nil { l.logger.Error("event subscription error", "error", err, "blockchainID", l.blockchainID, "contractAddress", l.contractAddress.String()) diff --git a/pkg/blockchain/evm/listener_test.go b/pkg/blockchain/evm/listener_test.go index f0e339a99..3e787b254 100644 --- a/pkg/blockchain/evm/listener_test.go +++ b/pkg/blockchain/evm/listener_test.go @@ -64,8 +64,9 @@ func TestListener_Listen_CurrentEvents(t *testing.T) { // Channel to signal event handling eventHandled := make(chan struct{}) - handleEvent := func(ctx context.Context, log types.Log) { + handleEvent := func(ctx context.Context, log types.Log) error { close(eventHandled) + return nil } listener := NewListener(addr, mockClient, 1, 100, logger, handleEvent, getLatestEvent) @@ -157,7 +158,7 @@ func TestListener_Listen_HistoricalAndCurrent(t *testing.T) { var receivedCount int64 doneCh := make(chan struct{}) - handleEvent := func(ctx context.Context, log types.Log) { + handleEvent := func(ctx context.Context, log types.Log) error { count := atomic.AddInt64(&receivedCount, 1) if count >= 2 { // Expect 1 historical + 1 current select { @@ -166,6 +167,8 @@ func TestListener_Listen_HistoricalAndCurrent(t *testing.T) { close(doneCh) } } + + return nil } listener := NewListener(addr, mockClient, 1, 10, logger, handleEvent, getLatestEvent) diff --git a/pkg/blockchain/evm/locking_client.go b/pkg/blockchain/evm/locking_client.go new file mode 100644 index 000000000..56a49dcd0 --- /dev/null +++ b/pkg/blockchain/evm/locking_client.go @@ -0,0 +1,232 @@ +package evm + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + "github.com/shopspring/decimal" + + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/sign" +) + +// LockingClient provides access to a Locking contract. +type LockingClient struct { + BaseClient + lockingContractAddress common.Address + transactOpts *bind.TransactOpts + + tokenAddress common.Address + tokenDecimals uint8 +} + +// NewLockingClient creates a new LockingClient. +// If txSigner is provided, the client can perform write operations (lock, relock, unlock, withdraw). +func NewLockingClient(lockingContractAddress common.Address, evmClient EVMClient, blockchainID uint64, txSigner ...sign.Signer) (*LockingClient, error) { + c := &LockingClient{ + BaseClient: BaseClient{ + evmClient: evmClient, + blockchainID: blockchainID, + }, + lockingContractAddress: lockingContractAddress, + } + if len(txSigner) > 0 && txSigner[0] != nil { + c.transactOpts = signerTxOpts(txSigner[0], blockchainID) + } + + lockingContract, err := NewAppRegistry(c.lockingContractAddress, c.evmClient) + if err != nil { + return nil, errors.Wrap(err, "failed to instantiate Locking contract") + } + + tokenAddress, err := lockingContract.Asset(nil) + if err != nil { + return nil, errors.Wrap(err, "failed to get asset from the Locking contract") + } + + erc20Contract, err := NewIERC20(tokenAddress, c.evmClient) + if err != nil { + return nil, errors.Wrap(err, "failed to instantiate ERC20 contract") + } + + decimals, err := erc20Contract.Decimals(nil) + if err != nil { + return nil, errors.Wrapf(err, "failed to get decimals for token %s", tokenAddress.Hex()) + } + + c.tokenAddress = tokenAddress + c.tokenDecimals = decimals + + return c, nil +} + +// GetTokenDecimals returns the number of decimals for the token used in the AppRegistry. +// This is needed to convert between human-readable amounts and the raw integer amounts used in transactions. +func (c *LockingClient) GetTokenDecimals() (uint8, error) { + lockingContract, err := NewAppRegistry(c.lockingContractAddress, c.evmClient) + if err != nil { + return 0, errors.Wrap(err, "failed to instantiate Locking contract") + } + + tokenAddress, err := lockingContract.Asset(&bind.CallOpts{}) + if err != nil { + return 0, errors.Wrap(err, "failed to get asset from the Locking contract") + } + + erc20Contract, err := NewIERC20(tokenAddress, c.evmClient) + if err != nil { + return 0, errors.Wrap(err, "failed to instantiate ERC20 contract") + } + + decimals, err := erc20Contract.Decimals(&bind.CallOpts{}) + if err != nil { + return 0, errors.Wrapf(err, "failed to get decimals for token %s", tokenAddress.Hex()) + } + + return decimals, nil +} + +// Lock locks tokens into the Locking contract for the specified target address. +// The caller must have approved the Locking contract to spend the token beforehand. +func (c *LockingClient) Lock(targetWalletAddress string, amount decimal.Decimal) (string, error) { + if !common.IsHexAddress(targetWalletAddress) { + return "", errors.Errorf("invalid address %q", targetWalletAddress) + } + + targetAddr := common.HexToAddress(targetWalletAddress) + if c.transactOpts == nil { + return "", errors.New("transaction signer not configured") + } + + amountBig, err := core.DecimalToBigInt(amount, c.tokenDecimals) + if err != nil { + return "", errors.Wrap(err, "failed to convert amount with decimal precision") + } + + lockingContract, err := NewAppRegistry(c.lockingContractAddress, c.evmClient) + if err != nil { + return "", errors.Wrap(err, "failed to instantiate Locking contract") + } + + tx, err := lockingContract.Lock(c.transactOpts, targetAddr, amountBig) + if err != nil { + return "", errors.Wrap(err, "failed to send lock transaction") + } + return tx.Hash().Hex(), nil +} + +// Relock re-locks tokens that are in the unlocking state back to the locked state. +func (c *LockingClient) Relock() (string, error) { + if c.transactOpts == nil { + return "", errors.New("transaction signer not configured") + } + + lockingContract, err := NewAppRegistry(c.lockingContractAddress, c.evmClient) + if err != nil { + return "", errors.Wrap(err, "failed to instantiate Locking contract") + } + + tx, err := lockingContract.Relock(c.transactOpts) + if err != nil { + return "", errors.Wrap(err, "failed to send relock transaction") + } + return tx.Hash().Hex(), nil +} + +// Unlock initiates the unlock process for the caller's locked tokens. +// After the unlock period elapses, Withdraw can be called. +func (c *LockingClient) Unlock() (string, error) { + if c.transactOpts == nil { + return "", errors.New("transaction signer not configured") + } + + lockingContract, err := NewAppRegistry(c.lockingContractAddress, c.evmClient) + if err != nil { + return "", errors.Wrap(err, "failed to instantiate Locking contract") + } + + tx, err := lockingContract.Unlock(c.transactOpts) + if err != nil { + return "", errors.Wrap(err, "failed to send unlock transaction") + } + return tx.Hash().Hex(), nil +} + +// Withdraw withdraws unlocked tokens to the specified destination address. +// Can only be called after the unlock period has elapsed. +func (c *LockingClient) Withdraw(destination string) (string, error) { + if c.transactOpts == nil { + return "", errors.New("transaction signer not configured") + } + + destinationAddr := common.HexToAddress(destination) + + lockingContract, err := NewAppRegistry(c.lockingContractAddress, c.evmClient) + if err != nil { + return "", errors.Wrap(err, "failed to instantiate Locking contract") + } + + tx, err := lockingContract.Withdraw(c.transactOpts, destinationAddr) + if err != nil { + return "", errors.Wrap(err, "failed to send withdraw transaction") + } + return tx.Hash().Hex(), nil +} + +// ApproveToken approves the Locking contract to spend the specified amount of tokens. +// This must be called before Lock. +func (c *LockingClient) ApproveToken(amount decimal.Decimal) (string, error) { + if c.transactOpts == nil { + return "", errors.New("transaction signer not configured") + } + + amountBig, err := core.DecimalToBigInt(amount, c.tokenDecimals) + if err != nil { + return "", errors.Wrap(err, "failed to convert amount with decimal precision") + } + + lockingContract, err := NewAppRegistry(c.lockingContractAddress, c.evmClient) + if err != nil { + return "", errors.Wrap(err, "failed to instantiate Locking contract") + } + + tokenAddress, err := lockingContract.Asset(&bind.CallOpts{}) + if err != nil { + return "", errors.Wrap(err, "failed to get asset from Locking contract") + } + + erc20Contract, err := NewIERC20(tokenAddress, c.evmClient) + if err != nil { + return "", errors.Wrap(err, "failed to instantiate ERC20 contract") + } + + tx, err := erc20Contract.Approve(c.transactOpts, c.lockingContractAddress, amountBig) + if err != nil { + return "", errors.Wrap(err, "failed to send approve transaction") + } + return tx.Hash().Hex(), nil +} + +// GetBalance returns the locked balance of a user in the Locking contraсt. +func (c *LockingClient) GetBalance(user string) (decimal.Decimal, error) { + userAddr := common.HexToAddress(user) + lockingContract, err := NewAppRegistry(c.lockingContractAddress, c.evmClient) + if err != nil { + return decimal.Zero, errors.Wrap(err, "failed to instantiate Locking contract") + } + + balance, err := lockingContract.BalanceOf(nil, userAddr) + if err != nil { + return decimal.Zero, errors.Wrap(err, "failed to get balance") + } + + return decimal.NewFromBigInt(balance, -int32(c.tokenDecimals)), nil +} + +// decimalToBigInt converts a decimal amount to *big.Int given token decimals. +func decimalToBigInt(amount decimal.Decimal, decimals int32) *big.Int { + shifted := amount.Shift(decimals) + return shifted.BigInt() +} diff --git a/pkg/blockchain/evm/locking_reactor.go b/pkg/blockchain/evm/locking_reactor.go new file mode 100644 index 000000000..3493cd3f5 --- /dev/null +++ b/pkg/blockchain/evm/locking_reactor.go @@ -0,0 +1,164 @@ +package evm + +import ( + "context" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/pkg/errors" + "github.com/shopspring/decimal" + + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/log" +) + +var lockingContractAbi *abi.ABI +var lockingContractFilterer *AppRegistryFilterer +var eventMapping map[common.Hash]string + +func initLockingContract() { + var err error + lockingContractAbi, err = AppRegistryMetaData.GetAbi() + if err != nil { + panic(err) + } + + // Create a filterer for parsing events (address not needed for parsing) + contract := bind.NewBoundContract(common.Address{}, *lockingContractAbi, nil, nil, nil) + lockingContractFilterer = &AppRegistryFilterer{contract: contract} + + eventMapping = make(map[common.Hash]string) + for name, event := range lockingContractAbi.Events { + eventMapping[event.ID] = name + } +} + +type LockingContractReactor struct { + blockchainID uint64 + eventHandler core.LockingContractEventHandler + storeContractEvent StoreContractEvent + tokenDecimals int32 + onEventProcessed func(blockchainID uint64, success bool) +} + +func NewLockingContractReactor(blockchainID uint64, eventHandler core.LockingContractEventHandler, getTokenDecimals func() (uint8, error), storeContractEvent StoreContractEvent) (*LockingContractReactor, error) { + tokenDecimals, err := getTokenDecimals() + if err != nil { + return nil, errors.Wrap(err, "failed to get token decimals") + } + return &LockingContractReactor{ + blockchainID: blockchainID, + eventHandler: eventHandler, + tokenDecimals: int32(tokenDecimals), + storeContractEvent: storeContractEvent, + }, nil +} + +// SetOnEventProcessed sets an optional callback invoked after each event is processed. +func (r *LockingContractReactor) SetOnEventProcessed(fn func(blockchainID uint64, success bool)) { + r.onEventProcessed = fn +} + +func (r *LockingContractReactor) HandleEvent(ctx context.Context, l types.Log) error { + logger := log.FromContext(ctx) + + eventID := l.Topics[0] + eventName, ok := eventMapping[eventID] + if !ok { + logger.Warn("unknown event ID", "eventID", eventID.Hex(), "blockNumber", l.BlockNumber, "txHash", l.TxHash.String(), "logIndex", l.Index) + return nil + } + logger.Debug("received event", "name", eventName, "blockNumber", l.BlockNumber, "txHash", l.TxHash.String(), "logIndex", l.Index) + + var err error + switch eventID { + case lockingContractAbi.Events["Locked"].ID: + err = r.handleLocked(ctx, l) + case lockingContractAbi.Events["Relocked"].ID: + err = r.handleRelocked(ctx, l) + case lockingContractAbi.Events["UnlockInitiated"].ID: + err = r.handleUnlockInitiated(ctx, l) + case lockingContractAbi.Events["Withdrawn"].ID: + err = r.handleWithdrawn(ctx, l) + default: + logger.Warn("unknown event: " + eventID.Hex()) + } + if r.onEventProcessed != nil { + r.onEventProcessed(r.blockchainID, err == nil) + } + if err != nil { + logger.Warn("error processing event", "error", err) + return errors.Wrap(err, "error processing event") + } + + if err := r.storeContractEvent(core.BlockchainEvent{ + BlockNumber: l.BlockNumber, + BlockchainID: r.blockchainID, + Name: eventName, + ContractAddress: l.Address.Hex(), + TransactionHash: l.TxHash.String(), + LogIndex: uint32(l.Index), + }); err != nil { + logger.Warn("error storing contract event", "error", err, "event", eventName, "blockNumber", l.BlockNumber, "txHash", l.TxHash.String(), "logIndex", l.Index) + return errors.Wrap(err, "error storing contract event") + } + + logger.Info("processed event", "event", eventName, "blockNumber", l.BlockNumber, "txHash", l.TxHash.String(), "logIndex", l.Index) + return nil +} + +func (r *LockingContractReactor) handleLocked(ctx context.Context, l types.Log) error { + event, err := lockingContractFilterer.ParseLocked(l) + if err != nil { + return errors.Wrap(err, "failed to parse Locked event") + } + + ev := core.UserLockedBalanceUpdatedEvent{ + UserAddress: strings.ToLower(event.User.String()), + BlockchainID: r.blockchainID, + Balance: decimal.NewFromBigInt(event.NewBalance, -r.tokenDecimals), + } + return r.eventHandler.HandleUserLockedBalanceUpdated(ctx, &ev) +} + +func (r *LockingContractReactor) handleRelocked(ctx context.Context, l types.Log) error { + event, err := lockingContractFilterer.ParseRelocked(l) + if err != nil { + return errors.Wrap(err, "failed to parse Relocked event") + } + ev := core.UserLockedBalanceUpdatedEvent{ + UserAddress: strings.ToLower(event.User.String()), + BlockchainID: r.blockchainID, + Balance: decimal.NewFromBigInt(event.Balance, -r.tokenDecimals), + } + return r.eventHandler.HandleUserLockedBalanceUpdated(ctx, &ev) +} + +func (r *LockingContractReactor) handleUnlockInitiated(ctx context.Context, l types.Log) error { + event, err := lockingContractFilterer.ParseUnlockInitiated(l) + if err != nil { + return errors.Wrap(err, "failed to parse Unlockinitiated event") + } + ev := core.UserLockedBalanceUpdatedEvent{ + UserAddress: strings.ToLower(event.User.String()), + BlockchainID: r.blockchainID, + Balance: decimal.Zero, + } + return r.eventHandler.HandleUserLockedBalanceUpdated(ctx, &ev) +} + +func (r *LockingContractReactor) handleWithdrawn(ctx context.Context, l types.Log) error { + event, err := lockingContractFilterer.ParseWithdrawn(l) + if err != nil { + return errors.Wrap(err, "failed to parse Withdrawn event") + } + ev := core.UserLockedBalanceUpdatedEvent{ + UserAddress: strings.ToLower(event.User.String()), + BlockchainID: r.blockchainID, + Balance: decimal.Zero, + } + return r.eventHandler.HandleUserLockedBalanceUpdated(ctx, &ev) +} diff --git a/pkg/blockchain/evm/locking_reactor_test.go b/pkg/blockchain/evm/locking_reactor_test.go new file mode 100644 index 000000000..a03389e0f --- /dev/null +++ b/pkg/blockchain/evm/locking_reactor_test.go @@ -0,0 +1,127 @@ +package evm + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/layer-3/nitrolite/pkg/core" +) + +func TestAppRegistryReactor_HandleLocked(t *testing.T) { + userAddr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + deposited := big.NewInt(500_000_000) // 500 USDC (6 decimals) + newBalance := big.NewInt(1_000_000_000) // 1000 USDC + var tokenDecimals int32 = 6 + blockchainID := uint64(1) + + // ABI-encode the non-indexed parameters: deposited, newBalance + depositedPadded := common.LeftPadBytes(deposited.Bytes(), 32) + newBalancePadded := common.LeftPadBytes(newBalance.Bytes(), 32) + data := append(depositedPadded, newBalancePadded...) + + lockedEventID := lockingContractAbi.Events["Locked"].ID + logEntry := types.Log{ + Topics: []common.Hash{ + lockedEventID, + common.BytesToHash(userAddr.Bytes()), // indexed user + }, + Data: data, + BlockNumber: 100, + TxHash: common.HexToHash("0xdeadbeef"), + Index: 0, + } + + t.Run("success", func(t *testing.T) { + var capturedEvent *core.UserLockedBalanceUpdatedEvent + handler := &mockAppRegistryEventHandler{ + handleFn: func(_ context.Context, ev *core.UserLockedBalanceUpdatedEvent) error { + capturedEvent = ev + return nil + }, + } + + var storedEvent core.BlockchainEvent + storeContractEvent := func(ev core.BlockchainEvent) error { + storedEvent = ev + return nil + } + + reactor, err := NewLockingContractReactor(blockchainID, handler, func() (uint8, error) { + return uint8(tokenDecimals), nil + }, storeContractEvent) + require.NoError(t, err) + + var processedSuccess bool + reactor.SetOnEventProcessed(func(_ uint64, success bool) { + processedSuccess = success + }) + + reactor.HandleEvent(context.Background(), logEntry) + + require.NotNil(t, capturedEvent) + assert.Equal(t, userAddr.String(), common.HexToAddress(capturedEvent.UserAddress).String()) + assert.Equal(t, blockchainID, capturedEvent.BlockchainID) + expectedBalance := decimal.NewFromBigInt(newBalance, -tokenDecimals) + assert.True(t, expectedBalance.Equal(capturedEvent.Balance), "expected %s, got %s", expectedBalance, capturedEvent.Balance) + + assert.True(t, processedSuccess) + assert.Equal(t, "Locked", storedEvent.Name) + assert.Equal(t, uint64(100), storedEvent.BlockNumber) + assert.Equal(t, blockchainID, storedEvent.BlockchainID) + }) + + t.Run("getTokenDecimals error", func(t *testing.T) { + handler := &mockAppRegistryEventHandler{ + handleFn: func(_ context.Context, _ *core.UserLockedBalanceUpdatedEvent) error { + t.Fatal("handler should not be called") + return nil + }, + } + + storeContractEvent := func(_ core.BlockchainEvent) error { return nil } + + _, err := NewLockingContractReactor(blockchainID, handler, func() (uint8, error) { + return 0, assert.AnError + }, storeContractEvent) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to get token decimals") + }) + + t.Run("handler error", func(t *testing.T) { + handler := &mockAppRegistryEventHandler{ + handleFn: func(_ context.Context, _ *core.UserLockedBalanceUpdatedEvent) error { + return assert.AnError + }, + } + + storeContractEvent := func(_ core.BlockchainEvent) error { return nil } + + reactor, err := NewLockingContractReactor(blockchainID, handler, func() (uint8, error) { + return uint8(tokenDecimals), nil + }, storeContractEvent) + require.NoError(t, err) + + var processedSuccess bool + reactor.SetOnEventProcessed(func(_ uint64, success bool) { + processedSuccess = success + }) + + reactor.HandleEvent(context.Background(), logEntry) + assert.False(t, processedSuccess) + }) +} + +type mockAppRegistryEventHandler struct { + handleFn func(context.Context, *core.UserLockedBalanceUpdatedEvent) error +} + +func (m *mockAppRegistryEventHandler) HandleUserLockedBalanceUpdated(ctx context.Context, ev *core.UserLockedBalanceUpdatedEvent) error { + return m.handleFn(ctx, ev) +} diff --git a/pkg/core/event.go b/pkg/core/event.go index 07d8ec4dd..4d2c0bcdb 100644 --- a/pkg/core/event.go +++ b/pkg/core/event.go @@ -1,5 +1,7 @@ package core +import "github.com/shopspring/decimal" + // On-chain events // HomeChannelCreatedEvent represents the ChannelCreated event @@ -46,6 +48,12 @@ type channelChallengedEvent struct { ChallengeExpiry uint64 `json:"challenge_expiry"` } +type UserLockedBalanceUpdatedEvent struct { + UserAddress string `json:"user_address"` + BlockchainID uint64 `json:"blockchain_id"` + Balance decimal.Decimal `json:"balance"` +} + type BlockchainEvent struct { ContractAddress string `json:"contract_address"` BlockchainID uint64 `json:"blockchain_id"` diff --git a/pkg/core/interface.go b/pkg/core/interface.go index 4fabe848f..67c9b78e3 100644 --- a/pkg/core/interface.go +++ b/pkg/core/interface.go @@ -10,7 +10,7 @@ import ( // Client defines the interface for interacting with the ChannelsHub smart contract // TODO: add context to all methods -type Client interface { +type BlockchainClient interface { // Getters - IVault GetAccountsBalances(accounts []string, tokens []string) ([][]decimal.Decimal, error) @@ -29,6 +29,9 @@ type Client interface { Deposit(node, token string, amount decimal.Decimal) (string, error) Withdraw(node, token string, amount decimal.Decimal) (string, error) + // Node lifecycle + EnsureSigValidatorRegistered(validatorID uint8, validatorAddress string, checkOnly bool) error + // Channel lifecycle Create(def ChannelDefinition, initCCS State) (string, error) MigrateChannelHere(def ChannelDefinition, candidate State) (string, error) @@ -47,6 +50,19 @@ type Client interface { FinalizeEscrowWithdrawal(candidate State) (string, error) } +// ========= AppRegistryClient Interface ========= + +type AppRegistryClient interface { + ApproveToken(amount decimal.Decimal) (string, error) + GetBalance(user string) (decimal.Decimal, error) + GetTokenDecimals() (uint8, error) + + Lock(targetWallet string, amount decimal.Decimal) (string, error) + Relock() (string, error) + Unlock() (string, error) + Withdraw(destinationWallet string) (string, error) +} + // ========= TransitionValidator Interface ========= // StateAdvancer applies state transitions @@ -72,7 +88,7 @@ type AssetStore interface { } // Channel lifecycle event handlers -type BlockchainEventHandler interface { +type ChannelHubEventHandler interface { HandleHomeChannelCreated(context.Context, *HomeChannelCreatedEvent) error HandleHomeChannelMigrated(context.Context, *HomeChannelMigratedEvent) error HandleHomeChannelCheckpointed(context.Context, *HomeChannelCheckpointedEvent) error @@ -85,3 +101,7 @@ type BlockchainEventHandler interface { HandleEscrowWithdrawalChallenged(context.Context, *EscrowWithdrawalChallengedEvent) error HandleEscrowWithdrawalFinalized(context.Context, *EscrowWithdrawalFinalizedEvent) error } + +type LockingContractEventHandler interface { + HandleUserLockedBalanceUpdated(context.Context, *UserLockedBalanceUpdatedEvent) error +} diff --git a/pkg/core/types.go b/pkg/core/types.go index eb82f0ce1..b1e815417 100644 --- a/pkg/core/types.go +++ b/pkg/core/types.go @@ -931,10 +931,11 @@ func (t1 Transition) Equal(t2 Transition) error { // Blockchain represents information about a supported blockchain network type Blockchain struct { - Name string `json:"name"` // Blockchain name - ID uint64 `json:"id"` // Blockchain network ID - ChannelHubAddress string `json:"channel_hub_address"` // Address of the ChannelHub contract on this blockchain - BlockStep uint64 `json:"block_step"` // Number of blocks between each channel update + Name string `json:"name"` // Blockchain name + ID uint64 `json:"id"` // Blockchain network ID + ChannelHubAddress string `json:"channel_hub_address"` // Address of the ChannelHub contract on this blockchain + LockingContractAddress string `json:"locking_contract_address"` // Address of the Locking contract on this blockchain + BlockStep uint64 `json:"block_step"` // Number of blocks between each channel update } // Asset represents information about a supported asset diff --git a/pkg/rpc/types.go b/pkg/rpc/types.go index 3e054584e..31826af3f 100644 --- a/pkg/rpc/types.go +++ b/pkg/rpc/types.go @@ -283,7 +283,8 @@ type BlockchainInfoV1 struct { BlockchainID string `json:"blockchain_id"` // ChannelHubAddress is the contract address on this network ChannelHubAddress string `json:"channel_hub_address"` - // DefaultValidatorAddress is the validator contract address set in a channel supported by clearnode + // LockingContractAddress is the contract address for the locking contract on this network + LockingContractAddress string `json:"locking_contract_address"` } // ============================================================================ diff --git a/sdk/go/README.md b/sdk/go/README.md index 27421c63b..b35161500 100644 --- a/sdk/go/README.md +++ b/sdk/go/README.md @@ -58,6 +58,7 @@ client.RegisterApp(ctx, appID, metadata, approvalNotRequired) // Register new ap client.GetAppSessions(ctx, opts) // List sessions client.GetAppDefinition(ctx, appSessionID) // Session definition client.CreateAppSession(ctx, definition, sessionData, sigs) // Create session +client.CreateAppSession(ctx, def, data, sigs, opts) // Create with owner approval client.SubmitAppSessionDeposit(ctx, update, sigs, asset, amount) // Deposit to session client.SubmitAppState(ctx, update, sigs) // Update session client.RebalanceAppSessions(ctx, signedUpdates) // Atomic rebalance @@ -382,6 +383,37 @@ err := client.SubmitAppState(ctx, update, sigs) batchID, err := client.RebalanceAppSessions(ctx, signedUpdates) ``` +#### Owner Approval for App Session Creation + +When an app is registered with `creationApprovalNotRequired: false`, the app owner must sign the session creation request. Pass the owner's signature via `CreateAppSessionOptions`: + +```go +// Owner signs the create request using their app session signer +ownerSig, _ := ownerAppSessionSigner.Sign(createRequest) + +sessionID, _, _, err := client.CreateAppSession(ctx, def, data, sigs, + sdk.CreateAppSessionOptions{OwnerSig: ownerSig.String()}, +) +``` + +### App Session Signers (`pkg/app`) + +App session operations require signatures with a type byte prefix, similar to channel signers: + +| Type | Byte | Constructor | Usage | +|------|------|------------|-------| +| Wallet | `0xA1` | `app.NewAppSessionWalletSignerV1(msgSigner)` | Main wallet signs app session operations | +| Session Key | `0xA2` | `app.NewAppSessionKeySignerV1(msgSigner)` | Delegated session key signs on behalf of wallet | + +```go +// Create app session wallet signer +msgSigner, _ := sign.NewEthereumMsgSigner(privateKeyHex) +appSessionSigner, _ := app.NewAppSessionWalletSignerV1(msgSigner) + +// Sign app session operations (create, deposit, state updates, etc.) +sig, _ := appSessionSigner.Sign(packedRequest) +``` + ### Session Keys — App Sessions ```go @@ -578,12 +610,15 @@ go run lifecycle.go ``` This example demonstrates: -- Creating app sessions with multiple participants +- Registering apps in the app registry (with and without owner approval) +- Creating app sessions with single and multiple participants +- Owner approval for app session creation +- Session key delegation for app session participants - Depositing assets into app sessions - Operating on app session state (redistributing allocations) -- Atomic rebalancing across multiple app sessions - Withdrawing from app sessions - Closing app sessions +- Fail case: attempting to create a session for an unregistered app The example walks through a complete multi-party app session scenario with three wallets. diff --git a/sdk/go/app_session.go b/sdk/go/app_session.go index e4f56e108..e3627cb49 100644 --- a/sdk/go/app_session.go +++ b/sdk/go/app_session.go @@ -99,12 +99,20 @@ func (c *Client) GetAppDefinition(ctx context.Context, appSessionID string) (*ap return &def, nil } +// CreateAppSessionOptions contains optional parameters for CreateAppSession. +type CreateAppSessionOptions struct { + // OwnerSig is the app owner's signature approving session creation. + // Required when the app's CreationApprovalNotRequired is false. + OwnerSig string +} + // CreateAppSession creates a new application session between participants. // // Parameters: // - definition: The app definition with participants, quorum, application ID // - sessionData: Optional JSON stringified session data // - quorumSigs: Participant signatures for the app session creation +// - opts: Optional parameters (owner signature for apps requiring approval) // // Returns: // - AppSessionID of the created session @@ -121,12 +129,15 @@ func (c *Client) GetAppDefinition(ctx context.Context, appSessionID string) (*ap // Nonce: 1, // } // sessionID, version, status, err := client.CreateAppSession(ctx, def, "{}", []string{"sig1", "sig2"}) -func (c *Client) CreateAppSession(ctx context.Context, definition app.AppDefinitionV1, sessionData string, quorumSigs []string) (string, string, string, error) { +func (c *Client) CreateAppSession(ctx context.Context, definition app.AppDefinitionV1, sessionData string, quorumSigs []string, opts ...CreateAppSessionOptions) (string, string, string, error) { req := rpc.AppSessionsV1CreateAppSessionRequest{ Definition: transformAppDefinitionToRPC(definition), SessionData: sessionData, QuorumSigs: quorumSigs, } + if len(opts) > 0 && opts[0].OwnerSig != "" { + req.OwnerSig = opts[0].OwnerSig + } resp, err := c.rpcClient.AppSessionsV1CreateAppSession(ctx, req) if err != nil { return "", "", "", fmt.Errorf("failed to create app session: %w", err) diff --git a/sdk/go/channel.go b/sdk/go/channel.go index bf7a6ea68..f190a3e2e 100644 --- a/sdk/go/channel.go +++ b/sdk/go/channel.go @@ -566,12 +566,11 @@ func (c *Client) Checkpoint(ctx context.Context, asset string) (string, error) { blockchainID := state.HomeLedger.BlockchainID // Initialize blockchain client if needed - if err := c.initializeBlockchainClient(ctx, blockchainID); err != nil { + blockchainClient, err := c.getOrInitBlockchainClient(ctx, blockchainID) + if err != nil { return "", err } - blockchainClient := c.blockchainClients[blockchainID] - // Get home channel info to determine on-chain status channel, err := c.GetHomeChannel(ctx, userWallet, asset) if err != nil { @@ -699,11 +698,11 @@ func (c *Client) Challenge(ctx context.Context, state core.State) (string, error // Initialize blockchain client and submit blockchainID := state.HomeLedger.BlockchainID - if err := c.initializeBlockchainClient(ctx, blockchainID); err != nil { + blockchainClient, err := c.getOrInitBlockchainClient(ctx, blockchainID) + if err != nil { return "", err } - blockchainClient := c.blockchainClients[blockchainID] txHash, err := blockchainClient.Challenge(state, challengerSig, core.ChannelParticipantUser) if err != nil { return "", fmt.Errorf("failed to challenge on blockchain: %w", err) @@ -980,11 +979,11 @@ func (c *Client) SignChannelSessionKeyState(state core.ChannelSessionKeyStateV1) // - Transaction hash of the approval transaction // - Error if the operation fails or the asset is a native token func (c *Client) ApproveToken(ctx context.Context, chainID uint64, asset string, amount decimal.Decimal) (string, error) { - if err := c.initializeBlockchainClient(ctx, chainID); err != nil { + blockchainClient, err := c.getOrInitBlockchainClient(ctx, chainID) + if err != nil { return "", err } - blockchainClient := c.blockchainClients[chainID] return blockchainClient.Approve(asset, amount) } @@ -1003,10 +1002,10 @@ func (c *Client) ApproveToken(ctx context.Context, chainID uint64, asset string, // Requirements: // - Blockchain RPC must be configured for the chain (use WithBlockchainRPC) func (c *Client) GetOnChainBalance(ctx context.Context, chainID uint64, asset string, wallet string) (decimal.Decimal, error) { - if err := c.initializeBlockchainClient(ctx, chainID); err != nil { + blockchainClient, err := c.getOrInitBlockchainClient(ctx, chainID) + if err != nil { return decimal.Zero, err } - blockchainClient := c.blockchainClients[chainID] return blockchainClient.GetTokenBalance(asset, wallet) } diff --git a/sdk/go/client.go b/sdk/go/client.go index bb4e5de74..520bc7369 100644 --- a/sdk/go/client.go +++ b/sdk/go/client.go @@ -14,6 +14,7 @@ import ( "github.com/layer-3/nitrolite/pkg/core" "github.com/layer-3/nitrolite/pkg/rpc" "github.com/layer-3/nitrolite/pkg/sign" + "github.com/shopspring/decimal" ) // Client provides a unified interface for interacting with Clearnode. @@ -47,15 +48,17 @@ import ( // config, _ := client.GetConfig(ctx) // balances, _ := client.GetBalances(ctx, walletAddress) type Client struct { - rpcClient *rpc.Client - config Config - exitCh chan struct{} - closeOnce sync.Once - blockchainClients map[uint64]core.Client - homeBlockchains map[string]uint64 - stateSigner core.ChannelSigner - rawSigner sign.Signer - assetStore *clientAssetStore + rpcClient *rpc.Client + config Config + exitCh chan struct{} + closeOnce sync.Once + chainsMu sync.Mutex + blockchainClients map[uint64]core.BlockchainClient + blockchainLockingClients map[uint64]*evm.LockingClient + homeBlockchains map[string]uint64 + stateSigner core.ChannelSigner + rawSigner sign.Signer + assetStore *clientAssetStore } // NewClient creates a new Clearnode client with both high-level and low-level methods. @@ -101,13 +104,14 @@ func NewClient(wsURL string, stateSigner core.ChannelSigner, rawSigner sign.Sign // Create client instance client := &Client{ - rpcClient: rpcClient, - config: config, - exitCh: make(chan struct{}), - blockchainClients: make(map[uint64]core.Client), - homeBlockchains: make(map[string]uint64), - stateSigner: stateSigner, - rawSigner: rawSigner, + rpcClient: rpcClient, + config: config, + exitCh: make(chan struct{}), + blockchainClients: make(map[uint64]core.BlockchainClient), + blockchainLockingClients: make(map[uint64]*evm.LockingClient), + homeBlockchains: make(map[string]uint64), + stateSigner: stateSigner, + rawSigner: rawSigner, } // Create asset store @@ -296,41 +300,43 @@ func WithBlockchainRPC(chainID uint64, rpcURL string) Option { } } -// initializeBlockchainClient initializes a blockchain client for a specific chain. -// This is called lazily when a blockchain operation is needed. -func (c *Client) initializeBlockchainClient(ctx context.Context, chainID uint64) error { +// getOrInitBlockchainClient returns the blockchain client for a specific chain. +func (c *Client) getOrInitBlockchainClient(ctx context.Context, chainID uint64) (core.BlockchainClient, error) { + c.chainsMu.Lock() + defer c.chainsMu.Unlock() + // Check if already initialized - if _, exists := c.blockchainClients[chainID]; exists { - return nil + if bc, exists := c.blockchainClients[chainID]; exists { + return bc, nil } // Get RPC URL from config rpcURL, exists := c.config.BlockchainRPCs[chainID] if !exists { - return fmt.Errorf("blockchain RPC not configured for chain %d (use WithBlockchainRPC)", chainID) + return nil, fmt.Errorf("blockchain RPC not configured for chain %d (use WithBlockchainRPC)", chainID) } - // Get contract address for this blockchain - contractAddress, err := c.getContractAddress(ctx, chainID) + // Get channel hub address for this blockchain + channelHubAddress, err := c.getChannelHubAddress(ctx, chainID) if err != nil { - return err + return nil, err } // Get node address nodeAddress, err := c.getNodeAddress(ctx) if err != nil { - return err + return nil, err } // Connect to blockchain ethClient, err := ethclient.Dial(rpcURL) if err != nil { - return fmt.Errorf("failed to connect to blockchain RPC: %w", err) + return nil, fmt.Errorf("failed to connect to blockchain RPC: %w", err) } // Create blockchain client using the user's signer and node address - evmClient, err := evm.NewClient( - common.HexToAddress(contractAddress), + evmClient, err := evm.NewBlockchainClient( + common.HexToAddress(channelHubAddress), ethClient, c.rawSigner, chainID, @@ -339,11 +345,11 @@ func (c *Client) initializeBlockchainClient(ctx context.Context, chainID uint64) ) if err != nil { - return fmt.Errorf("failed to create blockchain client: %w", err) + return nil, fmt.Errorf("failed to create blockchain client: %w", err) } c.blockchainClients[chainID] = evmClient - return nil + return evmClient, nil } // generateNonce generates a random 8-byte nonce for channel creation. @@ -373,8 +379,8 @@ func (c *Client) getTokenAddress(ctx context.Context, blockchainID uint64, asset return "", fmt.Errorf("asset %s not found", asset) } -// getContractAddress retrieves the channel hub contract address for a specific blockchain from node config. -func (c *Client) getContractAddress(ctx context.Context, blockchainID uint64) (string, error) { +// getChannelHubAddress retrieves the channel hub contract address for a specific blockchain from node config. +func (c *Client) getChannelHubAddress(ctx context.Context, blockchainID uint64) (string, error) { nodeConfig, err := c.GetConfig(ctx) if err != nil { return "", fmt.Errorf("failed to get node config: %w", err) @@ -382,6 +388,9 @@ func (c *Client) getContractAddress(ctx context.Context, blockchainID uint64) (s for _, bc := range nodeConfig.Blockchains { if bc.ID == blockchainID { + if bc.ChannelHubAddress == "" { + return "", fmt.Errorf("channel hub address not configured for blockchain %d", blockchainID) + } return bc.ChannelHubAddress, nil } } @@ -389,6 +398,62 @@ func (c *Client) getContractAddress(ctx context.Context, blockchainID uint64) (s return "", fmt.Errorf("blockchain %d not found in node config", blockchainID) } +// getLockingContractAddress retrieves the Locking contract address for a specific blockchain from node config. +func (c *Client) getLockingContractAddress(ctx context.Context, blockchainID uint64) (string, error) { + nodeConfig, err := c.GetConfig(ctx) + if err != nil { + return "", fmt.Errorf("failed to get node config: %w", err) + } + + for _, bc := range nodeConfig.Blockchains { + if bc.ID == blockchainID { + if bc.LockingContractAddress == "" { + return "", fmt.Errorf("locking contract address not configured for blockchain %d", blockchainID) + } + return bc.LockingContractAddress, nil + } + } + + return "", fmt.Errorf("blockchain %d not found in node config", blockchainID) +} + +// getOrInitLockingClient returns the locking client for a specific chain, +// initializing it lazily if needed. Thread-safe. +func (c *Client) getOrInitLockingClient(ctx context.Context, chainID uint64) (*evm.LockingClient, error) { + c.chainsMu.Lock() + defer c.chainsMu.Unlock() + + if lc, exists := c.blockchainLockingClients[chainID]; exists { + return lc, nil + } + + rpcURL, exists := c.config.BlockchainRPCs[chainID] + if !exists { + return nil, fmt.Errorf("blockchain RPC not configured for chain %d (use WithBlockchainRPC)", chainID) + } + + lockingContractAddress, err := c.getLockingContractAddress(ctx, chainID) + if err != nil { + return nil, err + } + + ethClient, err := ethclient.Dial(rpcURL) + if err != nil { + return nil, fmt.Errorf("failed to connect to blockchain RPC: %w", err) + } + client, err := evm.NewLockingClient( + common.HexToAddress(lockingContractAddress), + ethClient, + chainID, + c.rawSigner, + ) + if err != nil { + return nil, fmt.Errorf("failed to create Locking client: %w", err) + } + c.blockchainLockingClients[chainID] = client + return client, nil +} + // getNodeAddress retrieves the node's Ethereum address from the node config. func (c *Client) getNodeAddress(ctx context.Context) (string, error) { nodeConfig, err := c.GetConfig(ctx) @@ -407,3 +472,125 @@ func (c *Client) getSupportedSigValidatorsBitmap(ctx context.Context) (string, e } return core.BuildSigValidatorsBitmap(nodeConfig.SupportedSigValidators), nil } + +// ============================================================================ +// Locking On-Chain Methods +// ============================================================================ + +// EscrowSecurityTokens locks tokens into the Locking contract on the specified blockchain. +// The tokens are locked for the caller's own address. Before calling this method, +// you must approve the Locking to spend your tokens using ApproveSecurityToken. +// +// Parameters: +// - ctx: Context for the operation +// - destinationWalletAddress: The Ethereum address to lock tokens for +// - blockchainID: The blockchain network ID +// - amount: The amount of tokens to lock (in human-readable decimals, e.g., 100.5 USDC) +// +// Returns: +// - Transaction hash +// - Error if the operation fails +func (c *Client) EscrowSecurityTokens(ctx context.Context, targetWalletAddress string, blockchainID uint64, amount decimal.Decimal) (string, error) { + lc, err := c.getOrInitLockingClient(ctx, blockchainID) + if err != nil { + return "", err + } + return lc.Lock(targetWalletAddress, amount) +} + +// InitiateSecurityTokensWithdrawal initiates the unlock process for locked tokens in the Locking contract. +// After the unlock period elapses, Withdraw Security Tokens can be called to retrieve the tokens. +// +// Parameters: +// - ctx: Context for the operation +// - blockchainID: The blockchain network ID +// +// Returns: +// - Transaction hash +// - Error if the operation fails +func (c *Client) InitiateSecurityTokensWithdrawal(ctx context.Context, blockchainID uint64) (string, error) { + lc, err := c.getOrInitLockingClient(ctx, blockchainID) + if err != nil { + return "", err + } + + return lc.Unlock() +} + +// CancelSecurityTokensWithdrawal re-locks tokens that are currently in the unlocking state, +// cancelling the pending unlock and returning them to the locked state. +// +// Parameters: +// - ctx: Context for the operation +// - blockchainID: The blockchain network ID +// +// Returns: +// - Transaction hash +// - Error if the operation fails +func (c *Client) CancelSecurityTokensWithdrawal(ctx context.Context, blockchainID uint64) (string, error) { + lc, err := c.getOrInitLockingClient(ctx, blockchainID) + if err != nil { + return "", err + } + + return lc.Relock() +} + +// WithdrawSecurityTokens withdraws unlocked tokens from the Locking contract to the specified destination. +// Can only be called after the unlock period has fully elapsed. +// +// Parameters: +// - ctx: Context for the operation +// - blockchainID: The blockchain network ID +// - destinationWalletAddress: The Ethereum address to receive the withdrawn tokens +// +// Returns: +// - Transaction hash +// - Error if the operation fails +func (c *Client) WithdrawSecurityTokens(ctx context.Context, blockchainID uint64, destinationWalletAddress string) (string, error) { + lc, err := c.getOrInitLockingClient(ctx, blockchainID) + if err != nil { + return "", err + } + + return lc.Withdraw(destinationWalletAddress) +} + +// ApproveSecurityToken approves the Locking contract to spend tokens on behalf of the caller. +// This must be called before Lock Security Tokens. +// +// Parameters: +// - ctx: Context for the operation +// - chainID: The blockchain network ID +// - amount: The amount of tokens to approve +// +// Returns: +// - Transaction hash +// - Error if the operation fails +func (c *Client) ApproveSecurityToken(ctx context.Context, chainID uint64, amount decimal.Decimal) (string, error) { + lc, err := c.getOrInitLockingClient(ctx, chainID) + if err != nil { + return "", err + } + + return lc.ApproveToken(amount) +} + +// GetLockedBalance returns the locked balance of a user in the Locking contract. +// +// Parameters: +// - ctx: Context for the operation +// - chainID: The blockchain network ID +// - wallet: The Ethereum address to check +// +// Returns: +// - The locked balance as a decimal (adjusted for token decimals) +// - Error if the query fails +func (c *Client) GetLockedBalance(ctx context.Context, chainID uint64, wallet string) (decimal.Decimal, error) { + lc, err := c.getOrInitLockingClient(ctx, chainID) + if err != nil { + return decimal.Zero, err + } + + return lc.GetBalance(wallet) +} diff --git a/sdk/go/examples/app_sessions/lifecycle.go b/sdk/go/examples/app_sessions/lifecycle.go index 78dadb041..bc2da6fb4 100644 --- a/sdk/go/examples/app_sessions/lifecycle.go +++ b/sdk/go/examples/app_sessions/lifecycle.go @@ -2,20 +2,27 @@ package main // Example: Complete App Session Lifecycle // +// Prerequisites (minimum channel balances): +// - Wallet 1: 0.0001 USDC +// - Wallet 2: 0.00015 WETH +// - Wallet 3: no balance required (receives funds via redistribution) +// // This example demonstrates: -// 1. Create first app session for wallet 1 -// 2. Deposit USDC into first app session by wallet 1 -// 3. Create second app session for wallet 2 with wallet 3 as a participant -// 4. Deposit WETH into second app session by wallet 2 -// 5. Redistribute app state within app session so that participant with wallet 3 also has some allocation -// 6. Rebalance 2 app sessions atomically +// 1. Register apps in the app registry (required before creating app sessions) +// 2. Create first app session for wallet 1 +// 3. Deposit USDC into first app session by wallet 1 +// 4. Create second app session for wallet 2 with wallet 3 as a participant +// 5. Deposit WETH into second app session by wallet 2 +// 6. Redistribute app state within app session so that participant with wallet 3 also has some allocation // 7. Wallet 3 withdraws from his app session // 8. Close both app sessions +// 9. Fail case: attempt to create app session for unregistered app (expected to fail) import ( "context" "fmt" "log" + "math/rand" "time" "github.com/ethereum/go-ethereum/common/hexutil" @@ -30,28 +37,42 @@ import ( func main() { ctx := context.Background() - wsURL := "wss://deployment.yellow.org/ws" + wsURL := "wss://clearnode-sandbox.yellow.org/v1/ws" // --- 0. Setup Wallets --- // Replace these strings with your actual hex private keys - wallet1PrivateKey := "0x7d60..." - wallet2PrivateKey := "0x9b65..." - wallet3PrivateKey := "0xf636..." + wallet1PrivateKey := "0x7d607..." + wallet2PrivateKey := "0x9b652..." + wallet3PrivateKey := "0xf6369..." - // Create signers from private keys - wallet1Signer, err := sign.NewEthereumMsgSigner(wallet1PrivateKey) + // Create raw signers from private keys + wallet1RawSigner, err := sign.NewEthereumRawSigner(wallet1PrivateKey) if err != nil { log.Fatalf("Invalid wallet 1 private key: %v", err) } - wallet2Signer, err := sign.NewEthereumMsgSigner(wallet2PrivateKey) + wallet2RawSigner, err := sign.NewEthereumRawSigner(wallet2PrivateKey) if err != nil { log.Fatalf("Invalid wallet 2 private key: %v", err) } - wallet3Signer, err := sign.NewEthereumMsgSigner(wallet3PrivateKey) + wallet3RawSigner, err := sign.NewEthereumRawSigner(wallet3PrivateKey) if err != nil { log.Fatalf("Invalid wallet 3 private key: %v", err) } + // Create msg signers (EIP-191 prefixed) for channel and app session signing + wallet1Signer, err := sign.NewEthereumMsgSignerFromRaw(wallet1RawSigner) + if err != nil { + log.Fatalf("Failed to create msg signer for wallet 1: %v", err) + } + wallet2Signer, err := sign.NewEthereumMsgSignerFromRaw(wallet2RawSigner) + if err != nil { + log.Fatalf("Failed to create msg signer for wallet 2: %v", err) + } + wallet3Signer, err := sign.NewEthereumMsgSignerFromRaw(wallet3RawSigner) + if err != nil { + log.Fatalf("Failed to create msg signer for wallet 3: %v", err) + } + channel1Signer, err := core.NewChannelDefaultSigner(wallet1Signer) if err != nil { log.Fatalf("Failed to create channel signer for wallet 1: %v", err) @@ -62,7 +83,7 @@ func main() { } channel3Signer, err := core.NewChannelDefaultSigner(wallet3Signer) if err != nil { - log.Fatalf("Failed to create channel signer for wallet 2: %v", err) + log.Fatalf("Failed to create channel signer for wallet 3: %v", err) } appSession1Signer, err := app.NewAppSessionWalletSignerV1(wallet1Signer) @@ -75,9 +96,9 @@ func main() { } // Extract wallet addresses - wallet1Address := wallet1Signer.PublicKey().Address().String() // 0x053aEAD7d3eebE4359300fDE849bCD9E77384989 - wallet2Address := wallet2Signer.PublicKey().Address().String() // 0x2BfA10aAd64Ae0F7855f54f27117Fcc9C61C6770 - wallet3Address := wallet3Signer.PublicKey().Address().String() // 0xaB5670b44cb4A3B5535BD637cb600DA572148c98 + wallet1Address := wallet1RawSigner.PublicKey().Address().String() + wallet2Address := wallet2RawSigner.PublicKey().Address().String() + wallet3Address := wallet3RawSigner.PublicKey().Address().String() fmt.Println("--- Wallets Imported ---") fmt.Printf("Wallet 1 Address: %s\n", wallet1Address) @@ -86,16 +107,41 @@ func main() { fmt.Println("------------------------") // Create SDK clients (in a real app, these would be separate instances) - wallet1Client, err := sdk.NewClient(wsURL, channel1Signer, wallet1Signer) + wallet1Client, err := sdk.NewClient(wsURL, channel1Signer, wallet1RawSigner) + if err != nil { + log.Fatal(err) + } + wallet2Client, err := sdk.NewClient(wsURL, channel2Signer, wallet2RawSigner) if err != nil { log.Fatal(err) } + wallet3Client, err := sdk.NewClient(wsURL, channel3Signer, wallet3RawSigner) + if err != nil { + log.Fatal(err) + } + + // --- 1. Register Apps --- + fmt.Println("=== Step 1: Registering Apps ===") + + suffix := fmt.Sprintf("%06d", rand.Intn(1000000)) + app1ID := "test-app-" + suffix + app2ID := "multi-party-app-" + suffix + + if err := wallet1Client.RegisterApp(ctx, app1ID, "{}", true); err != nil { + log.Fatalf("Failed to register %s: %v", app1ID, err) + } + fmt.Printf("✓ Registered app: %s\n", app1ID) + + if err := wallet1Client.RegisterApp(ctx, app2ID, "{}", false); err != nil { + log.Fatalf("Failed to register %s: %v", app2ID, err) + } + fmt.Printf("✓ Registered app: %s (owner approval required)\n\n", app2ID) - // --- 1. Create App Session 1 (Single Participant: Wallet 1) --- - fmt.Println("=== Step 1: Creating App Session 1 (Wallet 1 only) ===") + // --- 2. Create App Session 1 (Single Participant: Wallet 1) --- + fmt.Println("=== Step 2: Creating App Session 1 (Wallet 1 only) ===") session1Definition := app.AppDefinitionV1{ - ApplicationID: "test-app", + ApplicationID: app1ID, Participants: []app.AppParticipantV1{ {WalletAddress: wallet1Address, SignatureWeight: 100}, }, @@ -115,8 +161,8 @@ func main() { } fmt.Printf("✓ Created App Session 1: %s\n\n", session1ID) - // --- 2. Deposit USDC into Session 1 --- - fmt.Println("=== Step 2: Depositing USDC into Session 1 ===") + // --- 3. Deposit USDC into Session 1 --- + fmt.Println("=== Step 3: Depositing USDC into Session 1 ===") session1DepositAmount := decimal.NewFromFloat(0.0001) session1DepositUpdate := app.AppStateUpdateV1{ @@ -139,19 +185,10 @@ func main() { } fmt.Printf("✓ Deposited %s USDC into Session 1\n\n", session1DepositAmount) - // --- 3. Create App Session 2 (Multi-Party: Wallet 2 & 3) --- - fmt.Println("=== Step 3: Creating App Session 2 (Wallet 2 & 3) ===") + // --- 4. Create App Session 2 (Multi-Party: Wallet 2 & 3) --- + fmt.Println("=== Step 4: Creating App Session 2 (Wallet 2 & 3) ===") - appID := "multi-party-app" - wallet2Client, err := sdk.NewClient(wsURL, channel2Signer, wallet2Signer) - if err != nil { - log.Fatal(err) - } - - wallet3Client, err := sdk.NewClient(wsURL, channel3Signer, wallet3Signer) // No channel signer needed for wallet 3 since it's not creating channels - if err != nil { - log.Fatal(err) - } + appID := app2ID msgSigner3, err := generateMsgSigner() if err != nil { @@ -202,16 +239,23 @@ func main() { appSession2CreateSession2Sig, _ := appSession2Signer.Sign(session2CreateRequest) appSession3CreateSession2Sig, _ := appSession3Signer.Sign(session2CreateRequest) - session2ID, _, _, err := wallet2Client.CreateAppSession(ctx, session2Definition, "{}", []string{appSession2CreateSession2Sig.String(), appSession3CreateSession2Sig.String()}) + + // Owner approval: wallet1 is the owner of app2, sign the create request using app session signer + ownerApprovalSig, err := appSession1Signer.Sign(session2CreateRequest) + if err != nil { + log.Fatalf("Failed to sign owner approval: %v", err) + } + + session2ID, _, _, err := wallet2Client.CreateAppSession(ctx, session2Definition, "{}", []string{appSession2CreateSession2Sig.String(), appSession3CreateSession2Sig.String()}, sdk.CreateAppSessionOptions{OwnerSig: ownerApprovalSig.String()}) if err != nil { log.Fatal(err) } fmt.Printf("✓ Created App Session 2: %s\n\n", session2ID) - // --- 4. Deposit WETH into Session 2 by Wallet 2 --- - fmt.Println("=== Step 4: Depositing WETH into Session 2 ===") + // --- 5. Deposit WETH into Session 2 by Wallet 2 --- + fmt.Println("=== Step 5: Depositing WETH into Session 2 ===") - session2DepositAmount := decimal.NewFromFloat(0.015) + session2DepositAmount := decimal.NewFromFloat(0.00015) session2DepositUpdate := app.AppStateUpdateV1{ AppSessionID: session2ID, Intent: app.AppStateUpdateIntentDeposit, @@ -242,16 +286,16 @@ func main() { fmt.Printf("Session 2 before redistribution - Version: %d, Allocations: %+v\n\n", session2InfoBeforeRedist[0].Version, session2InfoBeforeRedist[0].Allocations) } - // --- 5. Redistribute within Session 2 (Wallet 2 -> Wallet 3) --- - fmt.Println("=== Step 5: Redistributing funds in Session 2 ===") + // --- 6. Redistribute within Session 2 (Wallet 2 -> Wallet 3) --- + fmt.Println("=== Step 6: Redistributing funds in Session 2 ===") session2RedistributeUpdate := app.AppStateUpdateV1{ AppSessionID: session2ID, Intent: app.AppStateUpdateIntentOperate, Version: 3, Allocations: []app.AppAllocationV1{ - {Participant: wallet2Address, Asset: "weth", Amount: decimal.NewFromFloat(0.01)}, - {Participant: wallet3Address, Asset: "weth", Amount: decimal.NewFromFloat(0.005)}, + {Participant: wallet2Address, Asset: "weth", Amount: decimal.NewFromFloat(0.0001)}, + {Participant: wallet3Address, Asset: "weth", Amount: decimal.NewFromFloat(0.00005)}, }, } @@ -268,83 +312,84 @@ func main() { if err != nil { log.Fatalf("Redistribution failed: %v", err) } - fmt.Println("✓ Redistributed WETH: Wallet 2 (0.01) -> Wallet 3 (0.005)") - - // --- 6. Rebalance Both App Sessions Atomically --- - fmt.Println("=== Step 6: Atomic Rebalance Across Sessions ===") - - // Check current allocations before rebalance - session1InfoBeforeRebalance, _, err := wallet1Client.GetAppSessions(ctx, &sdk.GetAppSessionsOptions{AppSessionID: &session1ID}) - if err != nil { - log.Fatal(err) - } - if len(session1InfoBeforeRebalance) > 0 { - fmt.Printf("Session 1 before rebalance - Version: %d, Allocations: %+v\n", session1InfoBeforeRebalance[0].Version, session1InfoBeforeRebalance[0].Allocations) - } - - session2InfoBeforeRebalance, _, err := wallet2Client.GetAppSessions(ctx, &sdk.GetAppSessionsOptions{AppSessionID: &session2ID}) - if err != nil { - log.Fatal(err) - } - if len(session2InfoBeforeRebalance) > 0 { - fmt.Printf("Session 2 before rebalance - Version: %d, Allocations: %+v\n\n", session2InfoBeforeRebalance[0].Version, session2InfoBeforeRebalance[0].Allocations) - } - - // Prepare rebalance updates for both sessions - session1RebalanceUpdate := app.AppStateUpdateV1{ - AppSessionID: session1ID, - Intent: app.AppStateUpdateIntentRebalance, - Version: 3, - Allocations: []app.AppAllocationV1{ - {Participant: wallet1Address, Asset: "weth", Amount: decimal.NewFromFloat(0.005)}, - {Participant: wallet1Address, Asset: "usdc", Amount: decimal.NewFromFloat(0.00005)}, - }, - } - - session1RebalanceRequest, err := app.PackAppStateUpdateV1(session1RebalanceUpdate) - if err != nil { - log.Fatal(err) - } - - appSession1RebalanceSig, _ := appSession1Signer.Sign(session1RebalanceRequest) - - session2RebalanceUpdate := app.AppStateUpdateV1{ - AppSessionID: session2ID, - Intent: app.AppStateUpdateIntentRebalance, - Version: 4, - Allocations: []app.AppAllocationV1{ - {Participant: wallet2Address, Asset: "usdc", Amount: decimal.NewFromFloat(0.00005)}, - {Participant: wallet2Address, Asset: "weth", Amount: decimal.NewFromFloat(0.005)}, - {Participant: wallet3Address, Asset: "weth", Amount: decimal.NewFromFloat(0.005)}, - }, - } - - session2RebalanceRequest, err := app.PackAppStateUpdateV1(session2RebalanceUpdate) - if err != nil { - log.Fatal(err) - } - - appSession2RebalanceSig, _ := appSession2Signer.Sign(session2RebalanceRequest) - appSession3RebalanceSig, _ := appSession3Signer.Sign(session2RebalanceRequest) - - // Submit atomic rebalance - signedRebalanceUpdates := []app.SignedAppStateUpdateV1{ - { - AppStateUpdate: session1RebalanceUpdate, - QuorumSigs: []string{appSession1RebalanceSig.String()}, - }, - { - AppStateUpdate: session2RebalanceUpdate, - QuorumSigs: []string{appSession2RebalanceSig.String(), appSession3RebalanceSig.String()}, - }, - } - - rebalanceBatchID, err := wallet2Client.RebalanceAppSessions(ctx, signedRebalanceUpdates) - if err != nil { - log.Printf("⚠ Rebalance Error: %v", err) - } else { - fmt.Printf("✓ Atomic Rebalance Submitted. BatchID: %s\n\n", rebalanceBatchID) - } + fmt.Println("✓ Redistributed WETH: Wallet 2 (0.0001) -> Wallet 3 (0.00005)") + + // NOTE: Rebalance step is disabled. + // // --- 7. Rebalance Both App Sessions Atomically --- + // fmt.Println("=== Step 6: Atomic Rebalance Across Sessions ===") + + // // Check current allocations before rebalance + // session1InfoBeforeRebalance, _, err := wallet1Client.GetAppSessions(ctx, &sdk.GetAppSessionsOptions{AppSessionID: &session1ID}) + // if err != nil { + // log.Fatal(err) + // } + // if len(session1InfoBeforeRebalance) > 0 { + // fmt.Printf("Session 1 before rebalance - Version: %d, Allocations: %+v\n", session1InfoBeforeRebalance[0].Version, session1InfoBeforeRebalance[0].Allocations) + // } + + // session2InfoBeforeRebalance, _, err := wallet2Client.GetAppSessions(ctx, &sdk.GetAppSessionsOptions{AppSessionID: &session2ID}) + // if err != nil { + // log.Fatal(err) + // } + // if len(session2InfoBeforeRebalance) > 0 { + // fmt.Printf("Session 2 before rebalance - Version: %d, Allocations: %+v\n\n", session2InfoBeforeRebalance[0].Version, session2InfoBeforeRebalance[0].Allocations) + // } + + // // Prepare rebalance updates for both sessions + // session1RebalanceUpdate := app.AppStateUpdateV1{ + // AppSessionID: session1ID, + // Intent: app.AppStateUpdateIntentRebalance, + // Version: 3, + // Allocations: []app.AppAllocationV1{ + // {Participant: wallet1Address, Asset: "weth", Amount: decimal.NewFromFloat(0.005)}, + // {Participant: wallet1Address, Asset: "usdc", Amount: decimal.NewFromFloat(0.00005)}, + // }, + // } + + // session1RebalanceRequest, err := app.PackAppStateUpdateV1(session1RebalanceUpdate) + // if err != nil { + // log.Fatal(err) + // } + + // appSession1RebalanceSig, _ := appSession1Signer.Sign(session1RebalanceRequest) + + // session2RebalanceUpdate := app.AppStateUpdateV1{ + // AppSessionID: session2ID, + // Intent: app.AppStateUpdateIntentRebalance, + // Version: 4, + // Allocations: []app.AppAllocationV1{ + // {Participant: wallet2Address, Asset: "usdc", Amount: decimal.NewFromFloat(0.00005)}, + // {Participant: wallet2Address, Asset: "weth", Amount: decimal.NewFromFloat(0.005)}, + // {Participant: wallet3Address, Asset: "weth", Amount: decimal.NewFromFloat(0.005)}, + // }, + // } + + // session2RebalanceRequest, err := app.PackAppStateUpdateV1(session2RebalanceUpdate) + // if err != nil { + // log.Fatal(err) + // } + + // appSession2RebalanceSig, _ := appSession2Signer.Sign(session2RebalanceRequest) + // appSession3RebalanceSig, _ := appSession3Signer.Sign(session2RebalanceRequest) + + // // Submit atomic rebalance + // signedRebalanceUpdates := []app.SignedAppStateUpdateV1{ + // { + // AppStateUpdate: session1RebalanceUpdate, + // QuorumSigs: []string{appSession1RebalanceSig.String()}, + // }, + // { + // AppStateUpdate: session2RebalanceUpdate, + // QuorumSigs: []string{appSession2RebalanceSig.String(), appSession3RebalanceSig.String()}, + // }, + // } + + // rebalanceBatchID, err := wallet2Client.RebalanceAppSessions(ctx, signedRebalanceUpdates) + // if err != nil { + // log.Printf("⚠ Rebalance Error: %v", err) + // } else { + // fmt.Printf("✓ Atomic Rebalance Submitted. BatchID: %s\n\n", rebalanceBatchID) + // } // --- 7. Wallet 3 Withdraws from Session 2 --- fmt.Println("=== Step 7: Wallet 3 Withdrawing from Session 2 ===") @@ -352,11 +397,10 @@ func main() { session2WithdrawUpdate := app.AppStateUpdateV1{ AppSessionID: session2ID, Intent: app.AppStateUpdateIntentWithdraw, - Version: 5, + Version: 4, Allocations: []app.AppAllocationV1{ - {Participant: wallet2Address, Asset: "usdc", Amount: decimal.NewFromFloat(0.00005)}, - {Participant: wallet2Address, Asset: "weth", Amount: decimal.NewFromFloat(0.005)}, - {Participant: wallet3Address, Asset: "weth", Amount: decimal.NewFromFloat(0.001)}, + {Participant: wallet2Address, Asset: "weth", Amount: decimal.NewFromFloat(0.00005)}, + {Participant: wallet3Address, Asset: "weth", Amount: decimal.NewFromFloat(0.00001)}, }, } @@ -372,7 +416,7 @@ func main() { if err != nil { log.Printf("⚠ Withdraw Error: %v", err) } else { - fmt.Println("✓ Wallet 3 successfully withdrew 0.004 WETH back to channel") + fmt.Println("✓ Wallet 3 successfully withdrew WETH back to channel") } // --- 8. Close Both App Sessions --- @@ -382,10 +426,9 @@ func main() { session1CloseUpdate := app.AppStateUpdateV1{ AppSessionID: session1ID, Intent: app.AppStateUpdateIntentClose, - Version: 4, + Version: 3, Allocations: []app.AppAllocationV1{ - {Participant: wallet1Address, Asset: "weth", Amount: decimal.NewFromFloat(0.005)}, - {Participant: wallet1Address, Asset: "usdc", Amount: decimal.NewFromFloat(0.00005)}, + {Participant: wallet1Address, Asset: "usdc", Amount: decimal.NewFromFloat(0.0001)}, }, } @@ -407,11 +450,10 @@ func main() { session2CloseUpdate := app.AppStateUpdateV1{ AppSessionID: session2ID, Intent: app.AppStateUpdateIntentClose, - Version: 6, + Version: 5, Allocations: []app.AppAllocationV1{ - {Participant: wallet2Address, Asset: "usdc", Amount: decimal.NewFromFloat(0.00005)}, - {Participant: wallet2Address, Asset: "weth", Amount: decimal.NewFromFloat(0.005)}, - {Participant: wallet3Address, Asset: "weth", Amount: decimal.NewFromFloat(0.001)}, + {Participant: wallet2Address, Asset: "weth", Amount: decimal.NewFromFloat(0.00005)}, + {Participant: wallet3Address, Asset: "weth", Amount: decimal.NewFromFloat(0.00001)}, }, } @@ -430,6 +472,31 @@ func main() { fmt.Println("✓ Session 2 successfully closed") } + // --- 9. Fail Case: Create App Session for Unregistered App --- + fmt.Println("\n=== Step 9: Creating App Session for Unregistered App (expected to fail) ===") + + unregisteredDefinition := app.AppDefinitionV1{ + ApplicationID: "unregistered-app-" + suffix, + Participants: []app.AppParticipantV1{ + {WalletAddress: wallet1Address, SignatureWeight: 100}, + }, + Quorum: 100, + Nonce: uint64(time.Now().UnixNano()), + } + + unregisteredCreateRequest, err := app.PackCreateAppSessionRequestV1(unregisteredDefinition, "{}") + if err != nil { + log.Fatal(err) + } + + unregisteredSig, _ := appSession1Signer.Sign(unregisteredCreateRequest) + _, _, _, err = wallet1Client.CreateAppSession(ctx, unregisteredDefinition, "{}", []string{unregisteredSig.String()}) + if err != nil { + fmt.Printf("✓ Expected error: %v\n", err) + } else { + fmt.Println("✗ Unexpected success: app session was created for unregistered app") + } + fmt.Println("\n=== Example Complete ===") } diff --git a/sdk/go/examples/challenge/main.go b/sdk/go/examples/challenge/main.go index 4d128df28..cdc9b3da2 100644 --- a/sdk/go/examples/challenge/main.go +++ b/sdk/go/examples/challenge/main.go @@ -30,7 +30,7 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() - wsURL := "wss://deployment.yellow.org/ws" + wsURL := "wss://clearnode-sandbox.yellow.org/v1/ws" privateKeyHex := "0x7d6..." chainID := uint64(11155111) rpcUrl := "https://sepolia.drpc.org" diff --git a/sdk/go/utils.go b/sdk/go/utils.go index faf992127..50040fb5b 100644 --- a/sdk/go/utils.go +++ b/sdk/go/utils.go @@ -26,10 +26,11 @@ func transformNodeConfig(resp rpc.NodeV1GetConfigResponse) (*core.NodeConfig, er } blockchains = append(blockchains, core.Blockchain{ - Name: info.Name, - ID: blockchainID, - ChannelHubAddress: info.ChannelHubAddress, - BlockStep: 0, // Not provided in RPC response + Name: info.Name, + ID: blockchainID, + ChannelHubAddress: info.ChannelHubAddress, + LockingContractAddress: info.LockingContractAddress, + BlockStep: 0, // Not provided in RPC response }) } diff --git a/sdk/ts-compat/examples/lifecycle.ts b/sdk/ts-compat/examples/lifecycle.ts index a97d47fa4..6bd259bac 100644 --- a/sdk/ts-compat/examples/lifecycle.ts +++ b/sdk/ts-compat/examples/lifecycle.ts @@ -12,7 +12,7 @@ * * Prerequisites: * - PRIVATE_KEY env var set to an Ethereum private key - * - CLEARNODE_WS_URL env var (defaults to wss://clearnode-v1-rc.yellow.org/ws) + * - CLEARNODE_WS_URL env var (defaults to wss://clearnode-sandbox.yellow.org/v1/ws) * - CHAIN_ID env var (defaults to 11155111 for Sepolia) * * Balance-dependent tests: @@ -100,7 +100,7 @@ async function walletSign(wc: any, hash: Hex): Promise { } const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`; -const WS_URL = process.env.CLEARNODE_WS_URL || 'wss://clearnode-v1-rc.yellow.org/ws'; +const WS_URL = process.env.CLEARNODE_WS_URL || 'wss://clearnode-sandbox.yellow.org/v1/ws'; const CHAIN_ID = parseInt(process.env.CHAIN_ID || '11155111', 10); if (Number.isNaN(CHAIN_ID)) { console.error('CHAIN_ID must be a valid integer'); diff --git a/sdk/ts-compat/examples/output.txt b/sdk/ts-compat/examples/output.txt index b8cf1c46e..d7563d30b 100644 --- a/sdk/ts-compat/examples/output.txt +++ b/sdk/ts-compat/examples/output.txt @@ -1,7 +1,7 @@ === Compat Layer Comprehensive Lifecycle === Wallet: 0x -Clearnode: wss://clearnode-v1-rc.yellow.org/ws +Clearnode: wss://clearnode-sandbox.yellow.org/v1/ws Chain: 11155111 -- 1. Client Initialization -- diff --git a/sdk/ts/README.md b/sdk/ts/README.md index f2a001f1e..fa42f0edf 100644 --- a/sdk/ts/README.md +++ b/sdk/ts/README.md @@ -63,6 +63,7 @@ client.registerApp(appID, metadata, approvalNotRequired) // Register new client.getAppSessions(opts) // List sessions client.getAppDefinition(appSessionId) // Session definition client.createAppSession(definition, sessionData, sigs) // Create session +client.createAppSession(def, data, sigs, { ownerSig }) // Create with owner approval client.submitAppSessionDeposit(update, sigs, asset, amount) // Deposit to session client.submitAppState(update, sigs) // Update session client.rebalanceAppSessions(signedUpdates) // Atomic rebalance @@ -151,7 +152,7 @@ main().catch(console.error); ``` sdk/ts/src/ ├── client.ts # Core client, constructors, high-level operations -├── signers.ts # EthereumMsgSigner and EthereumRawSigner +├── signers.ts # Signers (EthereumMsg/Raw, Channel, AppSession) ├── config.ts # Configuration options ├── asset_store.ts # Asset metadata caching ├── utils.ts # Type transformations @@ -438,6 +439,50 @@ await client.submitAppState(appUpdate, quorumSigs); const batchId = await client.rebalanceAppSessions(signedUpdates); ``` +#### Owner Approval for App Session Creation + +When an app is registered with `creationApprovalNotRequired: false`, the app owner must sign the session creation request. Pass the owner's signature via the options parameter: + +```typescript +import { AppSessionWalletSignerV1, EthereumMsgSigner } from '@yellow-org/sdk'; + +// Owner signs the create request using their app session signer +const ownerMsgSigner = new EthereumMsgSigner(ownerPrivateKey); +const ownerAppSessionSigner = new AppSessionWalletSignerV1(ownerMsgSigner); +const ownerSig = await ownerAppSessionSigner.signMessage(createRequest); + +const { appSessionId } = await client.createAppSession( + definition, + sessionData, + quorumSigs, + { ownerSig } +); +``` + +### App Session Signers + +App session operations require signatures with a type byte prefix, similar to channel signers: + +| Type | Byte | Class | Usage | +|------|------|-------|-------| +| Wallet | `0xA1` | `AppSessionWalletSignerV1` | Main wallet signs app session operations | +| Session Key | `0xA2` | `AppSessionKeySignerV1` | Delegated session key signs on behalf of wallet | + +```typescript +import { EthereumMsgSigner, AppSessionWalletSignerV1, AppSessionKeySignerV1 } from '@yellow-org/sdk'; + +// Create app session wallet signer +const msgSigner = new EthereumMsgSigner(privateKey); +const appSessionSigner = new AppSessionWalletSignerV1(msgSigner); + +// Sign app session operations (create, deposit, state updates, etc.) +const sig = await appSessionSigner.signMessage(packedRequest); + +// For session key delegation +const sessionKeyMsgSigner = new EthereumMsgSigner(sessionKeyPrivateKey); +const sessionKeySigner = new AppSessionKeySignerV1(sessionKeyMsgSigner); +``` + ### App Session Keys ```typescript @@ -755,8 +800,17 @@ queryTransactions().catch(console.error); ### Example 4: App Session Workflow +For a comprehensive multi-party app session example including app registration, owner approval, +session key delegation, deposits, redistribution, and more, see +[examples/app_sessions/lifecycle.ts](examples/app_sessions/lifecycle.ts). + ```typescript -import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; +import { + Client, createSigners, EthereumMsgSigner, + AppSessionWalletSignerV1, withBlockchainRPC +} from '@yellow-org/sdk'; +import { AppStateUpdateIntent } from '@yellow-org/sdk/app/types'; +import { packCreateAppSessionRequestV1, packAppStateUpdateV1 } from '@yellow-org/sdk/app/packing'; import Decimal from 'decimal.js'; async function appSessionExample() { @@ -769,42 +823,55 @@ async function appSessionExample() { ); try { + // Register app (required before creating sessions) + await client.registerApp('chess-v1', '{}', true); + + // Create app session signer + const msgSigner = new EthereumMsgSigner(process.env.PRIVATE_KEY!); + const appSessionSigner = new AppSessionWalletSignerV1(msgSigner); + // Create app session const definition = { - application: 'chess-v1', + applicationId: 'chess-v1', participants: [ - { walletAddress: client.getUserAddress(), signatureWeight: 1 }, - { walletAddress: '0xOpponent...', signatureWeight: 1 }, + { walletAddress: client.getUserAddress(), signatureWeight: 100 }, ], - quorum: 2, - nonce: 1n, + quorum: 100, + nonce: BigInt(Date.now() * 1000000), }; + const createRequest = packCreateAppSessionRequestV1(definition, '{}'); + const createSig = await appSessionSigner.signMessage(createRequest); + const { appSessionId } = await client.createAppSession( definition, '{}', - ['sig1', 'sig2'] + [createSig] ); console.log('Session created:', appSessionId); // Deposit to app session + const depositAmount = new Decimal(50); const appUpdate = { appSessionId, - intent: 1, // Deposit - version: 1n, + intent: AppStateUpdateIntent.Deposit, + version: 2n, allocations: [{ participant: client.getUserAddress(), asset: 'usdc', - amount: new Decimal(50), + amount: depositAmount, }], sessionData: '{}', }; + const depositRequest = packAppStateUpdateV1(appUpdate); + const depositSig = await appSessionSigner.signMessage(depositRequest); + const nodeSig = await client.submitAppSessionDeposit( appUpdate, - ['sig1'], + [depositSig], 'usdc', - new Decimal(50) + depositAmount ); console.log('Deposit signature:', nodeSig); @@ -876,6 +943,17 @@ import type { PaginationMetadata, } from '@yellow-org/sdk'; +// Signer types +import { + EthereumMsgSigner, + EthereumRawSigner, + ChannelDefaultSigner, + ChannelSessionKeyStateSigner, + AppSessionWalletSignerV1, + AppSessionKeySignerV1, + createSigners, +} from '@yellow-org/sdk'; + // App Registry types (from rpc/types) import type { AppV1, AppInfoV1 } from '@yellow-org/sdk'; ``` diff --git a/sdk/ts/examples/app_sessions/README.md b/sdk/ts/examples/app_sessions/README.md index 2afd7ea8b..819f1f751 100644 --- a/sdk/ts/examples/app_sessions/README.md +++ b/sdk/ts/examples/app_sessions/README.md @@ -15,7 +15,7 @@ This example demonstrates the complete lifecycle of Nitrolite app sessions, incl - Node.js 18+ installed - Three wallets with private keys -- Access to a Nitrolite node (default: `wss://deployment.yellow.org/ws`) +- Access to a Nitrolite node (default: `wss://clearnode-sandbox.yellow.org/v1/ws`) ## Setup diff --git a/sdk/ts/examples/app_sessions/lifecycle.ts b/sdk/ts/examples/app_sessions/lifecycle.ts index 0ec04893e..55cbbc60d 100644 --- a/sdk/ts/examples/app_sessions/lifecycle.ts +++ b/sdk/ts/examples/app_sessions/lifecycle.ts @@ -1,47 +1,66 @@ /** * Example: Complete App Session Lifecycle * + * Prerequisites (minimum channel balances): + * - Wallet 1: 0.0001 USDC + * - Wallet 2: 0.00015 WETH + * - Wallet 3: no balance required (receives funds via redistribution) + * * This example demonstrates: - * 1. Create first app session for wallet 1 - * 2. Deposit USDC into first app session by wallet 1 - * 3. Create second app session for wallet 2 with wallet 3 as a participant - * 4. Deposit WETH into second app session by wallet 2 - * 5. Redistribute app state within app session so that participant with wallet 3 also has some allocation - * 6. Rebalance 2 app sessions atomically + * 1. Register apps in the app registry (required before creating app sessions) + * 2. Create first app session for wallet 1 + * 3. Deposit USDC into first app session by wallet 1 + * 4. Create second app session for wallet 2 with wallet 3 as a participant + * 5. Deposit WETH into second app session by wallet 2 + * 6. Redistribute app state within app session so that participant with wallet 3 also has some allocation * 7. Wallet 3 withdraws from his app session * 8. Close both app sessions + * 9. Fail case: attempt to create app session for unregistered app (expected to fail) */ import Decimal from 'decimal.js'; +import { Hex } from 'viem'; +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { Client } from '../../src/client'; -import { createSigners } from '../../src/signers'; +import { + createSigners, + EthereumMsgSigner, + AppSessionWalletSignerV1, + AppSessionKeySignerV1, +} from '../../src/signers'; import { AppDefinitionV1, AppStateUpdateV1, AppStateUpdateIntent, - SignedAppStateUpdateV1, + AppSessionKeyStateV1, } from '../../src/app/types'; -import { packCreateAppSessionRequestV1, packAppStateUpdateV1 } from '../../src/app/packing'; +import { packCreateAppSessionRequestV1, packAppStateUpdateV1, packAppSessionKeyStateV1 } from '../../src/app/packing'; async function main() { // Replace with a real deployment url - const wsURL = 'wss://deployment.yellow.org/ws'; + const wsURL = 'wss://clearnode-sandbox.yellow.org/v1/ws'; // --- 0. Setup Wallets --- // Replace these strings with your actual hex private keys - const wallet1PrivateKey = '0x7d607...'; - const wallet2PrivateKey = '0x9b652...'; - const wallet3PrivateKey = '0xf6369...'; + const wallet1PrivateKey = '0x7d607...' as Hex; + const wallet2PrivateKey = '0x9b652...' as Hex; + const wallet3PrivateKey = '0xf6369...' as Hex; // Create signers from private keys const wallet1Signers = createSigners(wallet1PrivateKey); const wallet2Signers = createSigners(wallet2PrivateKey); const wallet3Signers = createSigners(wallet3PrivateKey); + // Create app session wallet signers (prepend 0x00 type byte) + const wallet1MsgSigner = new EthereumMsgSigner(wallet1PrivateKey); + const wallet2MsgSigner = new EthereumMsgSigner(wallet2PrivateKey); + const appSession1Signer = new AppSessionWalletSignerV1(wallet1MsgSigner); + const appSession2Signer = new AppSessionWalletSignerV1(wallet2MsgSigner); + // Extract wallet addresses - const wallet1Address = wallet1Signers.stateSigner.getAddress(); // 0x053aEAD7d3eebE4359300fDE849bCD9E77384989 - const wallet2Address = wallet2Signers.stateSigner.getAddress(); // 0x2BfA10aAd64Ae0F7855f54f27117Fcc9C61C6770 - const wallet3Address = wallet3Signers.stateSigner.getAddress(); // 0xaB5670b44cb4A3B5535BD637cb600DA572148c98 + const wallet1Address = wallet1Signers.stateSigner.getAddress(); + const wallet2Address = wallet2Signers.stateSigner.getAddress(); + const wallet3Address = wallet3Signers.stateSigner.getAddress(); console.log('--- Wallets Imported ---'); console.log(`Wallet 1 Address: ${wallet1Address}`); @@ -55,31 +74,52 @@ async function main() { wallet1Signers.stateSigner, wallet1Signers.txSigner ); + const wallet2Client = await Client.create( + wsURL, + wallet2Signers.stateSigner, + wallet2Signers.txSigner + ); + const wallet3Client = await Client.create( + wsURL, + wallet3Signers.stateSigner, + wallet3Signers.txSigner + ); + + // --- 1. Register Apps --- + console.log('=== Step 1: Registering Apps ==='); - // --- 1. Create App Session 1 (Single Participant: Wallet 1) --- - console.log('=== Step 1: Creating App Session 1 (Wallet 1 only) ==='); + const suffix = String(Math.floor(Math.random() * 1000000)).padStart(6, '0'); + const app1ID = `test-app-${suffix}`; + const app2ID = `multi-party-app-${suffix}`; + + await wallet1Client.registerApp(app1ID, '{}', true); + console.log(`✓ Registered app: ${app1ID}`); + + await wallet1Client.registerApp(app2ID, '{}', false); + console.log(`✓ Registered app: ${app2ID} (owner approval required)\n`); + + // --- 2. Create App Session 1 (Single Participant: Wallet 1) --- + console.log('=== Step 2: Creating App Session 1 (Wallet 1 only) ==='); const session1Definition: AppDefinitionV1 = { - application: 'test-app', + applicationId: app1ID, participants: [{ walletAddress: wallet1Address, signatureWeight: 100 }], quorum: 100, - nonce: BigInt(Date.now() * 1000000), // Use nanoseconds like Go + nonce: BigInt(Date.now() * 1000000), }; const session1CreateRequest = packCreateAppSessionRequestV1(session1Definition, '{}'); - const wallet1CreateSession1Sig = await wallet1Signers.stateSigner.signMessage( - session1CreateRequest - ); + const appSession1CreateSig = await appSession1Signer.signMessage(session1CreateRequest); const { appSessionId: session1ID } = await wallet1Client.createAppSession( session1Definition, '{}', - [wallet1CreateSession1Sig] + [appSession1CreateSig] ); console.log(`✓ Created App Session 1: ${session1ID}\n`); - // --- 2. Deposit USDC into Session 1 --- - console.log('=== Step 2: Depositing USDC into Session 1 ==='); + // --- 3. Deposit USDC into Session 1 --- + console.log('=== Step 3: Depositing USDC into Session 1 ==='); const session1DepositAmount = new Decimal(0.0001); const session1DepositUpdate: AppStateUpdateV1 = { @@ -93,12 +133,12 @@ async function main() { }; const session1DepositRequest = packAppStateUpdateV1(session1DepositUpdate); - const wallet1DepositSig = await wallet1Signers.stateSigner.signMessage(session1DepositRequest); + const appSession1DepositSig = await appSession1Signer.signMessage(session1DepositRequest); try { await wallet1Client.submitAppSessionDeposit( session1DepositUpdate, - [wallet1DepositSig], + [appSession1DepositSig], 'usdc', session1DepositAmount ); @@ -107,17 +147,39 @@ async function main() { console.log(`⚠ Deposit warning: ${err}`); } - // --- 3. Create App Session 2 (Multi-Party: Wallet 2 & 3) --- - console.log('=== Step 3: Creating App Session 2 (Wallet 2 & 3) ==='); + // --- 4. Create App Session 2 (Multi-Party: Wallet 2 & 3) --- + console.log('=== Step 4: Creating App Session 2 (Wallet 2 & 3) ==='); - const wallet2Client = await Client.create( - wsURL, - wallet2Signers.stateSigner, - wallet2Signers.txSigner - ); + const appID = app2ID; + + // Generate session key for wallet 3 + const sessionKey3PrivateKey = generatePrivateKey(); + const sessionKey3Account = privateKeyToAccount(sessionKey3PrivateKey); + const sessionKey3MsgSigner = new EthereumMsgSigner(sessionKey3PrivateKey); + + const expiresAt = Math.floor(Date.now() / 1000) + 10 * 60; // 10 minutes from now + + const appSessionKey3State: AppSessionKeyStateV1 = { + session_key: sessionKey3Account.address, + user_address: wallet3Address, + version: '1', + application_ids: [appID], + app_session_ids: [], + expires_at: String(expiresAt), + user_sig: '', + }; + + const packedAppSessionKey3State = packAppSessionKeyStateV1(appSessionKey3State); + const wallet3MsgSigner = new EthereumMsgSigner(wallet3PrivateKey); + const appSessionKey3StateSig = await wallet3MsgSigner.signMessage(packedAppSessionKey3State); + appSessionKey3State.user_sig = appSessionKey3StateSig; + + await wallet3Client.submitSessionKeyState(appSessionKey3State); + + const appSession3Signer = new AppSessionKeySignerV1(sessionKey3MsgSigner); const session2Definition: AppDefinitionV1 = { - application: 'multi-party-app', + applicationId: appID, participants: [ { walletAddress: wallet2Address, signatureWeight: 50 }, { walletAddress: wallet3Address, signatureWeight: 50 }, @@ -127,24 +189,24 @@ async function main() { }; const session2CreateRequest = packCreateAppSessionRequestV1(session2Definition, '{}'); - const wallet2CreateSession2Sig = await wallet2Signers.stateSigner.signMessage( - session2CreateRequest - ); - const wallet3CreateSession2Sig = await wallet3Signers.stateSigner.signMessage( - session2CreateRequest - ); + const appSession2CreateSig = await appSession2Signer.signMessage(session2CreateRequest); + const appSession3CreateSig = await appSession3Signer.signMessage(session2CreateRequest); + + // Owner approval: wallet1 is the owner of app2, sign the create request using app session signer + const ownerApprovalSig = await appSession1Signer.signMessage(session2CreateRequest); const { appSessionId: session2ID } = await wallet2Client.createAppSession( session2Definition, '{}', - [wallet2CreateSession2Sig, wallet3CreateSession2Sig] + [appSession2CreateSig, appSession3CreateSig], + { ownerSig: ownerApprovalSig } ); console.log(`✓ Created App Session 2: ${session2ID}\n`); - // --- 4. Deposit WETH into Session 2 by Wallet 2 --- - console.log('=== Step 4: Depositing WETH into Session 2 ==='); + // --- 5. Deposit WETH into Session 2 by Wallet 2 --- + console.log('=== Step 5: Depositing WETH into Session 2 ==='); - const session2DepositAmount = new Decimal(0.015); + const session2DepositAmount = new Decimal(0.00015); const session2DepositUpdate: AppStateUpdateV1 = { appSessionId: session2ID, intent: AppStateUpdateIntent.Deposit, @@ -156,12 +218,12 @@ async function main() { }; const session2DepositRequest = packAppStateUpdateV1(session2DepositUpdate); - const wallet2DepositSig = await wallet2Signers.stateSigner.signMessage(session2DepositRequest); - const wallet3DepositSig = await wallet3Signers.stateSigner.signMessage(session2DepositRequest); + const appSession2DepositSig = await appSession2Signer.signMessage(session2DepositRequest); + const appSession3DepositSig = await appSession3Signer.signMessage(session2DepositRequest); const nodeSig = await wallet2Client.submitAppSessionDeposit( session2DepositUpdate, - [wallet2DepositSig, wallet3DepositSig], + [appSession2DepositSig, appSession3DepositSig], 'weth', session2DepositAmount ); @@ -177,117 +239,43 @@ async function main() { ); } - // --- 5. Redistribute within Session 2 (Wallet 2 -> Wallet 3) --- - console.log('=== Step 5: Redistributing funds in Session 2 ==='); + // --- 6. Redistribute within Session 2 (Wallet 2 -> Wallet 3) --- + console.log('=== Step 6: Redistributing funds in Session 2 ==='); const session2RedistributeUpdate: AppStateUpdateV1 = { appSessionId: session2ID, intent: AppStateUpdateIntent.Operate, version: 3n, allocations: [ - { participant: wallet2Address, asset: 'weth', amount: new Decimal(0.01) }, - { participant: wallet3Address, asset: 'weth', amount: new Decimal(0.005) }, + { participant: wallet2Address, asset: 'weth', amount: new Decimal(0.0001) }, + { participant: wallet3Address, asset: 'weth', amount: new Decimal(0.00005) }, ], sessionData: '{}', }; const session2RedistributeRequest = packAppStateUpdateV1(session2RedistributeUpdate); - const wallet2RedistributeSig = await wallet2Signers.stateSigner.signMessage( + const appSession2RedistributeSig = await appSession2Signer.signMessage( session2RedistributeRequest ); - const wallet3RedistributeSig = await wallet3Signers.stateSigner.signMessage( + const appSession3RedistributeSig = await appSession3Signer.signMessage( session2RedistributeRequest ); // Multi-sig required for state transition try { await wallet2Client.submitAppState(session2RedistributeUpdate, [ - wallet2RedistributeSig, - wallet3RedistributeSig, + appSession2RedistributeSig, + appSession3RedistributeSig, ]); - console.log('✓ Redistributed WETH: Wallet 2 (0.01) -> Wallet 3 (0.005)\n'); + console.log('✓ Redistributed WETH: Wallet 2 (0.0001) -> Wallet 3 (0.00005)\n'); } catch (err) { console.error(`Redistribution failed: ${err}`); throw err; } - // --- 6. Rebalance Both App Sessions Atomically --- - console.log('=== Step 6: Atomic Rebalance Across Sessions ==='); - - // Check current allocations before rebalance - const { sessions: session1InfoBeforeRebalance } = await wallet1Client.getAppSessions({ - appSessionId: session1ID, - }); - if (session1InfoBeforeRebalance.length > 0) { - console.log( - `Session 1 before rebalance - Version: ${session1InfoBeforeRebalance[0].version}, Allocations: ${JSON.stringify(session1InfoBeforeRebalance[0].allocations)}` - ); - } - - const { sessions: session2InfoBeforeRebalance } = await wallet2Client.getAppSessions({ - appSessionId: session2ID, - }); - if (session2InfoBeforeRebalance.length > 0) { - console.log( - `Session 2 before rebalance - Version: ${session2InfoBeforeRebalance[0].version}, Allocations: ${JSON.stringify(session2InfoBeforeRebalance[0].allocations)}\n` - ); - } - - // Prepare rebalance updates for both sessions - const session1RebalanceUpdate: AppStateUpdateV1 = { - appSessionId: session1ID, - intent: AppStateUpdateIntent.Rebalance, - version: 3n, - allocations: [ - { participant: wallet1Address, asset: 'weth', amount: new Decimal(0.005) }, - { participant: wallet1Address, asset: 'usdc', amount: new Decimal(0.00005) }, - ], - sessionData: '{}', - }; - - const session1RebalanceRequest = packAppStateUpdateV1(session1RebalanceUpdate); - const wallet1RebalanceSig = await wallet1Signers.stateSigner.signMessage( - session1RebalanceRequest - ); - - const session2RebalanceUpdate: AppStateUpdateV1 = { - appSessionId: session2ID, - intent: AppStateUpdateIntent.Rebalance, - version: 4n, - allocations: [ - { participant: wallet2Address, asset: 'usdc', amount: new Decimal(0.00005) }, - { participant: wallet2Address, asset: 'weth', amount: new Decimal(0.005) }, - { participant: wallet3Address, asset: 'weth', amount: new Decimal(0.005) }, - ], - sessionData: '{}', - }; - - const session2RebalanceRequest = packAppStateUpdateV1(session2RebalanceUpdate); - const wallet2RebalanceSig = await wallet2Signers.stateSigner.signMessage( - session2RebalanceRequest - ); - const wallet3RebalanceSig = await wallet3Signers.stateSigner.signMessage( - session2RebalanceRequest - ); - - // Submit atomic rebalance - const signedRebalanceUpdates: SignedAppStateUpdateV1[] = [ - { - appStateUpdate: session1RebalanceUpdate, - quorumSigs: [wallet1RebalanceSig], - }, - { - appStateUpdate: session2RebalanceUpdate, - quorumSigs: [wallet2RebalanceSig, wallet3RebalanceSig], - }, - ]; - - try { - const rebalanceBatchID = await wallet2Client.rebalanceAppSessions(signedRebalanceUpdates); - console.log(`✓ Atomic Rebalance Submitted. BatchID: ${rebalanceBatchID}\n`); - } catch (err) { - console.log(`⚠ Rebalance Error: ${err}\n`); - } + // NOTE: Rebalance step is disabled. + // // --- 7. Rebalance Both App Sessions Atomically --- + // ... (rebalance code omitted) ... // --- 7. Wallet 3 Withdraws from Session 2 --- console.log('=== Step 7: Wallet 3 Withdrawing from Session 2 ==='); @@ -295,29 +283,28 @@ async function main() { const session2WithdrawUpdate: AppStateUpdateV1 = { appSessionId: session2ID, intent: AppStateUpdateIntent.Withdraw, - version: 5n, + version: 4n, allocations: [ - { participant: wallet2Address, asset: 'usdc', amount: new Decimal(0.00005) }, - { participant: wallet2Address, asset: 'weth', amount: new Decimal(0.005) }, - { participant: wallet3Address, asset: 'weth', amount: new Decimal(0.001) }, + { participant: wallet2Address, asset: 'weth', amount: new Decimal(0.00005) }, + { participant: wallet3Address, asset: 'weth', amount: new Decimal(0.00001) }, ], sessionData: '{}', }; const session2WithdrawRequest = packAppStateUpdateV1(session2WithdrawUpdate); - const wallet2WithdrawSig = await wallet2Signers.stateSigner.signMessage( + const appSession2WithdrawSig = await appSession2Signer.signMessage( session2WithdrawRequest ); - const wallet3WithdrawSig = await wallet3Signers.stateSigner.signMessage( + const appSession3WithdrawSig = await appSession3Signer.signMessage( session2WithdrawRequest ); try { await wallet2Client.submitAppState(session2WithdrawUpdate, [ - wallet2WithdrawSig, - wallet3WithdrawSig, + appSession2WithdrawSig, + appSession3WithdrawSig, ]); - console.log('✓ Wallet 3 successfully withdrew 0.004 WETH back to channel\n'); + console.log('✓ Wallet 3 successfully withdrew WETH back to channel\n'); } catch (err) { console.log(`⚠ Withdraw Error: ${err}\n`); } @@ -329,19 +316,18 @@ async function main() { const session1CloseUpdate: AppStateUpdateV1 = { appSessionId: session1ID, intent: AppStateUpdateIntent.Close, - version: 4n, + version: 3n, allocations: [ - { participant: wallet1Address, asset: 'weth', amount: new Decimal(0.005) }, - { participant: wallet1Address, asset: 'usdc', amount: new Decimal(0.00005) }, + { participant: wallet1Address, asset: 'usdc', amount: new Decimal(0.0001) }, ], sessionData: '{}', }; const session1CloseRequest = packAppStateUpdateV1(session1CloseUpdate); - const wallet1CloseSig = await wallet1Signers.stateSigner.signMessage(session1CloseRequest); + const appSession1CloseSig = await appSession1Signer.signMessage(session1CloseRequest); try { - await wallet1Client.submitAppState(session1CloseUpdate, [wallet1CloseSig]); + await wallet1Client.submitAppState(session1CloseUpdate, [appSession1CloseSig]); console.log('✓ Session 1 successfully closed'); } catch (err) { console.log(`⚠ Close Session 1 Error: ${err}`); @@ -351,34 +337,58 @@ async function main() { const session2CloseUpdate: AppStateUpdateV1 = { appSessionId: session2ID, intent: AppStateUpdateIntent.Close, - version: 6n, + version: 5n, allocations: [ - { participant: wallet2Address, asset: 'usdc', amount: new Decimal(0.00005) }, - { participant: wallet2Address, asset: 'weth', amount: new Decimal(0.005) }, - { participant: wallet3Address, asset: 'weth', amount: new Decimal(0.001) }, + { participant: wallet2Address, asset: 'weth', amount: new Decimal(0.00005) }, + { participant: wallet3Address, asset: 'weth', amount: new Decimal(0.00001) }, ], sessionData: '{}', }; const session2CloseRequest = packAppStateUpdateV1(session2CloseUpdate); - const wallet2CloseSig = await wallet2Signers.stateSigner.signMessage(session2CloseRequest); - const wallet3CloseSig = await wallet3Signers.stateSigner.signMessage(session2CloseRequest); + const appSession2CloseSig = await appSession2Signer.signMessage(session2CloseRequest); + const appSession3CloseSig = await appSession3Signer.signMessage(session2CloseRequest); try { await wallet2Client.submitAppState(session2CloseUpdate, [ - wallet2CloseSig, - wallet3CloseSig, + appSession2CloseSig, + appSession3CloseSig, ]); console.log('✓ Session 2 successfully closed'); } catch (err) { console.log(`⚠ Close Session 2 Error: ${err}`); } + // --- 9. Fail Case: Create App Session for Unregistered App --- + console.log('\n=== Step 9: Creating App Session for Unregistered App (expected to fail) ==='); + + const unregisteredDefinition: AppDefinitionV1 = { + applicationId: `unregistered-app-${suffix}`, + participants: [{ walletAddress: wallet1Address, signatureWeight: 100 }], + quorum: 100, + nonce: BigInt(Date.now() * 1000000), + }; + + const unregisteredCreateRequest = packCreateAppSessionRequestV1(unregisteredDefinition, '{}'); + const unregisteredSig = await appSession1Signer.signMessage(unregisteredCreateRequest); + + try { + await wallet1Client.createAppSession( + unregisteredDefinition, + '{}', + [unregisteredSig] + ); + console.log('✗ Unexpected success: app session was created for unregistered app'); + } catch (err) { + console.log(`✓ Expected error: ${err}`); + } + console.log('\n=== Example Complete ==='); // Close clients await wallet1Client.close(); await wallet2Client.close(); + await wallet3Client.close(); // Exit successfully process.exit(0); diff --git a/sdk/ts/examples/example-app/README.md b/sdk/ts/examples/example-app/README.md index e4bfe9b62..f1d4011e9 100644 --- a/sdk/ts/examples/example-app/README.md +++ b/sdk/ts/examples/example-app/README.md @@ -102,7 +102,7 @@ const txSigner = new WalletTransactionSigner(walletClient); // Connect const client = await Client.create( - 'wss://clearnode-v1-rc.yellow.org/ws', + 'wss://clearnode-sandbox.yellow.org/v1/ws', stateSigner, txSigner, withBlockchainRPC(11155111n, 'https://ethereum-sepolia-rpc.publicnode.com'), @@ -276,7 +276,7 @@ const sessionSigner = new ChannelSessionKeyStateSigner( const txSigner = new WalletTransactionSigner(walletClient); const sessionClient = await Client.create( - 'wss://clearnode-v1-rc.yellow.org/ws', + 'wss://clearnode-sandbox.yellow.org/v1/ws', sessionSigner, txSigner, withBlockchainRPC(11155111n, 'https://ethereum-sepolia-rpc.publicnode.com'), diff --git a/sdk/ts/examples/example-app/package-lock.json b/sdk/ts/examples/example-app/package-lock.json index 64a35a32b..48ff3c2ac 100644 --- a/sdk/ts/examples/example-app/package-lock.json +++ b/sdk/ts/examples/example-app/package-lock.json @@ -34,7 +34,7 @@ }, "../..": { "name": "@yellow-org/sdk", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "dependencies": { "abitype": "^1.2.3", diff --git a/sdk/ts/examples/example-app/src/App.tsx b/sdk/ts/examples/example-app/src/App.tsx index db3f2834c..4c481b8ed 100644 --- a/sdk/ts/examples/example-app/src/App.tsx +++ b/sdk/ts/examples/example-app/src/App.tsx @@ -10,7 +10,7 @@ import type { AppState, ClearnodeConfig, NetworkConfig, SessionKeyState, StatusM import { formatAddress } from './utils'; const CLEARNODES: ClearnodeConfig[] = [ - { name: 'YN Testnet', url: 'wss://clearnode-v1-rc.yellow.org/ws' }, + { name: 'YN Testnet', url: 'wss://clearnode-sandbox.yellow.org/v1/ws' }, ]; const NETWORKS: NetworkConfig[] = [ @@ -272,9 +272,10 @@ function App() { const currentNetwork = NETWORKS.find(n => n.chainId === appState.selectedChainId); return ( -
+
+ {/* Header */} -
+
@@ -301,14 +302,14 @@ function App() { {/* Network indicator */} {currentNetwork && ( - + {currentNetwork.name} )} {/* Wallet */} {autoConnecting ? ( -
+
Connecting...
@@ -321,7 +322,7 @@ function App() { ) : (
-
+
{formatAddress(appState.address)}
@@ -343,7 +344,7 @@ function App() { {status && setStatus(null)} />} {/* Main Content */} -
+
{appState.connected && appState.client && appState.address && walletClient ? (
+ ) : !autoConnecting && appState.address && !appState.connected ? ( +
+
+

+ Connection Failed +

+

+ Wallet connected as {formatAddress(appState.address)} but the clearnode is unreachable. +

+
+ + +
+
+
) : null} {/* Footer */} -